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.
43 from point
import Point
47 from model
import Id
, BlobModel
, BlobDataSelect
, special_state_chars
50 class BlobView(object):
51 """The canvas view of the pipeline"""
52 def __init__(self
, model
):
53 # A unit blob will appear at size blobSize inside a space of
55 self
.blobSize
= Point(45.0, 45.0)
56 self
.pitch
= Point(60.0, 60.0)
57 self
.origin
= Point(50.0, 50.0)
58 # Some common line definitions to cut down on arbitrary
60 self
.thickLineWidth
= 10.0
61 self
.thinLineWidth
= 4.0
62 self
.midLineWidth
= 6.0
63 # The scale from the units of pitch to device units (nominally
64 # pixels for 1.0 to 1.0
65 self
.masterScale
= Point(1.0,1.0)
67 self
.fillColour
= colours
.emptySlotColour
71 self
.controlbar
= None
72 # The sequence number selector state
73 self
.dataSelect
= BlobDataSelect()
74 # Offset of this view's time from self.time used for miniviews
75 # This is actually an offset of the index into the array of times
76 # seen in the event file)
78 # Maximum view size for initial window mapping
79 self
.initialHeight
= 600.0
81 # Overlays are speech bubbles explaining blob data
84 self
.da
= gtk
.DrawingArea()
87 self
.da
.connect('expose_event', draw
)
89 # Handy offsets from the blob size
90 self
.blobIndent
= (self
.pitch
- self
.blobSize
).scale(0.5)
91 self
.blobIndentFactor
= self
.blobIndent
/ self
.pitch
93 def add_control_bar(self
, controlbar
):
94 """Add a BlobController to this view"""
95 self
.controlbar
= controlbar
97 def draw_to_png(self
, filename
):
98 """Draw the view to a PNG file"""
99 surface
= cairo
.ImageSurface(
101 self
.da
.get_allocation().width
,
102 self
.da
.get_allocation().height
)
103 cr
= gtk
.gdk
.CairoContext(cairo
.Context(surface
))
105 surface
.write_to_png(filename
)
107 def draw_to_cr(self
, cr
):
108 """Draw to a given CairoContext"""
109 cr
.set_source_color(colours
.backgroundColour
)
110 cr
.set_line_width(self
.thickLineWidth
)
113 cr
.scale(*self
.masterScale
.to_pair())
114 cr
.translate(*self
.origin
.to_pair())
119 for blob
in self
.model
.blobs
:
120 blob_event
= self
.model
.find_unit_event_by_time(
121 blob
.unit
, self
.time
)
124 pos
= blob
.render(cr
, self
, blob_event
, self
.dataSelect
,
129 positions
.append((blob
, centre
, size
))
131 # Draw all the overlays over the top
132 for overlay
in self
.overlays
:
140 """Redraw the whole view"""
141 buffer = cairo
.ImageSurface(
143 self
.da
.get_allocation().width
,
144 self
.da
.get_allocation().height
)
146 cr
= gtk
.gdk
.CairoContext(cairo
.Context(buffer))
147 positions
= self
.draw_to_cr(cr
)
149 # Assume that blobs are in order for depth so we want to
150 # hit the frontmost blob first if we search by position
152 self
.positions
= positions
154 # Paint the drawn buffer onto the DrawingArea
155 dacr
= self
.da
.window
.cairo_create()
156 dacr
.set_source_surface(buffer, 0.0, 0.0)
161 def set_time_index(self
, time
):
162 """Set the time index for the view. A time index is an index into
163 the model's times array of seen event times"""
164 self
.timeIndex
= time
+ self
.timeOffset
165 if len(self
.model
.times
) != 0:
166 if self
.timeIndex
>= len(self
.model
.times
):
167 self
.time
= self
.model
.times
[len(self
.model
.times
) - 1]
169 self
.time
= self
.model
.times
[self
.timeIndex
]
173 def get_pic_size(self
):
174 """Return the size of ASCII-art picture of the pipeline scaled by
176 return (self
.origin
+ self
.pitch
*
177 (self
.model
.picSize
+ Point(1.0,1.0)))
179 def set_da_size(self
):
180 """Set the DrawingArea size after scaling"""
181 self
.da
.set_size_request(10 , int(self
.initialHeight
))
183 class BlobController(object):
184 """The controller bar for the viewer"""
185 def __init__(self
, model
, view
,
186 defaultEventFile
="", defaultPictureFile
=""):
189 self
.playTimer
= None
190 self
.filenameEntry
= gtk
.Entry()
191 self
.filenameEntry
.set_text(defaultEventFile
)
192 self
.pictureEntry
= gtk
.Entry()
193 self
.pictureEntry
.set_text(defaultPictureFile
)
194 self
.timeEntry
= None
195 self
.defaultEventFile
= defaultEventFile
196 self
.startTime
= None
202 box
= gtk
.HBox(homogeneous
=False, spacing
=2)
203 box
.set_border_width(2)
204 for widget
, signal
, handler
in elems
:
205 if signal
is not None:
206 widget
.connect(signal
, handler
)
207 box
.pack_start(widget
, False, True, 0)
210 self
.timeEntry
= gtk
.Entry()
212 t
= gtk
.ToggleButton('T')
214 s
= gtk
.ToggleButton('S')
216 p
= gtk
.ToggleButton('P')
218 l
= gtk
.ToggleButton('L')
220 f
= gtk
.ToggleButton('F')
222 e
= gtk
.ToggleButton('E')
225 # Should really generate this from above
226 self
.view
.dataSelect
.ids
= set("SPLFE")
228 self
.bar
= gtk
.VBox()
229 self
.bar
.set_homogeneous(False)
232 (gtk
.Button('Start'), 'clicked', self
.time_start
),
233 (gtk
.Button('End'), 'clicked', self
.time_end
),
234 (gtk
.Button('Back'), 'clicked', self
.time_back
),
235 (gtk
.Button('Forward'), 'clicked', self
.time_forward
),
236 (gtk
.Button('Play'), 'clicked', self
.time_play
),
237 (gtk
.Button('Stop'), 'clicked', self
.time_stop
),
238 (self
.timeEntry
, 'activate', self
.time_set
),
239 (gtk
.Label('Visible ids:'), None, None),
240 (t
, 'clicked', self
.toggle_id('T')),
241 (gtk
.Label('/'), None, None),
242 (s
, 'clicked', self
.toggle_id('S')),
243 (gtk
.Label('.'), None, None),
244 (p
, 'clicked', self
.toggle_id('P')),
245 (gtk
.Label('/'), None, None),
246 (l
, 'clicked', self
.toggle_id('L')),
247 (gtk
.Label('/'), None, None),
248 (f
, 'clicked', self
.toggle_id('F')),
249 (gtk
.Label('.'), None, None),
250 (e
, 'clicked', self
.toggle_id('E')),
251 (self
.filenameEntry
, 'activate', self
.load_events
),
252 (gtk
.Button('Reload'), 'clicked', self
.load_events
)
255 self
.bar
.pack_start(row1
, False, True, 0)
256 self
.set_time_index(0)
258 def toggle_id(self
, id):
259 """One of the sequence number selector buttons has been toggled"""
261 if button
.get_active():
262 self
.view
.dataSelect
.ids
.add(id)
264 self
.view
.dataSelect
.ids
.discard(id)
266 # Always leave one thing visible
267 if len(self
.view
.dataSelect
.ids
) == 0:
268 self
.view
.dataSelect
.ids
.add(id)
269 button
.set_active(True)
273 def set_time_index(self
, time
):
274 """Set the time index in the view"""
275 self
.view
.set_time_index(time
)
277 for view
in self
.otherViews
:
278 view
.set_time_index(time
)
281 self
.timeEntry
.set_text(str(self
.view
.time
))
283 def time_start(self
, button
):
285 self
.set_time_index(0)
288 def time_end(self
, button
):
290 self
.set_time_index(len(self
.model
.times
) - 1)
293 def time_forward(self
, button
):
294 """Step forward pressed"""
295 self
.set_time_index(min(self
.view
.timeIndex
+ 1,
296 len(self
.model
.times
) - 1))
300 def time_back(self
, button
):
301 """Step back pressed"""
302 self
.set_time_index(max(self
.view
.timeIndex
- 1, 0))
305 def time_set(self
, entry
):
306 """Time dialogue changed. Need to find a suitable time
307 <= the entry's time"""
308 newTime
= self
.model
.find_time_index(int(entry
.get_text()))
309 self
.set_time_index(newTime
)
313 """Time step while playing"""
314 if not self
.playTimer \
315 or self
.view
.timeIndex
== len(self
.model
.times
) - 1:
319 self
.time_forward(None)
322 def time_play(self
, play
):
323 """Automatically advance time every 100 ms"""
324 if not self
.playTimer
:
325 self
.playTimer
= gobject
.timeout_add(100, self
.time_step
)
327 def time_stop(self
, play
):
328 """Stop play pressed"""
330 gobject
.source_remove(self
.playTimer
)
331 self
.playTimer
= None
333 def load_events(self
, button
):
334 """Reload events file"""
335 self
.model
.load_events(self
.filenameEntry
.get_text(),
336 startTime
=self
.startTime
, endTime
=self
.endTime
)
337 self
.set_time_index(min(len(self
.model
.times
) - 1,
338 self
.view
.timeIndex
))
341 class Overlay(object):
342 """An Overlay is a speech bubble explaining the data in a blob"""
343 def __init__(self
, model
, view
, point
, blob
):
349 def find_event(self
):
350 """Find the event for a changing time and a fixed blob"""
351 return self
.model
.find_unit_event_by_time(self
.blob
.unit
,
355 """Draw the overlay"""
356 event
= self
.find_event()
361 insts
= event
.find_ided_objects(self
.model
, self
.blob
.picChar
,
364 cr
.set_line_width(self
.view
.thinLineWidth
)
365 cr
.translate(*(Point(0.0,0.0) - self
.view
.origin
).to_pair())
366 cr
.scale(*(Point(1.0,1.0) / self
.view
.masterScale
).to_pair())
368 # Get formatted data from the insts to format into a table
369 lines
= list(inst
.table_line() for inst
in insts
)
372 cr
.set_font_size(text_size
)
375 xb
, yb
, width
, height
, dx
, dy
= cr
.text_extents(str)
378 # Find the maximum number of columns and the widths of each column
381 num_columns
= max(num_columns
, len(line
))
383 widths
= [0] * num_columns
385 for i
in xrange(0, len(line
)):
386 widths
[i
] = max(widths
[i
], text_width(line
[i
]))
388 # Calculate the size of the speech bubble
389 column_gap
= 1 * text_size
390 id_width
= 6 * text_size
391 total_width
= sum(widths
) + id_width
+ column_gap
* (num_columns
+ 1)
392 gap_step
= Point(1.0, 0.0).scale(column_gap
)
394 text_point
= self
.point
395 text_step
= Point(0.0, text_size
)
397 size
= Point(total_width
, text_size
* len(insts
))
399 # Draw the speech bubble
400 blobs
.speech_bubble(cr
, self
.point
, size
, text_size
)
401 cr
.set_source_color(colours
.backgroundColour
)
403 cr
.set_source_color(colours
.black
)
406 text_point
+= Point(1.0,1.0).scale(2.0 * text_size
)
408 id_size
= Point(id_width
, text_size
)
410 # Draw the rows in the table
411 for i
in xrange(0, len(insts
)):
412 row_point
= text_point
415 blobs
.striped_box(cr
, row_point
+ id_size
.scale(0.5),
416 id_size
, inst
.id.to_striped_block(self
.view
.dataSelect
))
417 cr
.set_source_color(colours
.black
)
419 row_point
+= Point(1.0, 0.0).scale(id_width
)
420 row_point
+= text_step
421 # Draw the columns of each row
422 for j
in xrange(0, len(line
)):
423 row_point
+= gap_step
424 cr
.move_to(*row_point
.to_pair())
425 cr
.show_text(line
[j
])
426 row_point
+= Point(1.0, 0.0).scale(widths
[j
])
428 text_point
+= text_step
430 class BlobWindow(object):
431 """The top-level window and its mouse control"""
432 def __init__(self
, model
, view
, controller
):
435 self
.controller
= controller
436 self
.controlbar
= None
438 self
.miniViewCount
= 0
440 def add_control_bar(self
, controlbar
):
441 self
.controlbar
= controlbar
443 def show_window(self
):
444 self
.window
= gtk
.Window()
446 self
.vbox
= gtk
.VBox()
447 self
.vbox
.set_homogeneous(False)
449 self
.vbox
.pack_start(self
.controlbar
, False, True, 0)
450 self
.vbox
.add(self
.view
.da
)
452 if self
.miniViewCount
> 0:
454 self
.miniViewHBox
= gtk
.HBox(homogeneous
=True, spacing
=2)
457 for i
in xrange(1, self
.miniViewCount
+ 1):
458 miniView
= BlobView(self
.model
)
459 miniView
.set_time_index(0)
460 miniView
.masterScale
= Point(0.1, 0.1)
461 miniView
.set_da_size()
462 miniView
.timeOffset
= i
+ 1
463 self
.miniViews
.append(miniView
)
464 self
.miniViewHBox
.pack_start(miniView
.da
, False, True, 0)
466 self
.controller
.otherViews
= self
.miniViews
467 self
.vbox
.add(self
.miniViewHBox
)
469 self
.window
.add(self
.vbox
)
471 def show_event(picChar
, event
):
472 print '**** Comments for', event
.unit
, \
473 'at time', self
.view
.time
474 for name
, value
in event
.pairs
.iteritems():
475 print name
, '=', value
476 for comment
in event
.comments
:
478 if picChar
in event
.visuals
:
479 # blocks = event.visuals[picChar].elems()
480 print '**** Colour data'
481 objs
= event
.find_ided_objects(self
.model
, picChar
, True)
483 print ' '.join(obj
.table_line())
485 def clicked_da(da
, b
):
486 point
= Point(b
.x
, b
.y
)
489 for blob
, centre
, size
in self
.view
.positions
:
490 if point
.is_within_box((centre
, size
)):
491 event
= self
.model
.find_unit_event_by_time(blob
.unit
,
493 if event
is not None:
495 overlay
= Overlay(self
.model
, self
.view
, point
,
497 show_event(blob
.picChar
, event
)
498 if overlay
is not None:
499 self
.view
.overlays
= [overlay
]
501 self
.view
.overlays
= []
505 # Set initial size and event callbacks
506 self
.view
.set_da_size()
507 self
.view
.da
.add_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
508 self
.view
.da
.connect('button-press-event', clicked_da
)
509 self
.window
.connect('destroy', lambda(widget
): gtk
.main_quit())
511 def resize(window
, event
):
512 """Resize DrawingArea to match new window size"""
513 size
= Point(float(event
.width
), float(event
.height
))
514 proportion
= size
/ self
.view
.get_pic_size()
515 # Preserve aspect ratio
516 daScale
= min(proportion
.x
, proportion
.y
)
517 self
.view
.masterScale
= Point(daScale
, daScale
)
518 self
.view
.overlays
= []
520 self
.view
.da
.connect('configure-event', resize
)
522 self
.window
.show_all()