1# Copyright (c) 2013 ARM Limited
2# All rights reserved
3#
4# The license below extends only to copyright in the software and shall
5# not be construed as granting a license to any other intellectual
6# property including but not limited to intellectual property relating
7# to a hardware implementation of the functionality of the software
8# licensed hereunder.  You may use the software subject to the license
9# terms below provided that you ensure that this notice is replicated
10# unmodified and in its entirety in all distributions of the software,
11# modified or unmodified, in source code or in binary form.
12#
13# Redistribution and use in source and binary forms, with or without
14# modification, are permitted provided that the following conditions are
15# met: redistributions of source code must retain the above copyright
16# notice, this list of conditions and the following disclaimer;
17# redistributions in binary form must reproduce the above copyright
18# notice, this list of conditions and the following disclaimer in the
19# documentation and/or other materials provided with the distribution;
20# neither the name of the copyright holders nor the names of its
21# contributors may be used to endorse or promote products derived from
22# this software without specific prior written permission.
23#
24# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35#
36# Authors: Andrew Bardsley
37
38import pygtk
39pygtk.require('2.0')
40import gtk
41import gobject
42import cairo
43import re
44
45from point import Point
46import parse
47import colours
48import model
49from model import Id, BlobModel, BlobDataSelect, special_state_chars
50import blobs
51
52class BlobView(object):
53    """The canvas view of the pipeline"""
54    def __init__(self, model):
55        # A unit blob will appear at size blobSize inside a space of
56        #   size pitch.
57        self.blobSize = Point(45.0, 45.0)
58        self.pitch = Point(60.0, 60.0)
59        self.origin = Point(50.0, 50.0)
60        # Some common line definitions to cut down on arbitrary
61        #   set_line_widths
62        self.thickLineWidth = 10.0
63        self.thinLineWidth = 4.0
64        self.midLineWidth = 6.0
65        # The scale from the units of pitch to device units (nominally
66        #   pixels for 1.0 to 1.0
67        self.masterScale = Point(1.0,1.0)
68        self.model = model
69        self.fillColour = colours.emptySlotColour
70        self.timeIndex = 0
71        self.time = 0
72        self.positions = []
73        self.controlbar = None
74        # The sequence number selector state
75        self.dataSelect = BlobDataSelect()
76        # Offset of this view's time from self.time used for miniviews
77        #   This is actually an offset of the index into the array of times
78        #   seen in the event file)
79        self.timeOffset = 0
80        # Maximum view size for initial window mapping
81        self.initialHeight = 600.0
82
83        # Overlays are speech bubbles explaining blob data
84        self.overlays = []
85
86        self.da = gtk.DrawingArea()
87        def draw(arg1, arg2):
88            self.redraw()
89        self.da.connect('expose_event', draw)
90
91        # Handy offsets from the blob size
92        self.blobIndent = (self.pitch - self.blobSize).scale(0.5)
93        self.blobIndentFactor = self.blobIndent / self.pitch
94
95    def add_control_bar(self, controlbar):
96        """Add a BlobController to this view"""
97        self.controlbar = controlbar
98
99    def draw_to_png(self, filename):
100        """Draw the view to a PNG file"""
101        surface = cairo.ImageSurface(
102            cairo.FORMAT_ARGB32,
103            self.da.get_allocation().width,
104            self.da.get_allocation().height)
105        cr = gtk.gdk.CairoContext(cairo.Context(surface))
106        self.draw_to_cr(cr)
107        surface.write_to_png(filename)
108
109    def draw_to_cr(self, cr):
110        """Draw to a given CairoContext"""
111        cr.set_source_color(colours.backgroundColour)
112        cr.set_line_width(self.thickLineWidth)
113        cr.paint()
114        cr.save()
115        cr.scale(*self.masterScale.to_pair())
116        cr.translate(*self.origin.to_pair())
117
118        positions = [] # {}
119
120        # Draw each blob
121        for blob in self.model.blobs:
122            blob_event = self.model.find_unit_event_by_time(
123                blob.unit, self.time)
124
125            cr.save()
126            pos = blob.render(cr, self, blob_event, self.dataSelect,
127                self.time)
128            cr.restore()
129            if pos is not None:
130                (centre, size) = pos
131                positions.append((blob, centre, size))
132
133        # Draw all the overlays over the top
134        for overlay in self.overlays:
135            overlay.show(cr)
136
137        cr.restore()
138
139        return positions
140
141    def redraw(self):
142        """Redraw the whole view"""
143        buffer = cairo.ImageSurface(
144            cairo.FORMAT_ARGB32,
145            self.da.get_allocation().width,
146            self.da.get_allocation().height)
147
148        cr = gtk.gdk.CairoContext(cairo.Context(buffer))
149        positions = self.draw_to_cr(cr)
150
151        # Assume that blobs are in order for depth so we want to
152        #   hit the frontmost blob first if we search by position
153        positions.reverse()
154        self.positions = positions
155
156        # Paint the drawn buffer onto the DrawingArea
157        dacr = self.da.window.cairo_create()
158        dacr.set_source_surface(buffer, 0.0, 0.0)
159        dacr.paint()
160
161        buffer.finish()
162
163    def set_time_index(self, time):
164        """Set the time index for the view.  A time index is an index into
165        the model's times array of seen event times"""
166        self.timeIndex = time + self.timeOffset
167        if len(self.model.times) != 0:
168            if self.timeIndex >= len(self.model.times):
169                self.time = self.model.times[len(self.model.times) - 1]
170            else:
171                self.time = self.model.times[self.timeIndex]
172        else:
173            self.time = 0
174
175    def get_pic_size(self):
176        """Return the size of ASCII-art picture of the pipeline scaled by
177        the blob pitch"""
178        return (self.origin + self.pitch *
179            (self.model.picSize + Point(1.0,1.0)))
180
181    def set_da_size(self):
182        """Set the DrawingArea size after scaling"""
183        self.da.set_size_request(10 , int(self.initialHeight))
184
185class BlobController(object):
186    """The controller bar for the viewer"""
187    def __init__(self, model, view,
188        defaultEventFile="", defaultPictureFile=""):
189        self.model = model
190        self.view = view
191        self.playTimer = None
192        self.filenameEntry = gtk.Entry()
193        self.filenameEntry.set_text(defaultEventFile)
194        self.pictureEntry = gtk.Entry()
195        self.pictureEntry.set_text(defaultPictureFile)
196        self.timeEntry = None
197        self.defaultEventFile = defaultEventFile
198        self.startTime = None
199        self.endTime = None
200
201        self.otherViews = []
202
203        def make_bar(elems):
204            box = gtk.HBox(homogeneous=False, spacing=2)
205            box.set_border_width(2)
206            for widget, signal, handler in elems:
207                if signal is not None:
208                    widget.connect(signal, handler)
209                box.pack_start(widget, False, True, 0)
210            return box
211
212        self.timeEntry = gtk.Entry()
213
214        t = gtk.ToggleButton('T')
215        t.set_active(False)
216        s = gtk.ToggleButton('S')
217        s.set_active(True)
218        p = gtk.ToggleButton('P')
219        p.set_active(True)
220        l = gtk.ToggleButton('L')
221        l.set_active(True)
222        f = gtk.ToggleButton('F')
223        f.set_active(True)
224        e = gtk.ToggleButton('E')
225        e.set_active(True)
226
227        # Should really generate this from above
228        self.view.dataSelect.ids = set("SPLFE")
229
230        self.bar = gtk.VBox()
231        self.bar.set_homogeneous(False)
232
233        row1 = make_bar([
234            (gtk.Button('Start'), 'clicked', self.time_start),
235            (gtk.Button('End'), 'clicked', self.time_end),
236            (gtk.Button('Back'), 'clicked', self.time_back),
237            (gtk.Button('Forward'), 'clicked', self.time_forward),
238            (gtk.Button('Play'), 'clicked', self.time_play),
239            (gtk.Button('Stop'), 'clicked', self.time_stop),
240            (self.timeEntry, 'activate', self.time_set),
241            (gtk.Label('Visible ids:'), None, None),
242            (t, 'clicked', self.toggle_id('T')),
243            (gtk.Label('/'), None, None),
244            (s, 'clicked', self.toggle_id('S')),
245            (gtk.Label('.'), None, None),
246            (p, 'clicked', self.toggle_id('P')),
247            (gtk.Label('/'), None, None),
248            (l, 'clicked', self.toggle_id('L')),
249            (gtk.Label('/'), None, None),
250            (f, 'clicked', self.toggle_id('F')),
251            (gtk.Label('.'), None, None),
252            (e, 'clicked', self.toggle_id('E')),
253            (self.filenameEntry, 'activate', self.load_events),
254            (gtk.Button('Reload'), 'clicked', self.load_events)
255            ])
256
257        self.bar.pack_start(row1, False, True, 0)
258        self.set_time_index(0)
259
260    def toggle_id(self, id):
261        """One of the sequence number selector buttons has been toggled"""
262        def toggle(button):
263            if button.get_active():
264                self.view.dataSelect.ids.add(id)
265            else:
266                self.view.dataSelect.ids.discard(id)
267
268            # Always leave one thing visible
269            if len(self.view.dataSelect.ids) == 0:
270                self.view.dataSelect.ids.add(id)
271                button.set_active(True)
272            self.view.redraw()
273        return toggle
274
275    def set_time_index(self, time):
276        """Set the time index in the view"""
277        self.view.set_time_index(time)
278
279        for view in self.otherViews:
280            view.set_time_index(time)
281            view.redraw()
282
283        self.timeEntry.set_text(str(self.view.time))
284
285    def time_start(self, button):
286        """Start pressed"""
287        self.set_time_index(0)
288        self.view.redraw()
289
290    def time_end(self, button):
291        """End pressed"""
292        self.set_time_index(len(self.model.times) - 1)
293        self.view.redraw()
294
295    def time_forward(self, button):
296        """Step forward pressed"""
297        self.set_time_index(min(self.view.timeIndex + 1,
298            len(self.model.times) - 1))
299        self.view.redraw()
300        gtk.gdk.flush()
301
302    def time_back(self, button):
303        """Step back pressed"""
304        self.set_time_index(max(self.view.timeIndex - 1, 0))
305        self.view.redraw()
306
307    def time_set(self, entry):
308        """Time dialogue changed.  Need to find a suitable time
309        <= the entry's time"""
310        newTime = self.model.find_time_index(int(entry.get_text()))
311        self.set_time_index(newTime)
312        self.view.redraw()
313
314    def time_step(self):
315        """Time step while playing"""
316        if not self.playTimer \
317            or self.view.timeIndex == len(self.model.times) - 1:
318            self.time_stop(None)
319            return False
320        else:
321            self.time_forward(None)
322            return True
323
324    def time_play(self, play):
325        """Automatically advance time every 100 ms"""
326        if not self.playTimer:
327            self.playTimer = gobject.timeout_add(100, self.time_step)
328
329    def time_stop(self, play):
330        """Stop play pressed"""
331        if self.playTimer:
332            gobject.source_remove(self.playTimer)
333            self.playTimer = None
334
335    def load_events(self, button):
336        """Reload events file"""
337        self.model.load_events(self.filenameEntry.get_text(),
338            startTime=self.startTime, endTime=self.endTime)
339        self.set_time_index(min(len(self.model.times) - 1,
340            self.view.timeIndex))
341        self.view.redraw()
342
343class Overlay(object):
344    """An Overlay is a speech bubble explaining the data in a blob"""
345    def __init__(self, model, view, point, blob):
346        self.model = model
347        self.view = view
348        self.point = point
349        self.blob = blob
350
351    def find_event(self):
352        """Find the event for a changing time and a fixed blob"""
353        return self.model.find_unit_event_by_time(self.blob.unit,
354            self.view.time)
355
356    def show(self, cr):
357        """Draw the overlay"""
358        event = self.find_event()
359
360        if event is None:
361            return
362
363        insts = event.find_ided_objects(self.model, self.blob.picChar,
364            False)
365
366        cr.set_line_width(self.view.thinLineWidth)
367        cr.translate(*(Point(0.0,0.0) - self.view.origin).to_pair())
368        cr.scale(*(Point(1.0,1.0) / self.view.masterScale).to_pair())
369
370        # Get formatted data from the insts to format into a table
371        lines = list(inst.table_line() for inst in insts)
372
373        text_size = 10.0
374        cr.set_font_size(text_size)
375
376        def text_width(str):
377            xb, yb, width, height, dx, dy = cr.text_extents(str)
378            return width
379
380        # Find the maximum number of columns and the widths of each column
381        num_columns = 0
382        for line in lines:
383            num_columns = max(num_columns, len(line))
384
385        widths = [0] * num_columns
386        for line in lines:
387            for i in xrange(0, len(line)):
388                widths[i] = max(widths[i], text_width(line[i]))
389
390        # Calculate the size of the speech bubble
391        column_gap = 1 * text_size
392        id_width = 6 * text_size
393        total_width = sum(widths) + id_width + column_gap * (num_columns + 1)
394        gap_step = Point(1.0, 0.0).scale(column_gap)
395
396        text_point = self.point
397        text_step = Point(0.0, text_size)
398
399        size = Point(total_width, text_size * len(insts))
400
401        # Draw the speech bubble
402        blobs.speech_bubble(cr, self.point, size, text_size)
403        cr.set_source_color(colours.backgroundColour)
404        cr.fill_preserve()
405        cr.set_source_color(colours.black)
406        cr.stroke()
407
408        text_point += Point(1.0,1.0).scale(2.0 * text_size)
409
410        id_size = Point(id_width, text_size)
411
412        # Draw the rows in the table
413        for i in xrange(0, len(insts)):
414            row_point = text_point
415            inst = insts[i]
416            line = lines[i]
417            blobs.striped_box(cr, row_point + id_size.scale(0.5),
418                id_size, inst.id.to_striped_block(self.view.dataSelect))
419            cr.set_source_color(colours.black)
420
421            row_point += Point(1.0, 0.0).scale(id_width)
422            row_point += text_step
423            # Draw the columns of each row
424            for j in xrange(0, len(line)):
425                row_point += gap_step
426                cr.move_to(*row_point.to_pair())
427                cr.show_text(line[j])
428                row_point += Point(1.0, 0.0).scale(widths[j])
429
430            text_point += text_step
431
432class BlobWindow(object):
433    """The top-level window and its mouse control"""
434    def __init__(self, model, view, controller):
435        self.model = model
436        self.view = view
437        self.controller = controller
438        self.controlbar = None
439        self.window = None
440        self.miniViewCount = 0
441
442    def add_control_bar(self, controlbar):
443        self.controlbar = controlbar
444
445    def show_window(self):
446        self.window = gtk.Window()
447
448        self.vbox = gtk.VBox()
449        self.vbox.set_homogeneous(False)
450        if self.controlbar:
451            self.vbox.pack_start(self.controlbar, False, True, 0)
452        self.vbox.add(self.view.da)
453
454        if self.miniViewCount > 0:
455            self.miniViews = []
456            self.miniViewHBox = gtk.HBox(homogeneous=True, spacing=2)
457
458            # Draw mini views
459            for i in xrange(1, self.miniViewCount + 1):
460                miniView = BlobView(self.model)
461                miniView.set_time_index(0)
462                miniView.masterScale = Point(0.1, 0.1)
463                miniView.set_da_size()
464                miniView.timeOffset = i + 1
465                self.miniViews.append(miniView)
466                self.miniViewHBox.pack_start(miniView.da, False, True, 0)
467
468            self.controller.otherViews = self.miniViews
469            self.vbox.add(self.miniViewHBox)
470
471        self.window.add(self.vbox)
472
473        def show_event(picChar, event):
474            print '**** Comments for', event.unit, \
475                'at time', self.view.time
476            for name, value in event.pairs.iteritems():
477                print name, '=', value
478            for comment in event.comments:
479                print comment
480            if picChar in event.visuals:
481                # blocks = event.visuals[picChar].elems()
482                print '**** Colour data'
483                objs = event.find_ided_objects(self.model, picChar, True)
484                for obj in objs:
485                    print ' '.join(obj.table_line())
486
487        def clicked_da(da, b):
488            point = Point(b.x, b.y)
489
490            overlay = None
491            for blob, centre, size in self.view.positions:
492                if point.is_within_box((centre, size)):
493                    event = self.model.find_unit_event_by_time(blob.unit,
494                        self.view.time)
495                    if event is not None:
496                        if overlay is None:
497                            overlay = Overlay(self.model, self.view, point,
498                                blob)
499                        show_event(blob.picChar, event)
500            if overlay is not None:
501                self.view.overlays = [overlay]
502            else:
503                self.view.overlays = []
504
505            self.view.redraw()
506
507        # Set initial size and event callbacks
508        self.view.set_da_size()
509        self.view.da.add_events(gtk.gdk.BUTTON_PRESS_MASK)
510        self.view.da.connect('button-press-event', clicked_da)
511        self.window.connect('destroy', lambda(widget): gtk.main_quit())
512
513        def resize(window, event):
514            """Resize DrawingArea to match new window size"""
515            size = Point(float(event.width), float(event.height))
516            proportion = size / self.view.get_pic_size()
517            # Preserve aspect ratio
518            daScale = min(proportion.x, proportion.y)
519            self.view.masterScale = Point(daScale, daScale)
520            self.view.overlays = []
521
522        self.view.da.connect('configure-event', resize)
523
524        self.window.show_all()
525