PageRenderTime 40ms CodeModel.GetById 16ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 1ms

/tortoisehg/hgtk/logview/graphcell.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 275 lines | 267 code | 2 blank | 6 comment | 4 complexity | 14f5199c14c48f4261e70e392a5d52d4 MD5 | raw file
  1"""Cell renderer for directed graph.
  2
  3This module contains the implementation of a custom GtkCellRenderer that
  4draws part of the directed graph based on the lines suggested by the code
  5in graph.py.
  6
  7Because we're shiny, we use Cairo to do this, and because we're naughty
  8we cheat and draw over the bits of the TreeViewColumn that are supposed to
  9just be for the background.
 10"""
 11
 12__copyright__ = "Copyright 2005 Canonical Ltd."
 13__author__    = "Scott James Remnant <scott@ubuntu.com>"
 14
 15import math
 16
 17import gtk
 18import gobject
 19import pango
 20import cairo
 21
 22from tortoisehg.hgtk import gtklib
 23
 24# Styles used when rendering revision graph edges
 25style_SOLID = 0
 26style_DASHED = 1
 27
 28class CellRendererGraph(gtk.GenericCellRenderer):
 29    """Cell renderer for directed graph.
 30
 31    Properties:
 32      node              (column, colour) tuple to draw revision node,
 33      in_lines          (start, end, colour, style) tuple list to draw inward lines,
 34      out_lines         (start, end, colour, style) tuple list to draw outward lines.
 35    """
 36
 37    columns_len = 0
 38
 39    __gproperties__ = {
 40        "node":         ( gobject.TYPE_PYOBJECT, "node",
 41                          "revision node instruction",
 42                          gobject.PARAM_WRITABLE
 43                        ),
 44        "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
 45                          "instructions to draw lines into the cell",
 46                          gobject.PARAM_WRITABLE
 47                        ),
 48        "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
 49                          "instructions to draw lines out of the cell",
 50                          gobject.PARAM_WRITABLE
 51                        ),
 52        }
 53
 54    def do_set_property(self, property, value):
 55        """Set properties from GObject properties."""
 56        if property.name == "node":
 57            self.node = value
 58        elif property.name == "in-lines":
 59            self.in_lines = value
 60        elif property.name == "out-lines":
 61            self.out_lines = value
 62        else:
 63            raise AttributeError, "no such property: '%s'" % property.name
 64
 65    def box_size(self, widget):
 66        """Calculate box size based on widget's font.
 67
 68        Cache this as it's probably expensive to get.  It ensures that we
 69        draw the graph at least as large as the text.
 70        """
 71        try:
 72            return self._box_size
 73        except AttributeError:
 74            pango_ctx = widget.get_pango_context()
 75            font_desc = widget.get_style().font_desc
 76            metrics = pango_ctx.get_metrics(font_desc)
 77
 78            ascent = pango.PIXELS(metrics.get_ascent())
 79            descent = pango.PIXELS(metrics.get_descent())
 80
 81            self._box_size = ascent + descent + 1
 82            return self._box_size
 83
 84    def set_colour(self, ctx, colour, bg, fg):
 85        """Set the context source colour.
 86
 87        Picks a distinct colour based on an internal wheel; the bg
 88        parameter provides the value that should be assigned to the 'zero'
 89        colours and the fg parameter provides the multiplier that should be
 90        applied to the foreground colours.
 91        """
 92
 93        if isinstance(colour, str):
 94            r, g, b = colour[1:3], colour[3:5], colour[5:7]
 95            colour_rgb = int(r, 16) / 255., int(g, 16) / 255., int(b, 16) / 255.
 96        else:
 97            if colour == 0:
 98                colour_rgb = gtklib.MAINLINE_COLOR
 99            else:
