1 # Copyright (c) 2013 ARM Limited
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.
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.
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.
36 # Authors: Andrew Bardsley
45 from point
import Point
49 from model
import Id
, BlobModel
, BlobDataSelect
, special_state_chars
52 class 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
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
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)
69 self
.fillColour
= colours
.emptySlotColour
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)
80 # Maximum view size for initial window mapping
81 self
.initialHeight
= 600.0
83 # Overlays are speech bubbles explaining blob data
86 self
.da
= gtk
.DrawingArea()
89 self
.da
.connect('expose_event', draw
)
91 # Handy offsets from the blob size
92 self
.blobIndent
= (self
.pitch
- self
.blobSize
).scale(0.5)
93 self
.blobIndentFactor
= self
.blobIndent
/ self
.pitch
95 def add_control_bar(self
, controlbar
):
96 """Add a BlobController to this view"""
97 self
.controlbar
= controlbar
99 def draw_to_png(self
, filename
):
100 """Draw the view to a PNG file"""
101 surface
= cairo
.ImageSurface(
103 self
.da
.get_allocation().width
,
104 self
.da
.get_allocation().height
)
105 cr
= gtk
.gdk
.CairoContext(cairo
.Context(surface
))
107 surface
.write_to_png(filename
)
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
)
115 cr
.scale(*self
.masterScale
.to_pair())
116 cr
.translate(*self
.origin
.to_pair())
121 for blob
in self
.model
.blobs
:
122 blob_event
= self
.model
.find_unit_event_by_time(
123 blob
.unit
, self
.time
)
126 pos
= blob
.render(cr
, self
, blob_event
, self
.dataSelect
,
131 positions
.append((blob
, centre
, size
))
133 # Draw all the overlays over the top
134 for overlay
in self
.overlays
:
142 """Redraw the whole view"""
143 buffer = cairo
.ImageSurface(
145 self
.da
.get_allocation().width
,
146 self
.da
.get_allocation().height
)
148 cr
= gtk
.gdk
.CairoContext(cairo
.Context(buffer))
149 positions
= self
.draw_to_cr(cr
)
151 # Assume that blobs are in order for depth so we want to
152 # hit the frontmost blob first if we search by position
154 self
.positions
= positions
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)
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]
171 self
.time
= self
.model
.times
[self
.timeIndex
]
175 def get_pic_size(self
):
176 """Return the size of ASCII-art picture of the pipeline scaled by
178 return (self
.origin
+ self
.pitch
*
179 (self
.model
.picSize
+ Point(1.0,1.0)))
181 def set_da_size(self
):
182 """Set the DrawingArea size after scaling"""
183 self
.da
.set_size_request(10 , int(self
.initialHeight
))
185 class BlobController(object):
186 """The controller bar for the viewer"""
187 def __init__(self
, model
, view
,
188 defaultEventFile
="", defaultPictureFile
=""):
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
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)
212 self
.timeEntry
= gtk
.Entry()
214 t
= gtk
.ToggleButton('T')
216 s
= gtk
.ToggleButton('S')
218 p
= gtk
.ToggleButton('P')
220 l
= gtk
.ToggleButton('L')
222 f
= gtk
.ToggleButton('F')
224 e
= gtk
.ToggleButton('E')
227 # Should really generate this from above
228 self
.view
.dataSelect
.ids
= set("SPLFE")
230 self
.bar
= gtk
.VBox()
231 self
.bar
.set_homogeneous(False)
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
)
257 self
.bar
.pack_start(row1
, False, True, 0)
258 self
.set_time_index(0)
260 def toggle_id(self
, id):
261 """One of the sequence number selector buttons has been toggled"""
263 if button
.get_active():
264 self
.view
.dataSelect
.ids
.add(id)
266 self
.view
.dataSelect
.ids
.discard(id)
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)
275 def set_time_index(self
, time
):
276 """Set the time index in the view"""
277 self
.view
.set_time_index(time
)
279 for view
in self
.otherViews
:
280 view
.set_time_index(time
)
283 self
.timeEntry
.set_text(str(self
.view
.time
))
285 def time_start(self
, button
):
287 self
.set_time_index(0)
290 def time_end(self
, button
):
292 self
.set_time_index(len(self
.model
.times
) - 1)
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))
302 def time_back(self
, button
):
303 """Step back pressed"""
304 self
.set_time_index(max(self
.view
.timeIndex
- 1, 0))
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
)
315 """Time step while playing"""
316 if not self
.playTimer \
317 or self
.view
.timeIndex
== len(self
.model
.times
) - 1:
321 self
.time_forward(None)
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
)
329 def time_stop(self
, play
):
330 """Stop play pressed"""
332 gobject
.source_remove(self
.playTimer
)
333 self
.playTimer
= None
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
))
343 class Overlay(object):
344 """An Overlay is a speech bubble explaining the data in a blob"""
345 def __init__(self
, model
, view
, point
, blob
):
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
,
357 """Draw the overlay"""
358 event
= self
.find_event()
363 insts
= event
.find_ided_objects(self
.model
, self
.blob
.picChar
,
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())
370 # Get formatted data from the insts to format into a table
371 lines
= list(inst
.table_line() for inst
in insts
)
374 cr
.set_font_size(text_size
)
377 xb
, yb
, width
, height
, dx
, dy
= cr
.text_extents(str)
380 # Find the maximum number of columns and the widths of each column
383 num_columns
= max(num_columns
, len(line
))
385 widths
= [0] * num_columns
387 for i
in xrange(0, len(line
)):
388 widths
[i
] = max(widths
[i
], text_width(line
[i
]))
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
)
396 text_point
= self
.point
397 text_step
= Point(0.0, text_size
)
399 size
= Point(total_width
, text_size
* len(insts
))
401 # Draw the speech bubble
402 blobs
.speech_bubble(cr
, self
.point
, size
, text_size
)
403 cr
.set_source_color(colours
.backgroundColour
)
405 cr
.set_source_color(colours
.black
)
408 text_point
+= Point(1.0,1.0).scale(2.0 * text_size
)
410 id_size
= Point(id_width
, text_size
)
412 # Draw the rows in the table
413 for i
in xrange(0, len(insts
)):
414 row_point
= text_point
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
)
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
])
430 text_point
+= text_step
432 class BlobWindow(object):
433 """The top-level window and its mouse control"""
434 def __init__(self
, model
, view
, controller
):
437 self
.controller
= controller
438 self
.controlbar
= None
440 self
.miniViewCount
= 0
442 def add_control_bar(self
, controlbar
):
443 self
.controlbar
= controlbar
445 def show_window(self
):
446 self
.window
= gtk
.Window()
448 self
.vbox
= gtk
.VBox()
449 self
.vbox
.set_homogeneous(False)
451 self
.vbox
.pack_start(self
.controlbar
, False, True, 0)
452 self
.vbox
.add(self
.view
.da
)
454 if self
.miniViewCount
> 0:
456 self
.miniViewHBox
= gtk
.HBox(homogeneous
=True, spacing
=2)
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)
468 self
.controller
.otherViews
= self
.miniViews
469 self
.vbox
.add(self
.miniViewHBox
)
471 self
.window
.add(self
.vbox
)
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
:
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)
485 print ' '.join(obj
.table_line())
487 def clicked_da(da
, b
):
488 point
= Point(b
.x
, b
.y
)
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
,
495 if event
is not None:
497 overlay
= Overlay(self
.model
, self
.view
, point
,
499 show_event(blob
.picChar
, event
)
500 if overlay
is not None:
501 self
.view
.overlays
= [overlay
]
503 self
.view
.overlays
= []
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())
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
= []
522 self
.view
.da
.connect('configure-event', resize
)
524 self
.window
.show_all()