1
# -*- coding: UTF-8 -*-
2
"""Cell renderer for directed graph.
4
This module contains the implementation of a custom GtkCellRenderer that
5
draws part of the directed graph based on the lines suggested by the code
8
Because we're shiny, we use Cairo to do this, and because we're naughty
9
we cheat and draw over the bits of the TreeViewColumn that are supposed to
10
just be for the background.
13
__copyright__ = "Copyright © 2005 Canonical Ltd."
14
__author__ = "Scott James Remnant <scott@ubuntu.com>"
19
from gi.repository import Gtk
20
from gi.repository import GObject
21
from gi.repository import Pango
25
class CellRendererGraph(Gtk.GenericCellRenderer):
26
"""Cell renderer for directed graph.
29
node (column, colour) tuple to draw revision node,
30
in_lines (start, end, colour) tuple list to draw inward lines,
31
out_lines (start, end, colour) tuple list to draw outward lines.
37
"node": ( GObject.TYPE_PYOBJECT, "node",
38
"revision node instruction",
39
GObject.PARAM_WRITABLE
41
"tags": ( GObject.TYPE_PYOBJECT, "tags",
42
"list of tags associated with the node",
43
GObject.PARAM_WRITABLE
45
"in-lines": ( GObject.TYPE_PYOBJECT, "in-lines",
46
"instructions to draw lines into the cell",
47
GObject.PARAM_WRITABLE
49
"out-lines": ( GObject.TYPE_PYOBJECT, "out-lines",
50
"instructions to draw lines out of the cell",
51
GObject.PARAM_WRITABLE
55
def do_set_property(self, property, value):
56
"""Set properties from GObject properties."""
57
if property.name == "node":
59
elif property.name == "tags":
61
elif property.name == "in-lines":
63
elif property.name == "out-lines":
64
self.out_lines = value
66
raise AttributeError, "no such property: '%s'" % property.name
68
def box_size(self, widget):
69
"""Calculate box size based on widget's font.
71
Cache this as it's probably expensive to get. It ensures that we
72
draw the graph at least as large as the text.
76
except AttributeError:
77
pango_ctx = widget.get_pango_context()
78
font_desc = widget.get_style().font_desc
79
metrics = pango_ctx.get_metrics(font_desc)
81
ascent = Pango.PIXELS(metrics.get_ascent())
82
descent = Pango.PIXELS(metrics.get_descent())
84
self._box_size = ascent + descent + 6
87
def set_colour(self, ctx, colour, bg, fg):
88
"""Set the context source colour.
90
Picks a distinct colour based on an internal wheel; the bg
91
parameter provides the value that should be assigned to the 'zero'
92
colours and the fg parameter provides the multiplier that should be
93
applied to the foreground colours.
95
mainline_color = ( 0.0, 0.0, 0.0 )
106
colour_rgb = mainline_color
108
colour_rgb = colours[colour % len(colours)]
110
red = (colour_rgb[0] * fg) or bg
111
green = (colour_rgb[1] * fg) or bg
112
blue = (colour_rgb[2] * fg) or bg
114
ctx.set_source_rgb(red, green, blue)
116
def on_get_size(self, widget, cell_area):
117
"""Return the size we need for this cell.
119
Each cell is drawn individually and is only as wide as it needs
120
to be, we let the TreeViewColumn take care of making them all
123
box_size = self.box_size(widget) + 1
125
width = box_size * (self.columns_len + 1)
128
# FIXME I have no idea how to use cell_area properly
129
return (0, 0, width, height)
131
def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
132
"""Render an individual cell.
134
Draws the cell contents using cairo, taking care to clip what we
135
do to within the background area so we don't draw over other cells.
136
Note that we're a bit naughty there and should really be drawing
137
in the cell_area (or even the exposed area), but we explicitly don't
140
We try and be a little clever, if the line we need to draw is going
141
to cross other columns we actually draw it as in the .---' style
142
instead of a pure diagonal ... this reduces confusion by an
145
ctx = window.cairo_create()
146
ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
149
box_size = self.box_size(widget)
151
ctx.set_line_width(box_size / 8)
153
# Draw lines into the cell
154
for start, end, colour in self.in_lines:
155
self.render_line (ctx, cell_area, box_size,
156
bg_area.y, bg_area.height,
157
start, end, colour, flags)
159
# Draw lines out of the cell
160
for start, end, colour in self.out_lines:
161
self.render_line (ctx, cell_area, box_size,
162
bg_area.y + bg_area.height, bg_area.height,
163
start, end, colour, flags)
165
# Draw the revision node in the right column
166
(column, colour) = self.node
167
ctx.arc(cell_area.x + box_size * column + box_size / 2,
168
cell_area.y + cell_area.height / 2,
169
box_size / 4, 0, 2 * math.pi)
171
if flags & Gtk.CELL_RENDERER_SELECTED:
172
ctx.set_source_rgb(1.0, 1.0, 1.0)
173
ctx.set_line_width(box_size / 4)
174
ctx.stroke_preserve()
175
ctx.set_line_width(box_size / 8)
177
self.set_colour(ctx, colour, 0.0, 0.5)
178
ctx.stroke_preserve()
180
self.set_colour(ctx, colour, 0.5, 1.0)
183
self.render_tags(ctx, widget.create_pango_context(), cell_area, box_size)
185
def render_line(self, ctx, cell_area, box_size, mid, height, start, end, colour, flags):
187
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
188
x = cell_area.x + box_size * end + box_size / 2
189
ctx.move_to(x, mid + height / 3)
190
ctx.line_to(x, mid + height / 3)
191
ctx.move_to(x, mid + height / 6)
192
ctx.line_to(x, mid + height / 6)
195
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
196
x = cell_area.x + box_size * start + box_size / 2
197
ctx.move_to(x, mid - height / 3)
198
ctx.line_to(x, mid - height / 3)
199
ctx.move_to(x, mid - height / 6)
200
ctx.line_to(x, mid - height / 6)
203
ctx.set_line_cap(cairo.LINE_CAP_BUTT)
204
startx = cell_area.x + box_size * start + box_size / 2
205
endx = cell_area.x + box_size * end + box_size / 2
207
ctx.move_to(startx, mid - height / 2)
209
if start - end == 0 :
210
ctx.line_to(endx, mid + height / 2 + 1)
212
ctx.curve_to(startx, mid - height / 5,
213
startx, mid - height / 5,
214
startx + (endx - startx) / 2, mid)
216
ctx.curve_to(endx, mid + height / 5,
217
endx, mid + height / 5 ,
218
endx, mid + height / 2 + 1)
220
if flags & Gtk.CELL_RENDERER_SELECTED:
221
ctx.set_source_rgb(1.0, 1.0, 1.0)
222
ctx.set_line_width(box_size / 5)
223
ctx.stroke_preserve()
224
ctx.set_line_width(box_size / 8)
226
self.set_colour(ctx, colour, 0.0, 0.65)
230
def render_tags(self, ctx, pango_ctx, cell_area, box_size):
231
# colour ID used in self.set_colour on the tags
234
(column, colour) = self.node
236
font_desc = Pango.FontDescription()
237
font_desc.set_size(Pango.SCALE * 7)
239
tag_layout = Pango.Layout(pango_ctx)
240
tag_layout.set_font_description(font_desc)
242
# The width of the tag label stack
245
for tag_idx, tag in enumerate(self.tags):
246
tag_layout.set_text(" " + tag + " ")
247
text_width, text_height = tag_layout.get_pixel_size()
250
box_size * (column + 1.3) + width
253
cell_area.height / 2 - \
256
width += text_width + 5
258
# Draw the tag border
259
ctx.move_to(x0 - box_size / 3, y0 + text_height / 2)
261
ctx.line_to(x0 + text_width, y0)
262
ctx.line_to(x0 + text_width, y0 + text_height)
263
ctx.line_to(x0, y0 + text_height)
264
ctx.line_to(x0 - box_size / 3, y0 + text_height / 2)
267
ctx.arc(x0 - box_size / 12,
268
y0 + text_height / 2,
272
self.set_colour(ctx, TAG_COLOUR_ID, 0.0, 0.5)
273
ctx.stroke_preserve()
275
ctx.set_fill_rule (cairo.FILL_RULE_EVEN_ODD)
276
self.set_colour(ctx, TAG_COLOUR_ID, 0.5, 1.0)
280
self.set_colour(ctx, 0, 0.0, 0.0)
282
ctx.show_layout(tag_layout)