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#
38# blobs.py: Blobs are the visual blocks, arrows and other coloured
39#   objects on the visualiser.  This file contains Blob definition and
40#   their rendering instructions in pygtk/cairo.
41#
42
43import pygtk
44pygtk.require('2.0')
45import gtk
46import gobject
47import cairo
48import re
49import math
50
51from point import Point
52import parse
53import colours
54from colours import backgroundColour, black
55import model
56
57def centre_size_to_sides(centre, size):
58    """Returns a 4-tuple of the relevant ordinates of the left,
59    right, top and bottom sides of the described rectangle"""
60    (x, y) = centre.to_pair()
61    (half_width, half_height) = (size.scale(0.5)).to_pair()
62    left = x - half_width
63    right = x + half_width
64    top = y - half_height
65    bottom = y + half_height
66    return (left, right, top, bottom)
67
68def box(cr, centre, size):
69    """Draw a simple box"""
70    (left, right, top, bottom) = centre_size_to_sides(centre, size)
71    cr.move_to(left, top)
72    cr.line_to(right, top)
73    cr.line_to(right, bottom)
74    cr.line_to(left, bottom)
75    cr.close_path()
76
77def stroke_and_fill(cr, colour):
78    """Stroke with the current colour then fill the same path with the
79    given colour"""
80    join = cr.get_line_join()
81    cr.set_line_join(gtk.gdk.JOIN_ROUND)
82    cr.close_path()
83    cr.set_source_color(backgroundColour)
84    cr.stroke_preserve()
85    cr.set_source_color(colour)
86    cr.fill()
87    cr.set_line_join(join)
88
89def striped_box(cr, centre, size, colours):
90    """Fill a rectangle (without outline) striped with the colours given"""
91    num_colours = len(colours)
92    if num_colours == 0:
93        box(cr, centre, size)
94        cr.set_source_color(backgroundColour)
95        cr.fill()
96    elif num_colours == 1:
97        box(cr, centre, size)
98        stroke_and_fill(cr, colours[0])
99    else:
100        (left, right, top, bottom) = centre_size_to_sides(centre, size)
101        (width, height) = size.to_pair()
102        x_stripe_width = width / num_colours
103        half_x_stripe_width = x_stripe_width / 2.0
104        # Left triangle
105        cr.move_to(left,  bottom)
106        cr.line_to(left + half_x_stripe_width, bottom)
107        cr.line_to(left + x_stripe_width + half_x_stripe_width, top)
108        cr.line_to(left, top)
109        stroke_and_fill(cr, colours[0])
110        # Stripes
111        for i in xrange(1, num_colours - 1):
112            xOffset = x_stripe_width * i
113            cr.move_to(left + xOffset - half_x_stripe_width, bottom)
114            cr.line_to(left + xOffset + half_x_stripe_width, bottom)
115            cr.line_to(left + xOffset + x_stripe_width +
116                half_x_stripe_width, top)
117            cr.line_to(left + xOffset + x_stripe_width -
118                half_x_stripe_width, top)
119            stroke_and_fill(cr, colours[i])
120        # Right triangle
121        cr.move_to((right - x_stripe_width) - half_x_stripe_width, bottom)
122        cr.line_to(right, bottom)
123        cr.line_to(right, top)
124        cr.line_to((right - x_stripe_width) + half_x_stripe_width, top)
125        stroke_and_fill(cr, colours[num_colours - 1])
126
127def speech_bubble(cr, top_left, size, unit):
128    """Draw a speech bubble with 'size'-sized internal space with its
129    top left corner at Point(2.0 * unit, 2.0 * unit)"""
130    def local_arc(centre, angleFrom, angleTo):
131        cr.arc(centre.x, centre.y, unit, angleFrom * math.pi,
132            angleTo * math.pi)
133
134    cr.move_to(*top_left.to_pair())
135    cr.rel_line_to(unit * 2.0, unit)
136    cr.rel_line_to(size.x, 0.0)
137    local_arc(top_left + Point(size.x + unit * 2.0, unit * 2.0), -0.5, 0.0)
138    cr.rel_line_to(0.0, size.y)
139    local_arc(top_left + Point(size.x + unit * 2.0, size.y + unit * 2.0),
140        0, 0.5)
141    cr.rel_line_to(-size.x, 0.0)
142    local_arc(top_left + Point(unit * 2.0, size.y + unit * 2.0), 0.5, 1.0)
143    cr.rel_line_to(0, -size.y)
144    cr.close_path()
145
146def open_bottom(cr, centre, size):
147    """Draw a box with left, top and right sides"""
148    (left, right, top, bottom) = centre_size_to_sides(centre, size)
149    cr.move_to(left, bottom)
150    cr.line_to(left, top)
151    cr.line_to(right, top)
152    cr.line_to(right, bottom)
153
154def fifo(cr, centre, size):
155    """Draw just the vertical sides of a box"""
156    (left, right, top, bottom) = centre_size_to_sides(centre, size)
157    cr.move_to(left, bottom)
158    cr.line_to(left, top)
159    cr.move_to(right, bottom)
160    cr.line_to(right, top)
161
162def cross(cr, centre, size):
163    """Draw a cross parallel with the axes"""
164    (left, right, top, bottom) = centre_size_to_sides(centre, size)
165    (x, y) = centre.to_pair()
166    cr.move_to(left, y)
167    cr.line_to(right, y)
168    cr.move_to(x, top)
169    cr.line_to(x, bottom)
170
171class Blob(object):
172    """Blob super class"""
173    def __init__(self, picChar, unit, topLeft, colour, size = Point(1,1)):
174        self.picChar = picChar
175        self.unit = unit
176        self.displayName = unit
177        self.nameLoc = 'top'
178        self.topLeft = topLeft
179        self.colour = colour
180        self.size = size
181        self.border = 1.0
182        self.dataSelect = model.BlobDataSelect()
183        self.shorten = 0
184
185    def render(self, cr, view, event, select, time):
186        """Render this blob with the given event's data.  Returns either
187        None or a pair of (centre, size) in device coordinates for the drawn
188        blob.  The return value can be used to detect if mouse clicks on
189        the canvas are within the blob"""
190        return None
191
192class Block(Blob):
193    """Blocks are rectangular blogs colourable with a 2D grid of striped
194    blocks.  visualDecoder specifies how event data becomes this coloured
195    grid"""
196    def __init__(self, picChar, unit, topLeft=Point(0,0),
197        colour=colours.black,
198        size=Point(1,1)):
199        super(Block,self).__init__(picChar, unit, topLeft, colour,
200            size = size)
201        # {horiz, vert}
202        self.stripDir = 'horiz'
203        # {LR, RL}: LR means the first strip will be on the left/top,
204        #   RL means the first strip will be on the right/bottom
205        self.stripOrd = 'LR'
206        # Number of blank strips if this is a frame
207        self.blankStrips = 0
208        # {box, fifo, openBottom}
209        self.shape = 'box'
210        self.visualDecoder = None
211
212    def render(self, cr, view, event, select, time):
213        # Find the right event, visuals and sizes for things
214        if event is None or self.displayName.startswith('_'):
215            event = model.BlobEvent(self.unit, time)
216
217        if self.picChar in event.visuals:
218            strips = event.visuals[self.picChar].to_striped_block(
219                select & self.dataSelect)
220        else:
221            strips = [[[colours.unknownColour]]]
222
223        if self.stripOrd == 'RL':
224            strips.reverse()
225
226        if len(strips) == 0:
227            strips = [[colours.errorColour]]
228            print 'Problem with the colour of event:', event
229
230        num_strips = len(strips)
231        strip_proportion = 1.0 / num_strips
232        first_strip_offset = (num_strips / 2.0) - 0.5
233
234        # Adjust blocks with 'shorten' attribute to the length of the data
235        size = Point(*self.size.to_pair())
236        if self.shorten != 0 and self.size.x > (num_strips * self.shorten):
237            size.x = num_strips * self.shorten
238
239        box_size = size - view.blobIndentFactor.scale(2)
240
241        # Now do cr sensitive things
242        cr.save()
243        cr.scale(*view.pitch.to_pair())
244        cr.translate(*self.topLeft.to_pair())
245        cr.translate(*(size - Point(1,1)).scale(0.5).to_pair())
246
247        translated_centre = Point(*cr.user_to_device(0.0, 0.0))
248        translated_size = \
249            Point(*cr.user_to_device_distance(*size.to_pair()))
250
251        # The 2D grid is a grid of strips of blocks.  Data [[1,2],[3]]
252        # is 2 strips of 2 and 1 blocks respectively.
253        # if stripDir == 'horiz', strips are stacked vertically
254        #   from top to bottom if stripOrd == 'LR' or bottom to top if
255        #   stripOrd == 'RL'.
256        # if stripDir == 'vert', strips are stacked horizontally
257        #   from left to right if stripOf == 'LR' or right to left if
258        #   stripOrd == 'RL'.
259
260        strip_is_horiz = self.stripDir == 'horiz'
261
262        if strip_is_horiz:
263            strip_step_base = Point(1.0,0.0)
264            block_step_base = Point(0.0,1.0)
265        else:
266            strip_step_base = Point(0.0,1.0)
267            block_step_base = Point(1.0,0.0)
268
269        strip_size = (box_size * (strip_step_base.scale(strip_proportion) +
270            block_step_base))
271        strip_step = strip_size * strip_step_base
272        strip_centre = Point(0,0) - (strip_size *
273            strip_step_base.scale(first_strip_offset))
274
275        cr.set_line_width(view.midLineWidth / view.pitch.x)
276
277        # Draw the strips and their blocks
278        for strip_index in xrange(0, num_strips):
279            num_blocks = len(strips[strip_index])
280            block_proportion = 1.0 / num_blocks
281            firstBlockOffset = (num_blocks / 2.0) - 0.5
282
283            block_size = (strip_size *
284                (block_step_base.scale(block_proportion) +
285                strip_step_base))
286            block_step = block_size * block_step_base
287            block_centre = (strip_centre + strip_step.scale(strip_index) -
288                (block_size * block_step_base.scale(firstBlockOffset)))
289
290            for block_index in xrange(0, num_blocks):
291                striped_box(cr, block_centre +
292                    block_step.scale(block_index), block_size,
293                    strips[strip_index][block_index])
294
295        cr.set_font_size(0.7)
296        if self.border > 0.5:
297            weight = cairo.FONT_WEIGHT_BOLD
298        else:
299            weight = cairo.FONT_WEIGHT_NORMAL
300        cr.select_font_face('Helvetica', cairo.FONT_SLANT_NORMAL,
301            weight)
302
303        xb, yb, width, height, dx, dy = cr.text_extents(self.displayName)
304
305        text_comfort_space = 0.15
306
307        if self.nameLoc == 'left':
308            # Position text vertically along left side, top aligned
309            cr.save()
310            cr.rotate(- (math.pi / 2.0))
311            text_point = Point(size.y, size.x).scale(0.5) * Point(-1, -1)
312            text_point += Point(max(0, size.y - width), 0)
313            text_point += Point(-text_comfort_space, -text_comfort_space)
314        else: # Including top
315            # Position text above the top left hand corner
316            text_point = size.scale(0.5) * Point(-1,-1)
317            text_point += Point(0.00, -text_comfort_space)
318
319        if (self.displayName != '' and
320            not self.displayName.startswith('_')):
321            cr.set_source_color(self.colour)
322            cr.move_to(*text_point.to_pair())
323            cr.show_text(self.displayName)
324
325        if self.nameLoc == 'left':
326            cr.restore()
327
328        # Draw the outline shape
329        cr.save()
330        if strip_is_horiz:
331            cr.rotate(- (math.pi / 2.0))
332            box_size = Point(box_size.y, box_size.x)
333
334        if self.stripOrd == "RL":
335            cr.rotate(math.pi)
336
337        if self.shape == 'box':
338            box(cr, Point(0,0), box_size)
339        elif self.shape == 'openBottom':
340            open_bottom(cr, Point(0,0), box_size)
341        elif self.shape == 'fifo':
342            fifo(cr, Point(0,0), box_size)
343        cr.restore()
344
345        # Restore scale and stroke the outline
346        cr.restore()
347        cr.set_source_color(self.colour)
348        cr.set_line_width(view.thickLineWidth * self.border)
349        cr.stroke()
350
351        # Return blob size/position
352        if self.unit == '_':
353            return None
354        else:
355            return (translated_centre, translated_size)
356
357class Key(Blob):
358    """Draw a key to the special (and numeric colours) with swatches of the
359    colours half as wide as the key"""
360    def __init__(self, picChar, unit, topLeft, colour=colours.black,
361        size=Point(1,1)):
362        super(Key,self).__init__(picChar, unit, topLeft, colour, size = size)
363        self.colours = 'BBBB'
364        self.displayName = unit
365
366    def render(self, cr, view, event, select, time):
367        cr.save()
368        cr.scale(*view.pitch.to_pair())
369        cr.translate(*self.topLeft.to_pair())
370        # cr.translate(*(self.size - Point(1,1)).scale(0.5).to_pair())
371        half_width = self.size.x / 2.0
372        cr.translate(*(self.size - Point(1.0 + half_width,1.0)).scale(0.5).
373            to_pair())
374
375        num_colours = len(self.colours)
376        cr.set_line_width(view.midLineWidth / view.pitch.x)
377
378        blob_size = (Point(half_width,0.0) +
379            (self.size * Point(0.0,1.0 / num_colours)))
380        blob_step = Point(0.0,1.0) * blob_size
381        first_blob_centre = (Point(0.0,0.0) -
382            blob_step.scale((num_colours / 2.0) - 0.5))
383
384        cr.set_source_color(self.colour)
385        cr.set_line_width(view.thinLineWidth / view.pitch.x)
386
387        blob_proportion = 0.8
388
389        real_blob_size = blob_size.scale(blob_proportion)
390
391        cr.set_font_size(0.8 * blob_size.y * blob_proportion)
392        cr.select_font_face('Helvetica', cairo.FONT_SLANT_NORMAL,
393            cairo.FONT_WEIGHT_BOLD)
394
395        for i in xrange(0, num_colours):
396            centre = first_blob_centre + blob_step.scale(i)
397            box(cr, centre, real_blob_size)
398
399            colour_char = self.colours[i]
400            if colour_char.isdigit():
401                cr.set_source_color(colours.number_to_colour(
402                    int(colour_char)))
403                label = '...' + colour_char
404            else:
405                cr.set_source_color(model.special_state_colours[colour_char])
406                label = model.special_state_names[colour_char]
407
408            cr.fill_preserve()
409            cr.set_source_color(self.colour)
410            cr.stroke()
411
412            xb, yb, width, height, dx, dy = cr.text_extents(label)
413
414            text_left = (centre + (Point(0.5,0.0) * blob_size) +
415                Point(0.0, height / 2.0))
416
417            cr.move_to(*text_left.to_pair())
418            cr.show_text(label)
419
420class Arrow(Blob):
421    """Draw a left or right facing arrow"""
422    def __init__(self, unit, topLeft, colour=colours.black,
423        size=Point(1.0,1.0), direc='right'):
424        super(Arrow,self).__init__(unit, unit, topLeft, colour, size = size)
425        self.direc = direc
426
427    def render(self, cr, view, event, select, time):
428        cr.save()
429        cr.scale(*view.pitch.to_pair())
430        cr.translate(*self.topLeft.to_pair())
431        cr.translate(*(self.size - Point(1,1)).scale(0.5).to_pair())
432        cr.scale(*self.size.to_pair())
433        (blob_indent_x, blob_indent_y) = \
434            (view.blobIndentFactor / self.size).to_pair()
435        left = -0.5 - blob_indent_x
436        right = 0.5 + blob_indent_x
437
438        thickness = 0.2
439        flare = 0.2
440
441        if self.direc == 'left':
442            cr.rotate(math.pi)
443
444        cr.move_to(left, -thickness)
445        cr.line_to(0.0, -thickness)
446        cr.line_to(0.0, -(thickness + flare))
447        cr.line_to(right, 0)
448        # Break arrow to prevent the point ruining the appearance of boxes
449        cr.move_to(right, 0)
450        cr.line_to(0.0, (thickness + flare))
451        cr.line_to(0.0, +thickness)
452        cr.line_to(left, +thickness)
453
454        cr.restore()
455
456        # Draw arrow a bit more lightly than the standard line width
457        cr.set_line_width(cr.get_line_width() * 0.75)
458        cr.set_source_color(self.colour)
459        cr.stroke()
460
461        return None
462