| # 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. |
| |
| import pygtk |
| pygtk.require('2.0') |
| import gtk |
| import gobject |
| import cairo |
| import re |
| |
| from .point import Point |
| from . import parse |
| from . import colours |
| from . import model |
| from .model import Id, BlobModel, BlobDataSelect, special_state_chars |
| from . 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 range(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 range(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 range(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 range(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.items(): |
| 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() |