| # 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 |
| # |
| # blobs.py: Blobs are the visual blocks, arrows and other coloured |
| # objects on the visualiser. This file contains Blob definition and |
| # their rendering instructions in pygtk/cairo. |
| # |
| |
| import pygtk |
| pygtk.require('2.0') |
| import gtk |
| import gobject |
| import cairo |
| import re |
| import math |
| |
| from point import Point |
| import parse |
| import colours |
| from colours import backgroundColour, black |
| import model |
| |
| def centre_size_to_sides(centre, size): |
| """Returns a 4-tuple of the relevant ordinates of the left, |
| right, top and bottom sides of the described rectangle""" |
| (x, y) = centre.to_pair() |
| (half_width, half_height) = (size.scale(0.5)).to_pair() |
| left = x - half_width |
| right = x + half_width |
| top = y - half_height |
| bottom = y + half_height |
| return (left, right, top, bottom) |
| |
| def box(cr, centre, size): |
| """Draw a simple box""" |
| (left, right, top, bottom) = centre_size_to_sides(centre, size) |
| cr.move_to(left, top) |
| cr.line_to(right, top) |
| cr.line_to(right, bottom) |
| cr.line_to(left, bottom) |
| cr.close_path() |
| |
| def stroke_and_fill(cr, colour): |
| """Stroke with the current colour then fill the same path with the |
| given colour""" |
| join = cr.get_line_join() |
| cr.set_line_join(gtk.gdk.JOIN_ROUND) |
| cr.close_path() |
| cr.set_source_color(backgroundColour) |
| cr.stroke_preserve() |
| cr.set_source_color(colour) |
| cr.fill() |
| cr.set_line_join(join) |
| |
| def striped_box(cr, centre, size, colours): |
| """Fill a rectangle (without outline) striped with the colours given""" |
| num_colours = len(colours) |
| if num_colours == 0: |
| box(cr, centre, size) |
| cr.set_source_color(backgroundColour) |
| cr.fill() |
| elif num_colours == 1: |
| box(cr, centre, size) |
| stroke_and_fill(cr, colours[0]) |
| else: |
| (left, right, top, bottom) = centre_size_to_sides(centre, size) |
| (width, height) = size.to_pair() |
| x_stripe_width = width / num_colours |
| half_x_stripe_width = x_stripe_width / 2.0 |
| # Left triangle |
| cr.move_to(left, bottom) |
| cr.line_to(left + half_x_stripe_width, bottom) |
| cr.line_to(left + x_stripe_width + half_x_stripe_width, top) |
| cr.line_to(left, top) |
| stroke_and_fill(cr, colours[0]) |
| # Stripes |
| for i in xrange(1, num_colours - 1): |
| xOffset = x_stripe_width * i |
| cr.move_to(left + xOffset - half_x_stripe_width, bottom) |
| cr.line_to(left + xOffset + half_x_stripe_width, bottom) |
| cr.line_to(left + xOffset + x_stripe_width + |
| half_x_stripe_width, top) |
| cr.line_to(left + xOffset + x_stripe_width - |
| half_x_stripe_width, top) |
| stroke_and_fill(cr, colours[i]) |
| # Right triangle |
| cr.move_to((right - x_stripe_width) - half_x_stripe_width, bottom) |
| cr.line_to(right, bottom) |
| cr.line_to(right, top) |
| cr.line_to((right - x_stripe_width) + half_x_stripe_width, top) |
| stroke_and_fill(cr, colours[num_colours - 1]) |
| |
| def speech_bubble(cr, top_left, size, unit): |
| """Draw a speech bubble with 'size'-sized internal space with its |
| top left corner at Point(2.0 * unit, 2.0 * unit)""" |
| def local_arc(centre, angleFrom, angleTo): |
| cr.arc(centre.x, centre.y, unit, angleFrom * math.pi, |
| angleTo * math.pi) |
| |
| cr.move_to(*top_left.to_pair()) |
| cr.rel_line_to(unit * 2.0, unit) |
| cr.rel_line_to(size.x, 0.0) |
| local_arc(top_left + Point(size.x + unit * 2.0, unit * 2.0), -0.5, 0.0) |
| cr.rel_line_to(0.0, size.y) |
| local_arc(top_left + Point(size.x + unit * 2.0, size.y + unit * 2.0), |
| 0, 0.5) |
| cr.rel_line_to(-size.x, 0.0) |
| local_arc(top_left + Point(unit * 2.0, size.y + unit * 2.0), 0.5, 1.0) |
| cr.rel_line_to(0, -size.y) |
| cr.close_path() |
| |
| def open_bottom(cr, centre, size): |
| """Draw a box with left, top and right sides""" |
| (left, right, top, bottom) = centre_size_to_sides(centre, size) |
| cr.move_to(left, bottom) |
| cr.line_to(left, top) |
| cr.line_to(right, top) |
| cr.line_to(right, bottom) |
| |
| def fifo(cr, centre, size): |
| """Draw just the vertical sides of a box""" |
| (left, right, top, bottom) = centre_size_to_sides(centre, size) |
| cr.move_to(left, bottom) |
| cr.line_to(left, top) |
| cr.move_to(right, bottom) |
| cr.line_to(right, top) |
| |
| def cross(cr, centre, size): |
| """Draw a cross parallel with the axes""" |
| (left, right, top, bottom) = centre_size_to_sides(centre, size) |
| (x, y) = centre.to_pair() |
| cr.move_to(left, y) |
| cr.line_to(right, y) |
| cr.move_to(x, top) |
| cr.line_to(x, bottom) |
| |
| class Blob(object): |
| """Blob super class""" |
| def __init__(self, picChar, unit, topLeft, colour, size = Point(1,1)): |
| self.picChar = picChar |
| self.unit = unit |
| self.displayName = unit |
| self.nameLoc = 'top' |
| self.topLeft = topLeft |
| self.colour = colour |
| self.size = size |
| self.border = 1.0 |
| self.dataSelect = model.BlobDataSelect() |
| self.shorten = 0 |
| |
| def render(self, cr, view, event, select, time): |
| """Render this blob with the given event's data. Returns either |
| None or a pair of (centre, size) in device coordinates for the drawn |
| blob. The return value can be used to detect if mouse clicks on |
| the canvas are within the blob""" |
| return None |
| |
| class Block(Blob): |
| """Blocks are rectangular blogs colourable with a 2D grid of striped |
| blocks. visualDecoder specifies how event data becomes this coloured |
| grid""" |
| def __init__(self, picChar, unit, topLeft=Point(0,0), |
| colour=colours.black, |
| size=Point(1,1)): |
| super(Block,self).__init__(picChar, unit, topLeft, colour, |
| size = size) |
| # {horiz, vert} |
| self.stripDir = 'horiz' |
| # {LR, RL}: LR means the first strip will be on the left/top, |
| # RL means the first strip will be on the right/bottom |
| self.stripOrd = 'LR' |
| # Number of blank strips if this is a frame |
| self.blankStrips = 0 |
| # {box, fifo, openBottom} |
| self.shape = 'box' |
| self.visualDecoder = None |
| |
| def render(self, cr, view, event, select, time): |
| # Find the right event, visuals and sizes for things |
| if event is None or self.displayName.startswith('_'): |
| event = model.BlobEvent(self.unit, time) |
| |
| if self.picChar in event.visuals: |
| strips = event.visuals[self.picChar].to_striped_block( |
| select & self.dataSelect) |
| else: |
| strips = [[[colours.unknownColour]]] |
| |
| if self.stripOrd == 'RL': |
| strips.reverse() |
| |
| if len(strips) == 0: |
| strips = [[colours.errorColour]] |
| print 'Problem with the colour of event:', event |
| |
| num_strips = len(strips) |
| strip_proportion = 1.0 / num_strips |
| first_strip_offset = (num_strips / 2.0) - 0.5 |
| |
| # Adjust blocks with 'shorten' attribute to the length of the data |
| size = Point(*self.size.to_pair()) |
| if self.shorten != 0 and self.size.x > (num_strips * self.shorten): |
| size.x = num_strips * self.shorten |
| |
| box_size = size - view.blobIndentFactor.scale(2) |
| |
| # Now do cr sensitive things |
| cr.save() |
| cr.scale(*view.pitch.to_pair()) |
| cr.translate(*self.topLeft.to_pair()) |
| cr.translate(*(size - Point(1,1)).scale(0.5).to_pair()) |
| |
| translated_centre = Point(*cr.user_to_device(0.0, 0.0)) |
| translated_size = \ |
| Point(*cr.user_to_device_distance(*size.to_pair())) |
| |
| # The 2D grid is a grid of strips of blocks. Data [[1,2],[3]] |
| # is 2 strips of 2 and 1 blocks respectively. |
| # if stripDir == 'horiz', strips are stacked vertically |
| # from top to bottom if stripOrd == 'LR' or bottom to top if |
| # stripOrd == 'RL'. |
| # if stripDir == 'vert', strips are stacked horizontally |
| # from left to right if stripOf == 'LR' or right to left if |
| # stripOrd == 'RL'. |
| |
| strip_is_horiz = self.stripDir == 'horiz' |
| |
| if strip_is_horiz: |
| strip_step_base = Point(1.0,0.0) |
| block_step_base = Point(0.0,1.0) |
| else: |
| strip_step_base = Point(0.0,1.0) |
| block_step_base = Point(1.0,0.0) |
| |
| strip_size = (box_size * (strip_step_base.scale(strip_proportion) + |
| block_step_base)) |
| strip_step = strip_size * strip_step_base |
| strip_centre = Point(0,0) - (strip_size * |
| strip_step_base.scale(first_strip_offset)) |
| |
| cr.set_line_width(view.midLineWidth / view.pitch.x) |
| |
| # Draw the strips and their blocks |
| for strip_index in xrange(0, num_strips): |
| num_blocks = len(strips[strip_index]) |
| block_proportion = 1.0 / num_blocks |
| firstBlockOffset = (num_blocks / 2.0) - 0.5 |
| |
| block_size = (strip_size * |
| (block_step_base.scale(block_proportion) + |
| strip_step_base)) |
| block_step = block_size * block_step_base |
| block_centre = (strip_centre + strip_step.scale(strip_index) - |
| (block_size * block_step_base.scale(firstBlockOffset))) |
| |
| for block_index in xrange(0, num_blocks): |
| striped_box(cr, block_centre + |
| block_step.scale(block_index), block_size, |
| strips[strip_index][block_index]) |
| |
| cr.set_font_size(0.7) |
| if self.border > 0.5: |
| weight = cairo.FONT_WEIGHT_BOLD |
| else: |
| weight = cairo.FONT_WEIGHT_NORMAL |
| cr.select_font_face('Helvetica', cairo.FONT_SLANT_NORMAL, |
| weight) |
| |
| xb, yb, width, height, dx, dy = cr.text_extents(self.displayName) |
| |
| text_comfort_space = 0.15 |
| |
| if self.nameLoc == 'left': |
| # Position text vertically along left side, top aligned |
| cr.save() |
| cr.rotate(- (math.pi / 2.0)) |
| text_point = Point(size.y, size.x).scale(0.5) * Point(-1, -1) |
| text_point += Point(max(0, size.y - width), 0) |
| text_point += Point(-text_comfort_space, -text_comfort_space) |
| else: # Including top |
| # Position text above the top left hand corner |
| text_point = size.scale(0.5) * Point(-1,-1) |
| text_point += Point(0.00, -text_comfort_space) |
| |
| if (self.displayName != '' and |
| not self.displayName.startswith('_')): |
| cr.set_source_color(self.colour) |
| cr.move_to(*text_point.to_pair()) |
| cr.show_text(self.displayName) |
| |
| if self.nameLoc == 'left': |
| cr.restore() |
| |
| # Draw the outline shape |
| cr.save() |
| if strip_is_horiz: |
| cr.rotate(- (math.pi / 2.0)) |
| box_size = Point(box_size.y, box_size.x) |
| |
| if self.stripOrd == "RL": |
| cr.rotate(math.pi) |
| |
| if self.shape == 'box': |
| box(cr, Point(0,0), box_size) |
| elif self.shape == 'openBottom': |
| open_bottom(cr, Point(0,0), box_size) |
| elif self.shape == 'fifo': |
| fifo(cr, Point(0,0), box_size) |
| cr.restore() |
| |
| # Restore scale and stroke the outline |
| cr.restore() |
| cr.set_source_color(self.colour) |
| cr.set_line_width(view.thickLineWidth * self.border) |
| cr.stroke() |
| |
| # Return blob size/position |
| if self.unit == '_': |
| return None |
| else: |
| return (translated_centre, translated_size) |
| |
| class Key(Blob): |
| """Draw a key to the special (and numeric colours) with swatches of the |
| colours half as wide as the key""" |
| def __init__(self, picChar, unit, topLeft, colour=colours.black, |
| size=Point(1,1)): |
| super(Key,self).__init__(picChar, unit, topLeft, colour, size = size) |
| self.colours = 'BBBB' |
| self.displayName = unit |
| |
| def render(self, cr, view, event, select, time): |
| cr.save() |
| cr.scale(*view.pitch.to_pair()) |
| cr.translate(*self.topLeft.to_pair()) |
| # cr.translate(*(self.size - Point(1,1)).scale(0.5).to_pair()) |
| half_width = self.size.x / 2.0 |
| cr.translate(*(self.size - Point(1.0 + half_width,1.0)).scale(0.5). |
| to_pair()) |
| |
| num_colours = len(self.colours) |
| cr.set_line_width(view.midLineWidth / view.pitch.x) |
| |
| blob_size = (Point(half_width,0.0) + |
| (self.size * Point(0.0,1.0 / num_colours))) |
| blob_step = Point(0.0,1.0) * blob_size |
| first_blob_centre = (Point(0.0,0.0) - |
| blob_step.scale((num_colours / 2.0) - 0.5)) |
| |
| cr.set_source_color(self.colour) |
| cr.set_line_width(view.thinLineWidth / view.pitch.x) |
| |
| blob_proportion = 0.8 |
| |
| real_blob_size = blob_size.scale(blob_proportion) |
| |
| cr.set_font_size(0.8 * blob_size.y * blob_proportion) |
| cr.select_font_face('Helvetica', cairo.FONT_SLANT_NORMAL, |
| cairo.FONT_WEIGHT_BOLD) |
| |
| for i in xrange(0, num_colours): |
| centre = first_blob_centre + blob_step.scale(i) |
| box(cr, centre, real_blob_size) |
| |
| colour_char = self.colours[i] |
| if colour_char.isdigit(): |
| cr.set_source_color(colours.number_to_colour( |
| int(colour_char))) |
| label = '...' + colour_char |
| else: |
| cr.set_source_color(model.special_state_colours[colour_char]) |
| label = model.special_state_names[colour_char] |
| |
| cr.fill_preserve() |
| cr.set_source_color(self.colour) |
| cr.stroke() |
| |
| xb, yb, width, height, dx, dy = cr.text_extents(label) |
| |
| text_left = (centre + (Point(0.5,0.0) * blob_size) + |
| Point(0.0, height / 2.0)) |
| |
| cr.move_to(*text_left.to_pair()) |
| cr.show_text(label) |
| |
| class Arrow(Blob): |
| """Draw a left or right facing arrow""" |
| def __init__(self, unit, topLeft, colour=colours.black, |
| size=Point(1.0,1.0), direc='right'): |
| super(Arrow,self).__init__(unit, unit, topLeft, colour, size = size) |
| self.direc = direc |
| |
| def render(self, cr, view, event, select, time): |
| cr.save() |
| cr.scale(*view.pitch.to_pair()) |
| cr.translate(*self.topLeft.to_pair()) |
| cr.translate(*(self.size - Point(1,1)).scale(0.5).to_pair()) |
| cr.scale(*self.size.to_pair()) |
| (blob_indent_x, blob_indent_y) = \ |
| (view.blobIndentFactor / self.size).to_pair() |
| left = -0.5 - blob_indent_x |
| right = 0.5 + blob_indent_x |
| |
| thickness = 0.2 |
| flare = 0.2 |
| |
| if self.direc == 'left': |
| cr.rotate(math.pi) |
| |
| cr.move_to(left, -thickness) |
| cr.line_to(0.0, -thickness) |
| cr.line_to(0.0, -(thickness + flare)) |
| cr.line_to(right, 0) |
| # Break arrow to prevent the point ruining the appearance of boxes |
| cr.move_to(right, 0) |
| cr.line_to(0.0, (thickness + flare)) |
| cr.line_to(0.0, +thickness) |
| cr.line_to(left, +thickness) |
| |
| cr.restore() |
| |
| # Draw arrow a bit more lightly than the standard line width |
| cr.set_line_width(cr.get_line_width() * 0.75) |
| cr.set_source_color(self.colour) |
| cr.stroke() |
| |
| return None |