Toggle hidden with XPath!
[dom-editor.git] / Dome / Display2.py
blob89483de1cc6dce495bab9df5a5aedbf750f2609e
1 from __future__ import generators
3 import rox
4 from rox import g
5 from xml.dom import Node
6 import pango
7 from constants import XMLNS_NAMESPACE
9 import __main__
10 default_font = __main__.default_font
12 def calc_node(display, node, pos):
13 attribs = []
14 if node.nodeType == Node.TEXT_NODE:
15 text = node.nodeValue.strip()
16 elif node.nodeType == Node.ELEMENT_NODE:
17 if node.namespaceURI:
18 text = display.view.model.namespaces.prefix.get(node.namespaceURI, 'ERROR') + \
19 ':' + node.localName
20 else:
21 text = node.localName
22 elif node.nodeType == Node.ATTRIBUTE_NODE:
23 if node.namespaceURI:
24 text = display.view.model.namespaces.prefix.get(node.namespaceURI, 'ERROR') + \
25 ':' + node.localName
26 else:
27 text = node.localName
28 text = ' %s=%s' % (unicode(text), unicode(node.value))
29 elif node.nodeType == Node.COMMENT_NODE:
30 text = node.nodeValue.strip()
31 elif node.nodeName:
32 text = node.nodeName
33 elif node.nodeValue:
34 text = '<noname>' + node.nodeValue
35 else:
36 text = '<unknown>'
38 # PyGtk leaks PangoLayouts, so just reuse a single one
39 layout = display.surface_layout
40 layout.set_text(text)
41 width, height = layout.get_pixel_size()
42 x, y = pos
44 text_x = x
45 if node.nodeType != Node.ATTRIBUTE_NODE:
46 text_x += 12
48 def draw_fn():
49 surface = display.pm
50 style = display.surface.style # Different surface ;-)
51 fg = style.fg_gc
52 bg = style.bg_gc
54 if node.nodeType != Node.ATTRIBUTE_NODE:
55 marker = True
56 surface.draw_rectangle(fg[g.STATE_NORMAL], True,
57 x, y, 8, height - 1)
58 if node.nodeType == Node.ELEMENT_NODE and node.hasAttributeNS(None, 'hidden'):
59 message = node.attributes[(None, 'hidden')].value or 'hidden'
60 surface.draw_layout(fg[g.STATE_PRELIGHT], text_x + width + 2, y,
61 display.create_pango_layout('(%s)' % message))
62 else:
63 marker = False
65 if node in display.selection:
66 if marker:
67 surface.draw_rectangle(style.bg_gc[g.STATE_SELECTED], True,
68 x + 1, y + 1, 6, height - 3)
69 surface.draw_rectangle(bg[g.STATE_SELECTED], True,
70 text_x, y, width - 1, height - 1)
71 surface.draw_layout(fg[g.STATE_SELECTED], text_x, y, layout)
72 else:
73 if marker:
74 surface.draw_rectangle(style.white_gc, True, x + 1, y + 1, 6, height - 3)
75 if node.nodeType == Node.TEXT_NODE:
76 gc = style.text_gc[g.STATE_NORMAL]
77 elif node.nodeType == Node.ATTRIBUTE_NODE:
78 gc = style.fg_gc[g.STATE_INSENSITIVE]
79 elif node.nodeType == Node.COMMENT_NODE:
80 gc = style.text_gc[g.STATE_INSENSITIVE]
81 else:
82 gc = style.fg_gc[g.STATE_NORMAL]
83 surface.draw_layout(gc, text_x, y, layout)
85 if node in display.view.marked:
86 surface.draw_rectangle(style.text_gc[g.STATE_PRELIGHT], False,
87 x - 1, y - 1, width + (text_x - x), height)
89 bbox = (x, y, text_x + width, y + height)
90 return bbox, draw_fn
92 class Display(g.HBox):
93 visible = 1 # Always visible
95 def __init__(self, window, view):
96 g.HBox.__init__(self, False, 0)
98 self.surface = g.EventBox()
99 self.surface_layout = self.surface.create_pango_layout('')
100 self.pack_start(self.surface, True, True, 0)
101 self.surface.show()
102 self.surface.set_app_paintable(True)
103 self.surface.set_double_buffered(False)
104 self.update_timeout = 0
105 self.cached_nodes = None
107 self.scroll_adj = g.Adjustment(lower = 0, upper = 100, step_incr = 1)
108 self.scroll_adj.connect('value-changed', self.scroll_to)
109 scale = g.VScrollbar(self.scroll_adj)
110 scale.unset_flags(g.CAN_FOCUS)
111 #scale.set_draw_value(False)
112 self.pack_start(scale, False, True, 0)
114 self.view = None
115 self.parent_window = window
116 self.pm = None
118 s = self.surface.get_style().copy()
119 s.bg[g.STATE_NORMAL] = g.gdk.color_parse('old lace')
120 s.text[g.STATE_NORMAL] = g.gdk.color_parse('blue')
121 s.text[g.STATE_PRELIGHT] = g.gdk.color_parse('orange') # Mark
122 s.text[g.STATE_INSENSITIVE] = g.gdk.color_parse('dark green')# Comment
123 s.fg[g.STATE_PRELIGHT] = g.gdk.color_parse('red') # Hidden
124 self.surface.set_style(s)
126 self.signals = [self.connect('destroy', self.destroyed)]
127 self.surface.connect('button-press-event', self.bg_event)
128 self.surface.connect('button-release-event', self.bg_event)
130 # Display is relative to this node, which is the highest
131 # displayed node (possibly off the top of the screen)
132 self.ref_node = view.root
133 self.ref_pos = (0, 0)
135 self.last_alloc = None
136 self.surface.connect('size-allocate', lambda w, a: self.size_allocate(a))
137 self.surface.connect('size-request', lambda w, r: self.size_request(r))
138 self.surface.connect('expose-event', lambda w, e: 1)
140 self.pan_timeout = None
141 self.h_limits = (0, 0)
142 self.set_view(view)
144 def destroyed(self, widget):
145 self.view.remove_display(self)
146 for s in self.signals:
147 self.disconnect(s)
148 if self.update_timeout:
149 g.timeout_remove(self.update_timeout)
150 self.update_timeout = 0
152 #del self.selection
153 del self.view
154 del self.parent_window
155 del self.ref_node
156 del self.surface_layout
157 del self.surface
158 del self.scroll_adj
159 del self.drawn
160 del self.pm
162 def size_allocate(self, alloc):
163 new = (alloc.width, alloc.height)
164 if self.last_alloc == new:
165 return
166 self.last_alloc = new
167 assert self.window
168 #print "Alloc", alloc.width, alloc.height
169 pm = g.gdk.Pixmap(self.surface.window, alloc.width, alloc.height, -1)
170 self.surface.window.set_back_pixmap(pm, False)
171 self.pm = pm
173 if self.update_timeout:
174 g.timeout_remove(self.update_timeout)
175 self.update_timeout = 0
176 self.update()
178 def update(self):
179 # Must be called either:
180 # - With no update_timeout running, or
181 # - From the timeout callback
183 self.update_timeout = 0
185 if not self.pm: return 0
186 #print "update"
188 self.pm.draw_rectangle(self.surface.style.bg_gc[g.STATE_NORMAL], True,
189 0, 0, self.last_alloc[0], self.last_alloc[1])
191 self.drawn = {} # xmlNode -> ((x1, y1, y2, y2), attrib_parent)
193 n = self.ref_node
194 p = self.view.root.parentNode
195 while n is not p:
196 n = n.parentNode
197 if not n:
198 print "(lost root)"
199 self.ref_node = self.view.root
200 self.ref_pos = (0, 0)
201 break
203 if self.view.current_attrib:
204 self.selection = {self.view.current_attrib: None}
205 else:
206 self.selection = {}
207 for n in self.view.current_nodes:
208 self.selection[n] = None
210 pos = list(self.ref_pos)
211 self.h_limits = (self.ref_pos[0], self.ref_pos[0]) # Left, Right
212 node = self.ref_node
213 attr_parent = None
214 drawn = 0
215 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
216 if bbox[1] > self.last_alloc[1]: break # Off-screen
217 if bbox[1] > -self.last_alloc[1]:
218 draw_fn()
219 drawn += 1
220 else:
221 pass#print 'Warning: Ref node way off:', bbox[1]
222 if node.nodeType == Node.ATTRIBUTE_NODE:
223 self.drawn[node] = (bbox, attr_parent)
224 else:
225 attr_parent = node
226 self.drawn[node] = (bbox, None)
228 if bbox[1] < 0 and node.nodeType != Node.ATTRIBUTE_NODE:
229 self.ref_node = node
230 self.ref_pos = bbox[:2]
231 self.h_limits = (min(self.h_limits[0], bbox[0]),
232 max(self.h_limits[1], bbox[2]))
233 else:
234 # Didn't have enough nodes to fill the screen
235 frac_filled = float(bbox[3]) / self.last_alloc[1]
236 if frac_filled:
237 drawn /= frac_filled
239 self.surface.window.clear()
241 # Update adjustment
242 self.ensure_cache()
243 try:
244 pos = self.cached_nodes.index(self.ref_node)
245 except:
246 pos = 0
247 print "Missing ref node!!"
248 self.scroll_adj.value = float(pos)
249 self.scroll_adj.upper = float(len(self.cached_nodes) + drawn)
251 self.scroll_adj.page_size = float(drawn)
252 self.scroll_adj.page_increment = float(drawn)
254 return 0
256 def ensure_cache(self):
257 "Find all the nodes in the document, in document order. Not attributes."
258 if self.cached_nodes is not None:
259 return
260 nodes = [self.view.root.parentNode]
261 node = self.view.root
262 while node:
263 nodes.append(node)
264 if node.childNodes:
265 node = node.childNodes[0]
266 else:
267 while not node.nextSibling:
268 node = node.parentNode
269 if not node:
270 self.cached_nodes = nodes
271 return
272 node = node.nextSibling
273 self.cached_nodes = nodes
275 def walk_tree(self, node, pos):
276 """Yield this (node, bbox), and all following ones in document order."""
277 pos = list(pos)
278 while node:
279 bbox, draw_fn = calc_node(self, node, pos)
280 yield (node, bbox, draw_fn)
282 hidden = False
283 if node.nodeType == Node.ELEMENT_NODE:
284 hidden = node.hasAttributeNS(None, 'hidden')
285 if not hidden:
286 apos = [bbox[2] + 4, bbox[1]]
287 for key in node.attributes:
288 a = node.attributes[key]
289 if a.namespaceURI == XMLNS_NAMESPACE:
290 continue
291 abbox, draw_fn = calc_node(self, a, apos)
292 apos[0] = abbox[2] + 4
293 yield (a, abbox, draw_fn)
295 pos[1] = bbox[3] + 2
296 if node.childNodes and not hidden:
297 node = node.childNodes[0]
298 pos[0] += 16
299 else:
300 while not node.nextSibling:
301 node = node.parentNode
302 if not node: return
303 pos[0] -= 16
304 node = node.nextSibling
306 def size_request(self, req):
307 req.width = 4
308 req.height = 4
310 def do_update_now(self):
311 # Update now, if we need to
312 if self.update_timeout:
313 g.timeout_remove(self.update_timeout)
314 self.update_timeout = 0
315 self.update()
317 def update_all(self, node = None):
318 self.cached_nodes = None
320 if self.update_timeout:
321 return # Going to update anyway...
323 if self.view.running():
324 self.update_timeout = g.timeout_add(2000, self.update)
325 else:
326 self.update_timeout = g.timeout_add(10, self.update)
328 def move_from(self, old = []):
329 if not self.pm: return
330 if self.view.current_nodes:
331 selection = {}
332 for n in self.view.current_nodes:
333 selection[n] = None
334 shown = False
335 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
336 if bbox[1] > self.last_alloc[1]: break # Off-screen
337 if bbox[3] > 0 and node in selection:
338 shown = True
339 break # A selected node is shown
340 if not shown:
341 #print "(selected nodes not shown)"
342 self.ref_node = self.view.current_nodes[0]
343 self.ref_pos = (40, self.last_alloc[1] / 2)
344 self.backup_ref_node()
345 self.update_all()
347 def set_view(self, view):
348 if self.view:
349 self.view.remove_display(self)
350 self.view = view
351 self.view.add_display(self)
352 self.update_all()
354 def show_menu(self, bev):
355 pass
357 def node_clicked(self, node, event):
358 pass
360 def xy_to_node(self, x, y):
361 "Return the node at this point and, if it's an attribute, its parent."
362 for (n, ((x1, y1, x2, y2), attrib_parent)) in self.drawn.iteritems():
363 if x >= x1 and x <= x2 and y >= y1 and y <= y2:
364 return n, attrib_parent
365 return None, None
367 def pan(self):
368 def scale(x):
369 val = (float(abs(x)) ** 1.4)
370 if x < 0:
371 return -val
372 else:
373 return val
374 def chop(x):
375 if x > 10: return x - 10
376 if x < -10: return x + 10
377 return 0
378 x, y, mask = self.surface.window.get_pointer()
379 sx, sy = self.pan_start
380 dx, dy = scale(chop(x - sx)) / 20, scale(chop(y - sy))
381 dx = max(dx, 10 - self.h_limits[1])
382 dx = min(dx, self.last_alloc[0] - 10 - self.h_limits[0])
383 new = [self.ref_pos[0] + dx, self.ref_pos[1] + dy]
385 if new == self.ref_pos:
386 return 1
388 self.ref_pos = new
390 self.backup_ref_node()
392 if self.update_timeout:
393 g.timeout_remove(self.update_timeout)
394 self.update_timeout = 0
395 self.update()
397 return 1
399 def backup_ref_node(self):
400 self.ref_pos = list(self.ref_pos)
401 # Walk up the parents until we get a ref node above the start of the screen
402 # (redraw will come back down)
403 while self.ref_pos[1] > 0:
404 src = self.ref_node
406 if self.ref_node.previousSibling:
407 self.ref_node = self.ref_node.previousSibling
408 elif self.ref_node.parentNode:
409 self.ref_node = self.ref_node.parentNode
410 else:
411 break
413 # Walk from the parent node to find how far it is to this node...
414 for node, bbox, draw_fn in self.walk_tree(self.ref_node, (0, 0)):
415 if node is src: break
416 else:
417 assert 0
419 self.ref_pos[0] -= bbox[0]
420 self.ref_pos[1] -= bbox[1]
422 #print "(start from %s at (%d,%d))" % (self.ref_node, self.ref_pos[0], self.ref_pos[1])
424 if self.ref_pos[1] > 10:
425 self.ref_pos[1] = 10
426 elif self.ref_pos[1] < -100:
427 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
428 if bbox[3] > 10: break # Something is visible
429 else:
430 self.ref_pos[1] = -100
432 def bg_event(self, widget, event):
433 if event.type == g.gdk.BUTTON_PRESS and event.button == 3:
434 self.show_menu(event)
435 elif event.type == g.gdk.BUTTON_PRESS or event.type == g.gdk._2BUTTON_PRESS:
436 self.do_update_now()
437 node, attr_parent = self.xy_to_node(event.x, event.y)
438 if event.button == 1:
439 if node:
440 if attr_parent:
441 self.attrib_clicked(attr_parent, node, event)
442 else:
443 self.node_clicked(node, event)
444 elif event.button == 2:
445 assert self.pan_timeout is None
446 self.pan_start = (event.x, event.y)
447 self.pan_timeout = g.timeout_add(100, self.pan)
448 elif event.type == g.gdk.BUTTON_RELEASE and event.button == 2:
449 assert self.pan_timeout is not None
450 g.timeout_remove(self.pan_timeout)
451 self.pan_timeout = None
452 else:
453 return 0
454 return 1
456 def marked_changed(self, nodes):
457 "nodes is a list of nodes to be rechecked."
458 self.update_all()
460 def options_changed(self):
461 if default_font.has_changed:
462 #self.modify_font(pango.FontDescription(default_font.value))
463 self.update_all()
465 def scroll_to(self, adj):
466 n = int(adj.value)
467 self.ensure_cache()
468 try:
469 node = self.cached_nodes[n]
470 except:
471 node = self.cached_nodes[-1]
472 if self.ref_node == node:
473 return
474 self.ref_node = node
475 x = 0
476 while node.parentNode:
477 x += 16
478 node = node.parentNode
479 self.ref_pos = (x, 0)
480 self.update_all()
482 def set_status(self, message):
483 self.parent_window.set_status(message)