diff options
Diffstat (limited to 'util/minorview/view.py')
-rw-r--r-- | util/minorview/view.py | 524 |
1 files changed, 524 insertions, 0 deletions
diff --git a/util/minorview/view.py b/util/minorview/view.py new file mode 100644 index 000000000..8a9aaffea --- /dev/null +++ b/util/minorview/view.py @@ -0,0 +1,524 @@ +# 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() |