Don't draw far-away ref nodes.
[dom-editor.git] / Dome / Display2.py
blob70accc4b37b21f554cac1c36d60e83079c2ea870
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
94 self.cached_nodes = None
96 self.scroll_adj = g.Adjustment(lower = 0, upper = 100, step_incr = 1)
97 self.scroll_adj.connect('value-changed', self.scroll_to)
98 scale = g.VScale(self.scroll_adj)
99 scale.unset_flags(g.CAN_FOCUS)
100 scale.set_draw_value(False)
101 self.pack_start(scale, False, True, 0)
103 self.view = None
104 self.parent_window = window
105 self.pm = None
107 s = self.surface.get_style().copy()
108 s.bg[g.STATE_NORMAL] = g.gdk.color_parse('old lace')
109 s.text[g.STATE_NORMAL] = g.gdk.color_parse('blue')
110 s.text[g.STATE_PRELIGHT] = g.gdk.color_parse('orange') # Mark
111 s.text[g.STATE_INSENSITIVE] = g.gdk.color_parse('dark green')# Comment
112 s.fg[g.STATE_PRELIGHT] = g.gdk.color_parse('red') # Hidden
113 self.surface.set_style(s)
115 self.signals = [self.connect('destroy', self.destroyed)]
116 self.surface.connect('button-press-event', self.bg_event)
117 self.surface.connect('button-release-event', self.bg_event)
119 # Display is relative to this node, which is the highest
120 # displayed node (possibly off the top of the screen)
121 self.ref_node = view.root
122 self.ref_pos = (0, 0)
124 self.last_alloc = None
125 self.surface.connect('size-allocate', lambda w, a: self.size_allocate(a))
126 self.surface.connect('size-request', lambda w, r: self.size_request(r))
127 self.surface.connect('expose-event', lambda w, e: 1)
129 self.pan_timeout = None
130 self.h_limits = (0, 0)
131 self.set_view(view)
133 def destroyed(self, widget):
134 self.view.remove_display(self)
135 for s in self.signals:
136 self.disconnect(s)
137 if self.update_timeout:
138 g.timeout_remove(self.update_timeout)
139 self.update_timeout = 0
141 #del self.selection
142 del self.view
143 del self.parent_window
144 del self.ref_node
145 del self.surface_layout
146 del self.surface
147 del self.scroll_adj
148 del self.drawn
149 del self.pm
151 def size_allocate(self, alloc):
152 new = (alloc.width, alloc.height)
153 if self.last_alloc == new:
154 return
155 self.last_alloc = new
156 assert self.window
157 #print "Alloc", alloc.width, alloc.height
158 pm = g.gdk.Pixmap(self.surface.window, alloc.width, alloc.height, -1)
159 self.surface.window.set_back_pixmap(pm, False)
160 self.pm = pm
162 if self.update_timeout:
163 g.timeout_remove(self.update_timeout)
164 self.update_timeout = 0
165 self.update()
167 def update(self):
168 # Must be called either:
169 # - With no update_timeout running, or
170 # - From the timeout callback
172 self.update_timeout = 0
174 if not self.pm: return 0
175 #print "update"
177 self.pm.draw_rectangle(self.surface.style.bg_gc[g.STATE_NORMAL], True,
178 0, 0, self.last_alloc[0], self.last_alloc[1])
180 self.drawn = {} # xmlNode -> ((x1, y1, y2, y2), attrib_parent)
182 n = self.ref_node
183 p = self.view.root.parentNode
184 while n is not p:
185 n = n.parentNode
186 if not n:
187 print "(lost root)"
188 self.ref_node = self.view.root
189 self.ref_pos = (0, 0)
190 break
192 if self.view.current_attrib:
193 self.selection = {self.view.current_attrib: None}
194 else:
195 self.selection = {}
196 for n in self.view.current_nodes:
197 self.selection[n] = None
199 pos = list(self.ref_pos)
200 self.h_limits = (self.ref_pos[0], self.ref_pos[0]) # Left, Right
201 node = self.ref_node
202 attr_parent = None
203 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
204 if bbox[1] > self.last_alloc[1]: break # Off-screen
205 if bbox[1] > -self.last_alloc[1]:
206 draw_fn()
207 else:
208 print 'Warning: Ref node way off:', bbox[1]
209 if node.nodeType == Node.ATTRIBUTE_NODE:
210 self.drawn[node] = (bbox, attr_parent)
211 else:
212 attr_parent = node
213 self.drawn[node] = (bbox, None)
215 if bbox[1] < 0 and node.nodeType != Node.ATTRIBUTE_NODE:
216 self.ref_node = node
217 self.ref_pos = bbox[:2]
218 self.h_limits = (min(self.h_limits[0], bbox[0]),
219 max(self.h_limits[1], bbox[2]))
221 self.surface.window.clear()
223 # Update adjustment
224 self.ensure_cache()
225 try:
226 pos = self.cached_nodes.index(self.ref_node)
227 except:
228 pos = 0
229 print "Missing ref node!!"
230 self.scroll_adj.value = float(pos)
231 self.scroll_adj.upper = float(len(self.cached_nodes))
233 return 0
235 def ensure_cache(self):
236 "Find all the nodes in the document, in document order. Not attributes."
237 if self.cached_nodes is not None:
238 return
239 nodes = [self.view.root.parentNode]
240 node = self.view.root
241 while node:
242 nodes.append(node)
243 if node.childNodes:
244 node = node.childNodes[0]
245 else:
246 while not node.nextSibling:
247 node = node.parentNode
248 if not node:
249 self.cached_nodes = nodes
250 return
251 node = node.nextSibling
252 self.cached_nodes = nodes
254 def walk_tree(self, node, pos):
255 """Yield this (node, bbox), and all following ones in document order."""
256 pos = list(pos)
257 while node:
258 bbox, draw_fn = calc_node(self, node, pos)
259 yield (node, bbox, draw_fn)
261 hidden = False
262 if node.nodeType == Node.ELEMENT_NODE:
263 hidden = node.hasAttributeNS(None, 'hidden')
264 if not hidden:
265 apos = [bbox[2] + 4, bbox[1]]
266 for key in node.attributes:
267 a = node.attributes[key]
268 abbox, draw_fn = calc_node(self, a, apos)
269 apos[0] = abbox[2] + 4
270 yield (a, abbox, draw_fn)
272 pos[1] = bbox[3] + 2
273 if node.childNodes and not hidden:
274 node = node.childNodes[0]
275 pos[0] += 16
276 else:
277 while not node.nextSibling:
278 node = node.parentNode
279 if not node: return
280 pos[0] -= 16
281 node = node.nextSibling
283 def size_request(self, req):
284 req.width = 4
285 req.height = 4
287 def do_update_now(self):
288 # Update now, if we need to
289 if self.update_timeout:
290 g.timeout_remove(self.update_timeout)
291 self.update_timeout = 0
292 self.update()
294 def update_all(self, node = None):
295 self.cached_nodes = None
297 if self.update_timeout:
298 return # Going to update anyway...
300 if self.view.running():
301 self.update_timeout = g.timeout_add(2000, self.update)
302 else:
303 self.update_timeout = g.timeout_add(10, self.update)
305 def move_from(self, old = []):
306 if not self.pm: return
307 if self.view.current_nodes:
308 selection = {}
309 for n in self.view.current_nodes:
310 selection[n] = None
311 shown = False
312 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
313 if bbox[1] > self.last_alloc[1]: break # Off-screen
314 if bbox[3] > 0 and node in selection:
315 shown = True
316 break # A selected node is shown
317 if not shown:
318 print "(selected nodes not shown)"
319 self.ref_node = self.view.current_nodes[0]
320 self.ref_pos = (40, self.last_alloc[1] / 2)
321 self.backup_ref_node()
322 self.update_all()
324 def set_view(self, view):
325 if self.view:
326 self.view.remove_display(self)
327 self.view = view
328 self.view.add_display(self)
329 self.update_all()
331 def show_menu(self, bev):
332 pass
334 def node_clicked(self, node, event):
335 pass
337 def xy_to_node(self, x, y):
338 "Return the node at this point and, if it's an attribute, its parent."
339 for (n, ((x1, y1, x2, y2), attrib_parent)) in self.drawn.iteritems():
340 if x >= x1 and x <= x2 and y >= y1 and y <= y2:
341 return n, attrib_parent
342 return None, None
344 def pan(self):
345 def scale(x):
346 val = (float(abs(x)) ** 1.4)
347 if x < 0:
348 return -val
349 else:
350 return val
351 def chop(x):
352 if x > 10: return x - 10
353 if x < -10: return x + 10
354 return 0
355 x, y, mask = self.surface.window.get_pointer()
356 sx, sy = self.pan_start
357 dx, dy = scale(chop(x - sx)) / 20, scale(chop(y - sy))
358 dx = max(dx, 10 - self.h_limits[1])
359 dx = min(dx, self.last_alloc[0] - 10 - self.h_limits[0])
360 new = [self.ref_pos[0] + dx, self.ref_pos[1] + dy]
362 if new == self.ref_pos:
363 return 1
365 self.ref_pos = new
367 self.backup_ref_node()
369 if self.update_timeout:
370 g.timeout_remove(self.update_timeout)
371 self.update_timeout = 0
372 self.update()
374 return 1
376 def backup_ref_node(self):
377 self.ref_pos = list(self.ref_pos)
378 # Walk up the parents until we get a ref node above the start of the screen
379 # (redraw will come back down)
380 while self.ref_pos[1] > 0:
381 src = self.ref_node
383 if self.ref_node.previousSibling:
384 self.ref_node = self.ref_node.previousSibling
385 elif self.ref_node.parentNode:
386 self.ref_node = self.ref_node.parentNode
387 else:
388 break
390 # Walk from the parent node to find how far it is to this node...
391 for node, bbox, draw_fn in self.walk_tree(self.ref_node, (0, 0)):
392 if node is src: break
393 else:
394 assert 0
396 self.ref_pos[0] -= bbox[0]
397 self.ref_pos[1] -= bbox[1]
399 #print "(start from %s at (%d,%d))" % (self.ref_node, self.ref_pos[0], self.ref_pos[1])
401 if self.ref_pos[1] > 10:
402 self.ref_pos[1] = 10
403 elif self.ref_pos[1] < -100:
404 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
405 if bbox[3] > 10: break # Something is visible
406 else:
407 self.ref_pos[1] = -100
409 def bg_event(self, widget, event):
410 if event.type == g.gdk.BUTTON_PRESS and event.button == 3:
411 self.show_menu(event)
412 elif event.type == g.gdk.BUTTON_PRESS or event.type == g.gdk._2BUTTON_PRESS:
413 self.do_update_now()
414 node, attr_parent = self.xy_to_node(event.x, event.y)
415 if event.button == 1:
416 if node:
417 if attr_parent:
418 self.attrib_clicked(attr_parent, node, event)
419 else:
420 self.node_clicked(node, event)
421 elif event.button == 2:
422 assert self.pan_timeout is None
423 self.pan_start = (event.x, event.y)
424 self.pan_timeout = g.timeout_add(100, self.pan)
425 elif event.type == g.gdk.BUTTON_RELEASE and event.button == 2:
426 assert self.pan_timeout is not None
427 g.timeout_remove(self.pan_timeout)
428 self.pan_timeout = None
429 else:
430 return 0
431 return 1
433 def marked_changed(self, nodes):
434 "nodes is a list of nodes to be rechecked."
435 self.update_all()
437 def options_changed(self):
438 if default_font.has_changed:
439 #self.modify_font(pango.FontDescription(default_font.value))
440 self.update_all()
442 def scroll_to(self, adj):
443 n = int(adj.value)
444 self.ensure_cache()
445 try:
446 node = self.cached_nodes[n]
447 except:
448 node = self.cached_nodes[-1]
449 if self.ref_node == node:
450 return
451 self.ref_node = node
452 x = 0
453 while node.parentNode:
454 x += 16
455 node = node.parentNode
456 self.ref_pos = (x, 0)
457 self.update_all()