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