Don't leak PangoLayouts.
[dom-editor.git] / Dome / Display2.py
blob820093eba3aee1622bd224a443a44d9db6d447bd
1 from __future__ import generators
3 import rox
4 from rox import g
5 from xml.dom import Node
6 import pango
8 import __main__
9 default_font = __main__.default_font
11 def calc_node(display, node, pos):
12 attribs = []
13 if node.nodeType == Node.TEXT_NODE:
14 text = node.nodeValue.strip()
15 elif node.nodeType == Node.ELEMENT_NODE:
16 text = node.nodeName
17 elif node.nodeType == Node.ATTRIBUTE_NODE:
18 text = ' %s=%s' % (unicode(node.name), unicode(node.value))
19 elif node.nodeType == Node.COMMENT_NODE:
20 text = node.nodeValue.strip()
21 elif node.nodeName:
22 text = node.nodeName
23 elif node.nodeValue:
24 text = '<noname>' + node.nodeValue
25 else:
26 text = '<unknown>'
28 # PyGtk leaks PangoLayouts, so just reuse a single one
29 layout = display.surface_layout
30 layout.set_text(text, -1)
31 width, height = layout.get_pixel_size()
32 x, y = pos
34 text_x = x
35 if node.nodeType != Node.ATTRIBUTE_NODE:
36 text_x += 12
38 def draw_fn():
39 surface = display.pm
40 style = display.surface.style # Different surface ;-)
41 fg = style.fg_gc
42 bg = style.bg_gc
44 if node.nodeType != Node.ATTRIBUTE_NODE:
45 marker = True
46 surface.draw_rectangle(fg[g.STATE_NORMAL], True,
47 x, y, 8, height - 1)
48 if node.nodeType == Node.ELEMENT_NODE and node.hasAttributeNS(None, 'hidden'):
49 surface.draw_layout(fg[g.STATE_PRELIGHT], text_x + width + 2, y,
50 display.create_pango_layout('(hidden)'))
51 else:
52 marker = False
54 if node in display.selection:
55 if marker:
56 surface.draw_rectangle(style.bg_gc[g.STATE_SELECTED], True,
57 x + 1, y + 1, 6, height - 3)
58 surface.draw_rectangle(bg[g.STATE_SELECTED], True,
59 text_x, y, width - 1, height - 1)
60 surface.draw_layout(fg[g.STATE_SELECTED], text_x, y, layout)
61 else:
62 if marker:
63 surface.draw_rectangle(style.white_gc, True, x + 1, y + 1, 6, height - 3)
64 if node.nodeType == Node.TEXT_NODE:
65 gc = style.text_gc[g.STATE_NORMAL]
66 elif node.nodeType == Node.ATTRIBUTE_NODE:
67 gc = style.fg_gc[g.STATE_INSENSITIVE]
68 elif node.nodeType == Node.COMMENT_NODE:
69 gc = style.text_gc[g.STATE_INSENSITIVE]
70 else:
71 gc = style.fg_gc[g.STATE_NORMAL]
72 surface.draw_layout(gc, text_x, y, layout)
74 if node in display.view.marked:
75 surface.draw_rectangle(style.text_gc[g.STATE_PRELIGHT], False,
76 x - 1, y - 1, width + (text_x - x), height)
78 bbox = (x, y, text_x + width, y + height)
79 return bbox, draw_fn
81 class Display(g.HBox):
82 visible = 1 # Always visible
84 def __init__(self, window, view):
85 g.HBox.__init__(self, False, 0)
87 self.surface = g.EventBox()
88 self.surface_layout = self.surface.create_pango_layout('')
89 self.pack_start(self.surface, True, True, 0)
90 self.surface.show()
91 self.surface.set_app_paintable(True)
92 self.surface.set_double_buffered(False)
93 self.update_timeout = 0
95 self.scroll_adj = g.Adjustment(lower = 0, upper = 100, step_incr = 1)
96 self.scroll_adj.connect('value-changed', self.scroll_to)
97 scale = g.VScale(self.scroll_adj)
98 scale.unset_flags(g.CAN_FOCUS)
99 scale.set_draw_value(False)
100 self.pack_start(scale, False, True, 0)
102 self.view = None
103 self.parent_window = window
104 self.pm = None
106 s = self.surface.get_style().copy()
107 s.bg[g.STATE_NORMAL] = g.gdk.color_parse('old lace')
108 s.text[g.STATE_NORMAL] = g.gdk.color_parse('blue')
109 s.text[g.STATE_PRELIGHT] = g.gdk.color_parse('orange') # Mark
110 s.text[g.STATE_INSENSITIVE] = g.gdk.color_parse('dark green')# Comment
111 s.fg[g.STATE_PRELIGHT] = g.gdk.color_parse('red') # Hidden
112 self.surface.set_style(s)
114 self.connect('destroy', self.destroyed)
115 self.surface.connect('button-press-event', self.bg_event)
116 self.surface.connect('button-release-event', self.bg_event)
118 # Display is relative to this node, which is the highest displayed node (possibly
119 # off the top of the screen)
120 self.ref_node = view.root
121 self.ref_pos = (0, 0)
123 self.last_alloc = None
124 self.surface.connect('size-allocate', lambda w, a: self.size_allocate(a))
125 self.surface.connect('size-request', lambda w, r: self.size_request(r))
126 self.surface.connect('expose-event', lambda w, e: 1)
128 self.pan_timeout = None
129 self.h_limits = (0, 0)
130 self.set_view(view)
132 def destroyed(self, widget):
133 self.view.remove_display(self)
135 def size_allocate(self, alloc):
136 new = (alloc.width, alloc.height)
137 if self.last_alloc == new:
138 return
139 self.last_alloc = new
140 assert self.window
141 #print "Alloc", alloc.width, alloc.height
142 pm = g.gdk.Pixmap(self.surface.window, alloc.width, alloc.height, -1)
143 self.surface.window.set_back_pixmap(pm, False)
144 self.pm = pm
145 self.update()
147 def update(self):
148 if not self.pm: return
149 #print "update"
151 self.update_timeout = 0
153 self.pm.draw_rectangle(self.surface.style.bg_gc[g.STATE_NORMAL], True,
154 0, 0, self.last_alloc[0], self.last_alloc[1])
156 self.drawn = {} # xmlNode -> ((x1, y1, y2, y2), attrib_parent)
158 n = self.ref_node
159 p = self.view.root.parentNode
160 while n is not p:
161 n = n.parentNode
162 if not n:
163 print "(lost root)"
164 self.ref_node = self.view.root
165 self.ref_pos = (0, 0)
166 break
168 if self.view.current_attrib:
169 self.selection = {self.view.current_attrib: None}
170 else:
171 self.selection = {}
172 for n in self.view.current_nodes:
173 self.selection[n] = None
175 pos = list(self.ref_pos)
176 self.h_limits = (self.ref_pos[0], self.ref_pos[0]) # Left, Right
177 node = self.ref_node
178 attr_parent = None
179 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
180 if bbox[1] > self.last_alloc[1]: break # Off-screen
182 draw_fn()
183 if node.nodeType == Node.ATTRIBUTE_NODE:
184 self.drawn[node] = (bbox, attr_parent)
185 else:
186 attr_parent = node
187 self.drawn[node] = (bbox, None)
189 if bbox[1] < 0 and node.nodeType != Node.ATTRIBUTE_NODE:
190 self.ref_node = node
191 self.ref_pos = bbox[:2]
192 self.h_limits = (min(self.h_limits[0], bbox[0]),
193 max(self.h_limits[1], bbox[2]))
195 self.surface.window.clear()
197 # Update adjustment
198 r = self.ref_node
199 n = 0
200 pos = 0
201 for x in self.quick_walk():
202 if x is r:
203 pos = n
204 n += 1
205 self.scroll_adj.value = float(pos)
206 self.scroll_adj.upper = float(n)
208 return 0
210 def quick_walk(self):
211 "Return all the nodes in the document, in document order. Not attributes."
212 yield self.view.root.parentNode
213 node = self.view.root
214 while node:
215 yield node
216 if node.childNodes:
217 node = node.childNodes[0]
218 else:
219 while not node.nextSibling:
220 node = node.parentNode
221 if not node: return
222 node = node.nextSibling
224 def walk_tree(self, node, pos):
225 """Yield this (node, bbox), and all following ones in document order."""
226 pos = list(pos)
227 while node:
228 bbox, draw_fn = calc_node(self, node, pos)
229 yield (node, bbox, draw_fn)
231 hidden = False
232 if node.nodeType == Node.ELEMENT_NODE:
233 hidden = node.hasAttributeNS(None, 'hidden')
234 if not hidden:
235 apos = [bbox[2] + 4, bbox[1]]
236 for key in node.attributes:
237 a = node.attributes[key]
238 abbox, draw_fn = calc_node(self, a, apos)
239 apos[0] = abbox[2] + 4
240 yield (a, abbox, draw_fn)
242 pos[1] = bbox[3] + 2
243 if node.childNodes and not hidden:
244 node = node.childNodes[0]
245 pos[0] += 16
246 else:
247 while not node.nextSibling:
248 node = node.parentNode
249 if not node: return
250 pos[0] -= 16
251 node = node.nextSibling
253 def size_request(self, req):
254 req.width = 4
255 req.height = 4
257 def do_update_now(self):
258 # Update now, if we need to
259 if self.update_timeout:
260 g.timeout_remove(self.update_timeout)
261 self.update()
263 def update_all(self, node = None):
264 if self.update_timeout:
265 return # Going to update anyway...
267 if self.view.running():
268 self.update_timeout = g.timeout_add(2000, self.update)
269 else:
270 self.update_timeout = g.timeout_add(10, self.update)
272 def move_from(self, old = []):
273 if not self.pm: return
274 if self.view.current_nodes:
275 selection = {}
276 for n in self.view.current_nodes:
277 selection[n] = None
278 shown = False
279 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
280 if bbox[1] > self.last_alloc[1]: break # Off-screen
281 if bbox[3] > 0 and node in selection:
282 shown = True
283 break # A selected node is shown
284 if not shown:
285 print "(selected nodes not shown)"
286 self.ref_node = self.view.current_nodes[0]
287 self.ref_pos = (40, self.last_alloc[1] / 2)
288 self.backup_ref_node()
289 self.update_all()
291 def set_view(self, view):
292 if self.view:
293 self.view.remove_display(self)
294 self.view = view
295 self.view.add_display(self)
296 self.update_all()
298 def show_menu(self, bev):
299 pass
301 def node_clicked(self, node, event):
302 pass
304 def xy_to_node(self, x, y):
305 "Return the node at this point and, if it's an attribute, its parent."
306 for (n, ((x1, y1, x2, y2), attrib_parent)) in self.drawn.iteritems():
307 if x >= x1 and x <= x2 and y >= y1 and y <= y2:
308 return n, attrib_parent
309 return None, None
311 def pan(self):
312 def scale(x):
313 val = (float(abs(x)) ** 1.4)
314 if x < 0:
315 return -val
316 else:
317 return val
318 def chop(x):
319 if x > 10: return x - 10
320 if x < -10: return x + 10
321 return 0
322 x, y, mask = self.surface.window.get_pointer()
323 sx, sy = self.pan_start
324 dx, dy = scale(chop(x - sx)) / 20, scale(chop(y - sy))
325 dx = max(dx, 10 - self.h_limits[1])
326 dx = min(dx, self.last_alloc[0] - 10 - self.h_limits[0])
327 new = [self.ref_pos[0] + dx, self.ref_pos[1] + dy]
329 if new == self.ref_pos:
330 return 1
332 self.ref_pos = new
334 self.backup_ref_node()
336 self.update()
338 return 1
340 def backup_ref_node(self):
341 self.ref_pos = list(self.ref_pos)
342 # Walk up the parents until we get a ref node above the start of the screen
343 # (redraw will come back down)
344 while self.ref_pos[1] > 0:
345 src = self.ref_node
347 if self.ref_node.previousSibling:
348 self.ref_node = self.ref_node.previousSibling
349 elif self.ref_node.parentNode:
350 self.ref_node = self.ref_node.parentNode
351 else:
352 break
354 # Walk from the parent node to find how far it is to this node...
355 for node, bbox, draw_fn in self.walk_tree(self.ref_node, (0, 0)):
356 if node is src: break
357 else:
358 assert 0
360 self.ref_pos[0] -= bbox[0]
361 self.ref_pos[1] -= bbox[1]
363 #print "(start from %s at (%d,%d))" % (self.ref_node, self.ref_pos[0], self.ref_pos[1])
365 if self.ref_pos[1] > 10:
366 self.ref_pos[1] = 10
367 elif self.ref_pos[1] < -100:
368 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
369 if bbox[3] > 10: break # Something is visible
370 else:
371 self.ref_pos[1] = -100
373 def bg_event(self, widget, event):
374 if event.type == g.gdk.BUTTON_PRESS and event.button == 3:
375 self.show_menu(event)
376 elif event.type == g.gdk.BUTTON_PRESS or event.type == g.gdk._2BUTTON_PRESS:
377 self.do_update_now()
378 node, attr_parent = self.xy_to_node(event.x, event.y)
379 if event.button == 1:
380 if node:
381 if attr_parent:
382 self.attrib_clicked(attr_parent, node, event)
383 else:
384 self.node_clicked(node, event)
385 elif event.button == 2:
386 assert self.pan_timeout is None
387 self.pan_start = (event.x, event.y)
388 self.pan_timeout = g.timeout_add(100, self.pan)
389 elif event.type == g.gdk.BUTTON_RELEASE and event.button == 2:
390 assert self.pan_timeout is not None
391 g.timeout_remove(self.pan_timeout)
392 self.pan_timeout = None
393 else:
394 return 0
395 return 1
397 def marked_changed(self, nodes):
398 "nodes is a list of nodes to be rechecked."
399 self.update_all()
401 def options_changed(self):
402 if default_font.has_changed:
403 #self.modify_font(pango.FontDescription(default_font.value))
404 self.update_all()
406 def scroll_to(self, adj):
407 n = int(adj.value)
408 for node in self.quick_walk():
409 n -= 1
410 if n < 0:
411 break
412 if self.ref_node == node:
413 return
414 self.ref_node = node
415 x = 0
416 while node.parentNode:
417 x += 16
418 node = node.parentNode
419 self.ref_pos = (x, 0)
420 self.update_all()