| # Copyright (c) 2013 ARM Limited | |
| # All rights reserved | |
| # | |
| # The license below extends only to copyright in the software and shall | |
| # not be construed as granting a license to any other intellectual | |
| # property including but not limited to intellectual property relating | |
| # to a hardware implementation of the functionality of the software | |
| # licensed hereunder. You may use the software subject to the license | |
| # terms below provided that you ensure that this notice is replicated | |
| # unmodified and in its entirety in all distributions of the software, | |
| # modified or unmodified, in source code or in binary form. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions are | |
| # met: redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer; | |
| # redistributions in binary form must reproduce the above copyright | |
| # notice, this list of conditions and the following disclaimer in the | |
| # documentation and/or other materials provided with the distribution; | |
| # neither the name of the copyright holders nor the names of its | |
| # contributors may be used to endorse or promote products derived from | |
| # this software without specific prior written permission. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| # | |
| # Authors: Andrew Bardsley | |
| import pygtk | |
| pygtk.require('2.0') | |
| import gtk | |
| import gobject | |
| import cairo | |
| import re | |
| from point import Point | |
| import parse | |
| import colours | |
| import model | |
| from model import Id, BlobModel, BlobDataSelect, special_state_chars | |
| import blobs | |
| class BlobView(object): | |
| """The canvas view of the pipeline""" | |
| def __init__(self, model): | |
| # A unit blob will appear at size blobSize inside a space of | |
| # size pitch. | |
| self.blobSize = Point(45.0, 45.0) | |
| self.pitch = Point(60.0, 60.0) | |
| self.origin = Point(50.0, 50.0) | |
| # Some common line definitions to cut down on arbitrary | |
| # set_line_widths | |
| self.thickLineWidth = 10.0 | |
| self.thinLineWidth = 4.0 | |
| self.midLineWidth = 6.0 | |
| # The scale from the units of pitch to device units (nominally | |
| # pixels for 1.0 to 1.0 | |
| self.masterScale = Point(1.0,1.0) | |
| self.model = model | |
| self.fillColour = colours.emptySlotColour | |
| self.timeIndex = 0 | |
| self.time = 0 | |
| self.positions = [] | |
| self.controlbar = None | |
| # The sequence number selector state | |
| self.dataSelect = BlobDataSelect() | |
| # Offset of this view's time from self.time used for miniviews | |
| # This is actually an offset of the index into the array of times | |
| # seen in the event file) | |
| self.timeOffset = 0 | |
| # Maximum view size for initial window mapping | |
| self.initialHeight = 600.0 | |
| # Overlays are speech bubbles explaining blob data | |
| self.overlays = [] | |
| self.da = gtk.DrawingArea() | |
| def draw(arg1, arg2): | |
| self.redraw() | |
| self.da.connect('expose_event', draw) | |
| # Handy offsets from the blob size | |
| self.blobIndent = (self.pitch - self.blobSize).scale(0.5) | |
| self.blobIndentFactor = self.blobIndent / self.pitch | |
| def add_control_bar(self, controlbar): | |
| """Add a BlobController to this view""" | |
| self.controlbar = controlbar | |
| def draw_to_png(self, filename): | |
| """Draw the view to a PNG file""" | |
| surface = cairo.ImageSurface( | |
| cairo.FORMAT_ARGB32, | |
| self.da.get_allocation().width, | |
| self.da.get_allocation().height) | |
| cr = gtk.gdk.CairoContext(cairo.Context(surface)) | |
| self.draw_to_cr(cr) | |
| surface.write_to_png(filename) | |
| def draw_to_cr(self, cr): | |
| """Draw to a given CairoContext""" | |
| cr.set_source_color(colours.backgroundColour) | |
| cr.set_line_width(self.thickLineWidth) | |
| cr.paint() | |
| cr.save() | |
| cr.scale(*self.masterScale.to_pair()) | |
| cr.translate(*self.origin.to_pair()) | |
| positions = [] # {} | |
| # Draw each blob | |
| for blob in self.model.blobs: | |
| blob_event = self.model.find_unit_event_by_time( | |
| blob.unit, self.time) | |
| cr.save() | |
| pos = blob.render(cr, self, blob_event, self.dataSelect, | |
| self.time) | |
| cr.restore() | |
| if pos is not None: | |
| (centre, size) = pos | |
| positions.append((blob, centre, size)) | |
| # Draw all the overlays over the top | |
| for overlay in self.overlays: | |
| overlay.show(cr) | |
| cr.restore() | |
| return positions | |
| def redraw(self): | |
| """Redraw the whole view""" | |
| buffer = cairo.ImageSurface( | |
| cairo.FORMAT_ARGB32, | |
| self.da.get_allocation().width, | |
| self.da.get_allocation().height) | |
| cr = gtk.gdk.CairoContext(cairo.Context(buffer)) | |
| positions = self.draw_to_cr(cr) | |
| # Assume that blobs are in order for depth so we want to | |
| # hit the frontmost blob first if we search by position | |
| positions.reverse() | |
| self.positions = positions | |
| # Paint the drawn buffer onto the DrawingArea | |
| dacr = self.da.window.cairo_create() | |
| dacr.set_source_surface(buffer, 0.0, 0.0) | |
| dacr.paint() | |
| buffer.finish() | |
| def set_time_index(self, time): | |
| """Set the time index for the view. A time index is an index into | |
| the model's times array of seen event times""" | |
| self.timeIndex = time + self.timeOffset | |
| if len(self.model.times) != 0: | |
| if self.timeIndex >= len(self.model.times): | |
| self.time = self.model.times[len(self.model.times) - 1] | |
| else: | |
| self.time = self.model.times[self.timeIndex] | |
| else: | |
| self.time = 0 | |
| def get_pic_size(self): | |
| """Return the size of ASCII-art picture of the pipeline scaled by | |
| the blob pitch""" | |
| return (self.origin + self.pitch * | |
| (self.model.picSize + Point(1.0,1.0))) | |
| def set_da_size(self): | |
| """Set the DrawingArea size after scaling""" | |
| self.da.set_size_request(10 , int(self.initialHeight)) | |
| class BlobController(object): | |
| """The controller bar for the viewer""" | |
| def __init__(self, model, view, | |
| defaultEventFile="", defaultPictureFile=""): | |
| self.model = model | |
| self.view = view | |
| self.playTimer = None | |
| self.filenameEntry = gtk.Entry() | |
| self.filenameEntry.set_text(defaultEventFile) | |
| self.pictureEntry = gtk.Entry() | |
| self.pictureEntry.set_text(defaultPictureFile) | |
| self.timeEntry = None | |
| self.defaultEventFile = defaultEventFile | |
| self.startTime = None | |
| self.endTime = None | |
| self.otherViews = [] | |
| def make_bar(elems): | |
| box = gtk.HBox(homogeneous=False, spacing=2) | |
| box.set_border_width(2) | |
| for widget, signal, handler in elems: | |
| if signal is not None: | |
| widget.connect(signal, handler) | |
| box.pack_start(widget, False, True, 0) | |
| return box | |
| self.timeEntry = gtk.Entry() | |
| t = gtk.ToggleButton('T') | |
| t.set_active(False) | |
| s = gtk.ToggleButton('S') | |
| s.set_active(True) | |
| p = gtk.ToggleButton('P') | |
| p.set_active(True) | |
| l = gtk.ToggleButton('L') | |
| l.set_active(True) | |
| f = gtk.ToggleButton('F') | |
| f.set_active(True) | |
| e = gtk.ToggleButton('E') | |
| e.set_active(True) | |
| # Should really generate this from above | |
| self.view.dataSelect.ids = set("SPLFE") | |
| self.bar = gtk.VBox() | |
| self.bar.set_homogeneous(False) | |
| row1 = make_bar([ | |
| (gtk.Button('Start'), 'clicked', self.time_start), | |
| (gtk.Button('End'), 'clicked', self.time_end), | |
| (gtk.Button('Back'), 'clicked', self.time_back), | |
| (gtk.Button('Forward'), 'clicked', self.time_forward), | |
| (gtk.Button('Play'), 'clicked', self.time_play), | |
| (gtk.Button('Stop'), 'clicked', self.time_stop), | |
| (self.timeEntry, 'activate', self.time_set), | |
| (gtk.Label('Visible ids:'), None, None), | |
| (t, 'clicked', self.toggle_id('T')), | |
| (gtk.Label('/'), None, None), | |
| (s, 'clicked', self.toggle_id('S')), | |
| (gtk.Label('.'), None, None), | |
| (p, 'clicked', self.toggle_id('P')), | |
| (gtk.Label('/'), None, None), | |
| (l, 'clicked', self.toggle_id('L')), | |
| (gtk.Label('/'), None, None), | |
| (f, 'clicked', self.toggle_id('F')), | |
| (gtk.Label('.'), None, None), | |
| (e, 'clicked', self.toggle_id('E')), | |
| (self.filenameEntry, 'activate', self.load_events), | |
| (gtk.Button('Reload'), 'clicked', self.load_events) | |
| ]) | |
| self.bar.pack_start(row1, False, True, 0) | |
| self.set_time_index(0) | |
| def toggle_id(self, id): | |
| """One of the sequence number selector buttons has been toggled""" | |
| def toggle(button): | |
| if button.get_active(): | |
| self.view.dataSelect.ids.add(id) | |
| else: | |
| self.view.dataSelect.ids.discard(id) | |
| # Always leave one thing visible | |
| if len(self.view.dataSelect.ids) == 0: | |
| self.view.dataSelect.ids.add(id) | |
| button.set_active(True) | |
| self.view.redraw() | |
| return toggle | |
| def set_time_index(self, time): | |
| """Set the time index in the view""" | |
| self.view.set_time_index(time) | |
| for view in self.otherViews: | |
| view.set_time_index(time) | |
| view.redraw() | |
| self.timeEntry.set_text(str(self.view.time)) | |
| def time_start(self, button): | |
| """Start pressed""" | |
| self.set_time_index(0) | |
| self.view.redraw() | |
| def time_end(self, button): | |
| """End pressed""" | |
| self.set_time_index(len(self.model.times) - 1) | |
| self.view.redraw() | |
| def time_forward(self, button): | |
| """Step forward pressed""" | |
| self.set_time_index(min(self.view.timeIndex + 1, | |
| len(self.model.times) - 1)) | |
| self.view.redraw() | |
| gtk.gdk.flush() | |
| def time_back(self, button): | |
| """Step back pressed""" | |
| self.set_time_index(max(self.view.timeIndex - 1, 0)) | |
| self.view.redraw() | |
| def time_set(self, entry): | |
| """Time dialogue changed. Need to find a suitable time | |
| <= the entry's time""" | |
| newTime = self.model.find_time_index(int(entry.get_text())) | |
| self.set_time_index(newTime) | |
| self.view.redraw() | |
| def time_step(self): | |
| """Time step while playing""" | |
| if not self.playTimer \ | |
| or self.view.timeIndex == len(self.model.times) - 1: | |
| self.time_stop(None) | |
| return False | |
| else: | |
| self.time_forward(None) | |
| return True | |
| def time_play(self, play): | |
| """Automatically advance time every 100 ms""" | |
| if not self.playTimer: | |
| self.playTimer = gobject.timeout_add(100, self.time_step) | |
| def time_stop(self, play): | |
| """Stop play pressed""" | |
| if self.playTimer: | |
| gobject.source_remove(self.playTimer) | |
| self.playTimer = None | |
| def load_events(self, button): | |
| """Reload events file""" | |
| self.model.load_events(self.filenameEntry.get_text(), | |
| startTime=self.startTime, endTime=self.endTime) | |
| self.set_time_index(min(len(self.model.times) - 1, | |
| self.view.timeIndex)) | |
| self.view.redraw() | |
| class Overlay(object): | |
| """An Overlay is a speech bubble explaining the data in a blob""" | |
| def __init__(self, model, view, point, blob): | |
| self.model = model | |
| self.view = view | |
| self.point = point | |
| self.blob = blob | |
| def find_event(self): | |
| """Find the event for a changing time and a fixed blob""" | |
| return self.model.find_unit_event_by_time(self.blob.unit, | |
| self.view.time) | |
| def show(self, cr): | |
| """Draw the overlay""" | |
| event = self.find_event() | |
| if event is None: | |
| return | |
| insts = event.find_ided_objects(self.model, self.blob.picChar, | |
| False) | |
| cr.set_line_width(self.view.thinLineWidth) | |
| cr.translate(*(Point(0.0,0.0) - self.view.origin).to_pair()) | |
| cr.scale(*(Point(1.0,1.0) / self.view.masterScale).to_pair()) | |
| # Get formatted data from the insts to format into a table | |
| lines = list(inst.table_line() for inst in insts) | |
| text_size = 10.0 | |
| cr.set_font_size(text_size) | |
| def text_width(str): | |
| xb, yb, width, height, dx, dy = cr.text_extents(str) | |
| return width | |
| # Find the maximum number of columns and the widths of each column | |
| num_columns = 0 | |
| for line in lines: | |
| num_columns = max(num_columns, len(line)) | |
| widths = [0] * num_columns | |
| for line in lines: | |
| for i in xrange(0, len(line)): | |
| widths[i] = max(widths[i], text_width(line[i])) | |
| # Calculate the size of the speech bubble | |
| column_gap = 1 * text_size | |
| id_width = 6 * text_size | |
| total_width = sum(widths) + id_width + column_gap * (num_columns + 1) | |
| gap_step = Point(1.0, 0.0).scale(column_gap) | |
| text_point = self.point | |
| text_step = Point(0.0, text_size) | |
| size = Point(total_width, text_size * len(insts)) | |
| # Draw the speech bubble | |
| blobs.speech_bubble(cr, self.point, size, text_size) | |
| cr.set_source_color(colours.backgroundColour) | |
| cr.fill_preserve() | |
| cr.set_source_color(colours.black) | |
| cr.stroke() | |
| text_point += Point(1.0,1.0).scale(2.0 * text_size) | |
| id_size = Point(id_width, text_size) | |
| # Draw the rows in the table | |
| for i in xrange(0, len(insts)): | |
| row_point = text_point | |
| inst = insts[i] | |
| line = lines[i] | |
| blobs.striped_box(cr, row_point + id_size.scale(0.5), | |
| id_size, inst.id.to_striped_block(self.view.dataSelect)) | |
| cr.set_source_color(colours.black) | |
| row_point += Point(1.0, 0.0).scale(id_width) | |
| row_point += text_step | |
| # Draw the columns of each row | |
| for j in xrange(0, len(line)): | |
| row_point += gap_step | |
| cr.move_to(*row_point.to_pair()) | |
| cr.show_text(line[j]) | |
| row_point += Point(1.0, 0.0).scale(widths[j]) | |
| text_point += text_step | |
| class BlobWindow(object): | |
| """The top-level window and its mouse control""" | |
| def __init__(self, model, view, controller): | |
| self.model = model | |
| self.view = view | |
| self.controller = controller | |
| self.controlbar = None | |
| self.window = None | |
| self.miniViewCount = 0 | |
| def add_control_bar(self, controlbar): | |
| self.controlbar = controlbar | |
| def show_window(self): | |
| self.window = gtk.Window() | |
| self.vbox = gtk.VBox() | |
| self.vbox.set_homogeneous(False) | |
| if self.controlbar: | |
| self.vbox.pack_start(self.controlbar, False, True, 0) | |
| self.vbox.add(self.view.da) | |
| if self.miniViewCount > 0: | |
| self.miniViews = [] | |
| self.miniViewHBox = gtk.HBox(homogeneous=True, spacing=2) | |
| # Draw mini views | |
| for i in xrange(1, self.miniViewCount + 1): | |
| miniView = BlobView(self.model) | |
| miniView.set_time_index(0) | |
| miniView.masterScale = Point(0.1, 0.1) | |
| miniView.set_da_size() | |
| miniView.timeOffset = i + 1 | |
| self.miniViews.append(miniView) | |
| self.miniViewHBox.pack_start(miniView.da, False, True, 0) | |
| self.controller.otherViews = self.miniViews | |
| self.vbox.add(self.miniViewHBox) | |
| self.window.add(self.vbox) | |
| def show_event(picChar, event): | |
| print '**** Comments for', event.unit, \ | |
| 'at time', self.view.time | |
| for name, value in event.pairs.iteritems(): | |
| print name, '=', value | |
| for comment in event.comments: | |
| print comment | |
| if picChar in event.visuals: | |
| # blocks = event.visuals[picChar].elems() | |
| print '**** Colour data' | |
| objs = event.find_ided_objects(self.model, picChar, True) | |
| for obj in objs: | |
| print ' '.join(obj.table_line()) | |
| def clicked_da(da, b): | |
| point = Point(b.x, b.y) | |
| overlay = None | |
| for blob, centre, size in self.view.positions: | |
| if point.is_within_box((centre, size)): | |
| event = self.model.find_unit_event_by_time(blob.unit, | |
| self.view.time) | |
| if event is not None: | |
| if overlay is None: | |
| overlay = Overlay(self.model, self.view, point, | |
| blob) | |
| show_event(blob.picChar, event) | |
| if overlay is not None: | |
| self.view.overlays = [overlay] | |
| else: | |
| self.view.overlays = [] | |
| self.view.redraw() | |
| # Set initial size and event callbacks | |
| self.view.set_da_size() | |
| self.view.da.add_events(gtk.gdk.BUTTON_PRESS_MASK) | |
| self.view.da.connect('button-press-event', clicked_da) | |
| self.window.connect('destroy', lambda(widget): gtk.main_quit()) | |
| def resize(window, event): | |
| """Resize DrawingArea to match new window size""" | |
| size = Point(float(event.width), float(event.height)) | |
| proportion = size / self.view.get_pic_size() | |
| # Preserve aspect ratio | |
| daScale = min(proportion.x, proportion.y) | |
| self.view.masterScale = Point(daScale, daScale) | |
| self.view.overlays = [] | |
| self.view.da.connect('configure-event', resize) | |
| self.window.show_all() |