100                colour_rgb = gtklib.LINE_COLORS[colour % len(gtklib.LINE_COLORS)]
101
102        red   = (colour_rgb[0] * fg) or bg
103        green = (colour_rgb[1] * fg) or bg
104        blue  = (colour_rgb[2] * fg) or bg
105
106        ctx.set_source_rgb(red, green, blue)
107
108    def on_get_size(self, widget, cell_area):
109        """Return the size we need for this cell.
110
111        Each cell is drawn individually and is only as wide as it needs
112        to be, we let the TreeViewColumn take care of making them all
113        line up.
114        """
115        box_size = self.box_size(widget) + 1
116
117        width = box_size * (self.columns_len + 1)
118        height = box_size
119
120        # FIXME I have no idea how to use cell_area properly
121        return (0, 0, width, height)
122
123    def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
124        """Render an individual cell.
125
126        Draws the cell contents using cairo, taking care to clip what we
127        do to within the background area so we don't draw over other cells.
128        Note that we're a bit naughty there and should really be drawing
129        in the cell_area (or even the exposed area), but we explicitly don't
130        want any gutter.
131
132        We try and be a little clever, if the line we need to draw is going
133        to cross other columns we actually draw it as in the .---' style
134        instead of a pure diagonal ... this reduces confusion by an
135        incredible amount.
136        """
137        ctx = window.cairo_create()
138        ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
139        ctx.clip()
140
141        box_size = self.box_size(widget)
142
143        # Maybe draw branch head highlight under revision node
144        if self.node:
145            (column, colour, status) = self.node
146            arc_start_position_x = cell_area.x + box_size * column + box_size / 2; 
147            arc_start_position_y = cell_area.y + cell_area.height / 2;
148
149            if status >= 8:   # branch head
150                ctx.arc(arc_start_position_x, arc_start_position_y,
151                    box_size /1.7, 0, 2 * math.pi)
152                self.set_colour(ctx, gtklib.PGREEN, 0.0, 1.0)
153                ctx.fill()
154                status -= 8
155
156        ctx.set_line_width(box_size / 8)
157        ctx.set_line_cap(cairo.LINE_CAP_ROUND)
158
159        # Draw lines into the cell
160        for start, end, lcolour, type in self.in_lines:
161            style = style_SOLID
162            if type & 1:
163                style = style_DASHED
164            self.render_line (ctx, cell_area, box_size,
165                         bg_area.y, bg_area.height,
166                         start, end, lcolour, style)
167
168        # Draw lines out of the cell
169        for start, end, lcolour, type in self.out_lines:
170            style = style_SOLID
171            if type & 2:
172                style = style_DASHED
173            self.render_line (ctx, cell_area, box_size,
174                         bg_area.y + bg_area.height, bg_area.height,
175                         start, end, lcolour, style)
176
177        # Draw the revision node in the right column
178        if not self.node:
179            return
180
181        if status >= 4:  # working directory parent
182            ctx.arc(arc_start_position_x, arc_start_position_y,
183                    box_size / 4, 0, 2 * math.pi)
184            status -= 4
185        else:
186            ctx.arc(arc_start_position_x, arc_start_position_y,
187                    box_size / 5, 0, 2 * math.pi)
188        self.set_colour(ctx, colour, 0.0, 0.5)
189        ctx.stroke_preserve()
190        self.set_colour(ctx, colour, 0.5, 1.0)
191        ctx.fill()
192
193        # Possible node status
194        if status != 0:
195            def draw_arrow(x, y, dir):
196                self.set_colour(ctx, gtklib.CELL_GREY, 0.0, 1.0)
197                ctx.rectangle(x, y, 2, 5)
198                ax, ay = x, y + (dir == 'down' and 5 or 0)
199                inc = 3 * (dir == 'up' and -1 or 1)
200                ctx.move_to(ax - 2, ay)
201                ctx.line_to(ax + 4, ay)
202                ctx.line_to(ax + 1, ay + inc)
203                ctx.line_to(ax - 2, ay)
204                ctx.stroke_preserve()
205                fillcolor = dir == 'up' and gtklib.UP_ARROW_COLOR or gtklib.DOWN_ARROW_COLOR
206                self.set_colour(ctx, fillcolor, 0.0, 1.0)
207                ctx.fill()
208
209            def draw_star(x, y, radius, nodes, offset=False):
210                self.set_colour(ctx, gtklib.CELL_GREY, 0.0, 1.0)
211                total_nodes = nodes * 2 #inner + outer nodes
212                angle = 2 * math.pi / total_nodes;
213                offset = offset and angle / 2 or 0
214                for value in range(total_nodes + 1): # + 1 = backing to the start to close
215                    radius_point = radius
216                    if value % 2:
217                        radius_point = 0.4 * radius_point;
218                    arc_y = y - math.sin(angle * value + offset) * radius_point
219                    arc_x = x - math.cos(angle * value + offset) * radius_point
220                    if value == 0:
221                        ctx.move_to(arc_x,arc_y)
222                    else:
223                        ctx.line_to(arc_x, arc_y)
224                ctx.stroke_preserve()
225                self.set_colour(ctx, gtklib.STAR_COLOR, 0.0, 1.0)
226                ctx.fill()
227
228            arrow_y = arc_start_position_y - box_size / 4
229            arrow_x = arc_start_position_x + 7;
230            if status == 1:  # Outgoing arrow
231                draw_arrow(arrow_x, arrow_y, 'up')
232            elif status == 2: # New changeset, recently added to tip
233                draw_star(arrow_x + box_size / 4 - 1,
234                          arc_start_position_y, 4, 5, True)
235            elif status == 3:  # Incoming (bundle preview) arrow
236                draw_arrow(arrow_x, arrow_y, 'down')
237
238    def render_line (self, ctx, cell_area, box_size, mid,
239            height, start, end, colour, style):
240        if start is None:
241            x = cell_area.x + box_size * end + box_size / 2
242            ctx.move_to(x, mid + height / 3)
243            ctx.line_to(x, mid + height / 3)
244            ctx.move_to(x, mid + height / 6)
245            ctx.line_to(x, mid + height / 6)
246
247        elif end is None:
248            x = cell_area.x + box_size * start + box_size / 2
249            ctx.move_to(x, mid - height / 3)
250            ctx.line_to(x, mid - height / 3)
251            ctx.move_to(x, mid - height / 6)
252            ctx.line_to(x, mid - height / 6)
253        else:
254            startx = cell_area.x + box_size * start + box_size / 2
255            endx = cell_area.x + box_size * end + box_size / 2
256
257            ctx.move_to(startx, mid - height / 2)
258
259            if start - end == 0 :
260                ctx.line_to(endx, mid + height / 2)
261            else:
262                ctx.curve_to(startx, mid - height / 5,
263                             startx, mid - height / 5,
264                             startx + (endx - startx) / 2, mid)
265
266                ctx.curve_to(endx, mid + height / 5,
267                             endx, mid + height / 5 ,
268                             endx, mid + height / 2)
269
270        self.set_colour(ctx, colour, 0.0, 0.65)
271        if style == style_DASHED:
272            dashes = [1, 2]
273            ctx.set_dash(dashes)
274        ctx.stroke()
275        ctx.set_dash([])