# 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()