blob: 189443f5cc010ec1a95fa3e809cdf184d8797fc5 [file] [log] [blame]
# 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()