1# Copyright (c) 2013 ARM Limited 2# All rights reserved 3# 4# The license below extends only to copyright in the software and shall 5# not be construed as granting a license to any other intellectual 6# property including but not limited to intellectual property relating 7# to a hardware implementation of the functionality of the software 8# licensed hereunder. You may use the software subject to the license 9# terms below provided that you ensure that this notice is replicated 10# unmodified and in its entirety in all distributions of the software, 11# modified or unmodified, in source code or in binary form. 12# 13# Redistribution and use in source and binary forms, with or without 14# modification, are permitted provided that the following conditions are 15# met: redistributions of source code must retain the above copyright 16# notice, this list of conditions and the following disclaimer; 17# redistributions in binary form must reproduce the above copyright 18# notice, this list of conditions and the following disclaimer in the 19# documentation and/or other materials provided with the distribution; 20# neither the name of the copyright holders nor the names of its 21# contributors may be used to endorse or promote products derived from 22# this software without specific prior written permission. 23# 24# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 27# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 28# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 29# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 30# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35# 36# Authors: Andrew Bardsley 37 38import pygtk 39pygtk.require('2.0') 40import gtk 41import gobject 42import cairo 43import re 44 45from point import Point 46import parse 47import colours 48import model 49from model import Id, BlobModel, BlobDataSelect, special_state_chars 50import blobs 51 52class BlobView(object): 53 """The canvas view of the pipeline""" 54 def __init__(self, model): 55 # A unit blob will appear at size blobSize inside a space of 56 # size pitch. 57 self.blobSize = Point(45.0, 45.0) 58 self.pitch = Point(60.0, 60.0) 59 self.origin = Point(50.0, 50.0) 60 # Some common line definitions to cut down on arbitrary 61 # set_line_widths 62 self.thickLineWidth = 10.0 63 self.thinLineWidth = 4.0 64 self.midLineWidth = 6.0 65 # The scale from the units of pitch to device units (nominally 66 # pixels for 1.0 to 1.0 67 self.masterScale = Point(1.0,1.0) 68 self.model = model 69 self.fillColour = colours.emptySlotColour 70 self.timeIndex = 0 71 self.time = 0 72 self.positions = [] 73 self.controlbar = None 74 # The sequence number selector state 75 self.dataSelect = BlobDataSelect() 76 # Offset of this view's time from self.time used for miniviews 77 # This is actually an offset of the index into the array of times 78 # seen in the event file) 79 self.timeOffset = 0 80 # Maximum view size for initial window mapping 81 self.initialHeight = 600.0 82 83 # Overlays are speech bubbles explaining blob data 84 self.overlays = [] 85 86 self.da = gtk.DrawingArea() 87 def draw(arg1, arg2): 88 self.redraw() 89 self.da.connect('expose_event', draw) 90 91 # Handy offsets from the blob size 92 self.blobIndent = (self.pitch - self.blobSize).scale(0.5) 93 self.blobIndentFactor = self.blobIndent / self.pitch 94 95 def add_control_bar(self, controlbar): 96 """Add a BlobController to this view""" 97 self.controlbar = controlbar 98 99 def draw_to_png(self, filename): 100 """Draw the view to a PNG file""" 101 surface = cairo.ImageSurface( 102 cairo.FORMAT_ARGB32, 103 self.da.get_allocation().width, 104 self.da.get_allocation().height) 105 cr = gtk.gdk.CairoContext(cairo.Context(surface)) 106 self.draw_to_cr(cr) 107 surface.write_to_png(filename) 108 109 def draw_to_cr(self, cr): 110 """Draw to a given CairoContext""" 111 cr.set_source_color(colours.backgroundColour) 112 cr.set_line_width(self.thickLineWidth) 113 cr.paint() 114 cr.save() 115 cr.scale(*self.masterScale.to_pair()) 116 cr.translate(*self.origin.to_pair()) 117 118 positions = [] # {} 119 120 # Draw each blob 121 for blob in self.model.blobs: 122 blob_event = self.model.find_unit_event_by_time( 123 blob.unit, self.time) 124 125 cr.save() 126 pos = blob.render(cr, self, blob_event, self.dataSelect, 127 self.time) 128 cr.restore() 129 if pos is not None: 130 (centre, size) = pos 131 positions.append((blob, centre, size)) 132 133 # Draw all the overlays over the top 134 for overlay in self.overlays: 135 overlay.show(cr) 136 137 cr.restore() 138 139 return positions 140 141 def redraw(self): 142 """Redraw the whole view""" 143 buffer = cairo.ImageSurface( 144 cairo.FORMAT_ARGB32, 145 self.da.get_allocation().width, 146 self.da.get_allocation().height) 147 148 cr = gtk.gdk.CairoContext(cairo.Context(buffer)) 149 positions = self.draw_to_cr(cr) 150 151 # Assume that blobs are in order for depth so we want to 152 # hit the frontmost blob first if we search by position 153 positions.reverse() 154 self.positions = positions 155 156 # Paint the drawn buffer onto the DrawingArea 157 dacr = self.da.window.cairo_create() 158 dacr.set_source_surface(buffer, 0.0, 0.0) 159 dacr.paint() 160 161 buffer.finish() 162 163 def set_time_index(self, time): 164 """Set the time index for the view. A time index is an index into 165 the model's times array of seen event times""" 166 self.timeIndex = time + self.timeOffset 167 if len(self.model.times) != 0: 168 if self.timeIndex >= len(self.model.times): 169 self.time = self.model.times[len(self.model.times) - 1] 170 else: 171 self.time = self.model.times[self.timeIndex] 172 else: 173 self.time = 0 174 175 def get_pic_size(self): 176 """Return the size of ASCII-art picture of the pipeline scaled by 177 the blob pitch""" 178 return (self.origin + self.pitch * 179 (self.model.picSize + Point(1.0,1.0))) 180 181 def set_da_size(self): 182 """Set the DrawingArea size after scaling""" 183 self.da.set_size_request(10 , int(self.initialHeight)) 184 185class BlobController(object): 186 """The controller bar for the viewer""" 187 def __init__(self, model, view, 188 defaultEventFile="", defaultPictureFile=""): 189 self.model = model 190 self.view = view 191 self.playTimer = None 192 self.filenameEntry = gtk.Entry() 193 self.filenameEntry.set_text(defaultEventFile) 194 self.pictureEntry = gtk.Entry() 195 self.pictureEntry.set_text(defaultPictureFile) 196 self.timeEntry = None 197 self.defaultEventFile = defaultEventFile 198 self.startTime = None 199 self.endTime = None 200 201 self.otherViews = [] 202 203 def make_bar(elems): 204 box = gtk.HBox(homogeneous=False, spacing=2) 205 box.set_border_width(2) 206 for widget, signal, handler in elems: 207 if signal is not None: 208 widget.connect(signal, handler) 209 box.pack_start(widget, False, True, 0) 210 return box 211 212 self.timeEntry = gtk.Entry() 213 214 t = gtk.ToggleButton('T') 215 t.set_active(False) 216 s = gtk.ToggleButton('S') 217 s.set_active(True) 218 p = gtk.ToggleButton('P') 219 p.set_active(True) 220 l = gtk.ToggleButton('L') 221 l.set_active(True) 222 f = gtk.ToggleButton('F') 223 f.set_active(True) 224 e = gtk.ToggleButton('E') 225 e.set_active(True) 226 227 # Should really generate this from above 228 self.view.dataSelect.ids = set("SPLFE") 229 230 self.bar = gtk.VBox() 231 self.bar.set_homogeneous(False) 232 233 row1 = make_bar([ 234 (gtk.Button('Start'), 'clicked', self.time_start), 235 (gtk.Button('End'), 'clicked', self.time_end), 236 (gtk.Button('Back'), 'clicked', self.time_back), 237 (gtk.Button('Forward'), 'clicked', self.time_forward), 238 (gtk.Button('Play'), 'clicked', self.time_play), 239 (gtk.Button('Stop'), 'clicked', self.time_stop), 240 (self.timeEntry, 'activate', self.time_set), 241 (gtk.Label('Visible ids:'), None, None), 242 (t, 'clicked', self.toggle_id('T')), 243 (gtk.Label('/'), None, None), 244 (s, 'clicked', self.toggle_id('S')), 245 (gtk.Label('.'), None, None), 246 (p, 'clicked', self.toggle_id('P')), 247 (gtk.Label('/'), None, None), 248 (l, 'clicked', self.toggle_id('L')), 249 (gtk.Label('/'), None, None), 250 (f, 'clicked', self.toggle_id('F')), 251 (gtk.Label('.'), None, None), 252 (e, 'clicked', self.toggle_id('E')), 253 (self.filenameEntry, 'activate', self.load_events), 254 (gtk.Button('Reload'), 'clicked', self.load_events) 255 ]) 256 257 self.bar.pack_start(row1, False, True, 0) 258 self.set_time_index(0) 259 260 def toggle_id(self, id): 261 """One of the sequence number selector buttons has been toggled""" 262 def toggle(button): 263 if button.get_active(): 264 self.view.dataSelect.ids.add(id) 265 else: 266 self.view.dataSelect.ids.discard(id) 267 268 # Always leave one thing visible 269 if len(self.view.dataSelect.ids) == 0: 270 self.view.dataSelect.ids.add(id) 271 button.set_active(True) 272 self.view.redraw() 273 return toggle 274 275 def set_time_index(self, time): 276 """Set the time index in the view""" 277 self.view.set_time_index(time) 278 279 for view in self.otherViews: 280 view.set_time_index(time) 281 view.redraw() 282 283 self.timeEntry.set_text(str(self.view.time)) 284 285 def time_start(self, button): 286 """Start pressed""" 287 self.set_time_index(0) 288 self.view.redraw() 289 290 def time_end(self, button): 291 """End pressed""" 292 self.set_time_index(len(self.model.times) - 1) 293 self.view.redraw() 294 295 def time_forward(self, button): 296 """Step forward pressed""" 297 self.set_time_index(min(self.view.timeIndex + 1, 298 len(self.model.times) - 1)) 299 self.view.redraw() 300 gtk.gdk.flush() 301 302 def time_back(self, button): 303 """Step back pressed""" 304 self.set_time_index(max(self.view.timeIndex - 1, 0)) 305 self.view.redraw() 306 307 def time_set(self, entry): 308 """Time dialogue changed. Need to find a suitable time 309 <= the entry's time""" 310 newTime = self.model.find_time_index(int(entry.get_text())) 311 self.set_time_index(newTime) 312 self.view.redraw() 313 314 def time_step(self): 315 """Time step while playing""" 316 if not self.playTimer \ 317 or self.view.timeIndex == len(self.model.times) - 1: 318 self.time_stop(None) 319 return False 320 else: 321 self.time_forward(None) 322 return True 323 324 def time_play(self, play): 325 """Automatically advance time every 100 ms""" 326 if not self.playTimer: 327 self.playTimer = gobject.timeout_add(100, self.time_step) 328 329 def time_stop(self, play): 330 """Stop play pressed""" 331 if self.playTimer: 332 gobject.source_remove(self.playTimer) 333 self.playTimer = None 334 335 def load_events(self, button): 336 """Reload events file""" 337 self.model.load_events(self.filenameEntry.get_text(), 338 startTime=self.startTime, endTime=self.endTime) 339 self.set_time_index(min(len(self.model.times) - 1, 340 self.view.timeIndex)) 341 self.view.redraw() 342 343class Overlay(object): 344 """An Overlay is a speech bubble explaining the data in a blob""" 345 def __init__(self, model, view, point, blob): 346 self.model = model 347 self.view = view 348 self.point = point 349 self.blob = blob 350 351 def find_event(self): 352 """Find the event for a changing time and a fixed blob""" 353 return self.model.find_unit_event_by_time(self.blob.unit, 354 self.view.time) 355 356 def show(self, cr): 357 """Draw the overlay""" 358 event = self.find_event() 359 360 if event is None: 361 return 362 363 insts = event.find_ided_objects(self.model, self.blob.picChar, 364 False) 365 366 cr.set_line_width(self.view.thinLineWidth) 367 cr.translate(*(Point(0.0,0.0) - self.view.origin).to_pair()) 368 cr.scale(*(Point(1.0,1.0) / self.view.masterScale).to_pair()) 369 370 # Get formatted data from the insts to format into a table 371 lines = list(inst.table_line() for inst in insts) 372 373 text_size = 10.0 374 cr.set_font_size(text_size) 375 376 def text_width(str): 377 xb, yb, width, height, dx, dy = cr.text_extents(str) 378 return width 379 380 # Find the maximum number of columns and the widths of each column 381 num_columns = 0 382 for line in lines: 383 num_columns = max(num_columns, len(line)) 384 385 widths = [0] * num_columns 386 for line in lines: 387 for i in xrange(0, len(line)): 388 widths[i] = max(widths[i], text_width(line[i])) 389 390 # Calculate the size of the speech bubble 391 column_gap = 1 * text_size 392 id_width = 6 * text_size 393 total_width = sum(widths) + id_width + column_gap * (num_columns + 1) 394 gap_step = Point(1.0, 0.0).scale(column_gap) 395 396 text_point = self.point 397 text_step = Point(0.0, text_size) 398 399 size = Point(total_width, text_size * len(insts)) 400 401 # Draw the speech bubble 402 blobs.speech_bubble(cr, self.point, size, text_size) 403 cr.set_source_color(colours.backgroundColour) 404 cr.fill_preserve() 405 cr.set_source_color(colours.black) 406 cr.stroke() 407 408 text_point += Point(1.0,1.0).scale(2.0 * text_size) 409 410 id_size = Point(id_width, text_size) 411 412 # Draw the rows in the table 413 for i in xrange(0, len(insts)): 414 row_point = text_point 415 inst = insts[i] 416 line = lines[i] 417 blobs.striped_box(cr, row_point + id_size.scale(0.5), 418 id_size, inst.id.to_striped_block(self.view.dataSelect)) 419 cr.set_source_color(colours.black) 420 421 row_point += Point(1.0, 0.0).scale(id_width) 422 row_point += text_step 423 # Draw the columns of each row 424 for j in xrange(0, len(line)): 425 row_point += gap_step 426 cr.move_to(*row_point.to_pair()) 427 cr.show_text(line[j]) 428 row_point += Point(1.0, 0.0).scale(widths[j]) 429 430 text_point += text_step 431 432class BlobWindow(object): 433 """The top-level window and its mouse control""" 434 def __init__(self, model, view, controller): 435 self.model = model 436 self.view = view 437 self.controller = controller 438 self.controlbar = None 439 self.window = None 440 self.miniViewCount = 0 441 442 def add_control_bar(self, controlbar): 443 self.controlbar = controlbar 444 445 def show_window(self): 446 self.window = gtk.Window() 447 448 self.vbox = gtk.VBox() 449 self.vbox.set_homogeneous(False) 450 if self.controlbar: 451 self.vbox.pack_start(self.controlbar, False, True, 0) 452 self.vbox.add(self.view.da) 453 454 if self.miniViewCount > 0: 455 self.miniViews = [] 456 self.miniViewHBox = gtk.HBox(homogeneous=True, spacing=2) 457 458 # Draw mini views 459 for i in xrange(1, self.miniViewCount + 1): 460 miniView = BlobView(self.model) 461 miniView.set_time_index(0) 462 miniView.masterScale = Point(0.1, 0.1) 463 miniView.set_da_size() 464 miniView.timeOffset = i + 1 465 self.miniViews.append(miniView) 466 self.miniViewHBox.pack_start(miniView.da, False, True, 0) 467 468 self.controller.otherViews = self.miniViews 469 self.vbox.add(self.miniViewHBox) 470 471 self.window.add(self.vbox) 472 473 def show_event(picChar, event): 474 print '**** Comments for', event.unit, \ 475 'at time', self.view.time 476 for name, value in event.pairs.iteritems(): 477 print name, '=', value 478 for comment in event.comments: 479 print comment 480 if picChar in event.visuals: 481 # blocks = event.visuals[picChar].elems() 482 print '**** Colour data' 483 objs = event.find_ided_objects(self.model, picChar, True) 484 for obj in objs: 485 print ' '.join(obj.table_line()) 486 487 def clicked_da(da, b): 488 point = Point(b.x, b.y) 489 490 overlay = None 491 for blob, centre, size in self.view.positions: 492 if point.is_within_box((centre, size)): 493 event = self.model.find_unit_event_by_time(blob.unit, 494 self.view.time) 495 if event is not None: 496 if overlay is None: 497 overlay = Overlay(self.model, self.view, point, 498 blob) 499 show_event(blob.picChar, event) 500 if overlay is not None: 501 self.view.overlays = [overlay] 502 else: 503 self.view.overlays = [] 504 505 self.view.redraw() 506 507 # Set initial size and event callbacks 508 self.view.set_da_size() 509 self.view.da.add_events(gtk.gdk.BUTTON_PRESS_MASK) 510 self.view.da.connect('button-press-event', clicked_da) 511 self.window.connect('destroy', lambda(widget): gtk.main_quit()) 512 513 def resize(window, event): 514 """Resize DrawingArea to match new window size""" 515 size = Point(float(event.width), float(event.height)) 516 proportion = size / self.view.get_pic_size() 517 # Preserve aspect ratio 518 daScale = min(proportion.x, proportion.y) 519 self.view.masterScale = Point(daScale, daScale) 520 self.view.overlays = [] 521 522 self.view.da.connect('configure-event', resize) 523 524 self.window.show_all() 525