Better colour-scheme.
[dom-editor.git] / Dome / Display2.py
blob33e25d39e3bdb406f638f8720b3ec9855ac0c2c2
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 drag_cursor = g.gdk.Cursor(g.gdk.HAND1)
14 def calc_node(display, node, pos):
15 attribs = []
16 if node.nodeType == Node.TEXT_NODE:
17 text = node.nodeValue.strip()
18 elif node.nodeType == Node.ELEMENT_NODE:
19 if node.namespaceURI:
20 text = display.view.model.namespaces.prefix.get(node.namespaceURI, 'ERROR') + \
21 ':' + node.localName
22 else:
23 text = node.localName
24 elif node.nodeType == Node.ATTRIBUTE_NODE:
25 if node.namespaceURI:
26 text = display.view.model.namespaces.prefix.get(node.namespaceURI, 'ERROR') + \
27 ':' + node.localName
28 else:
29 text = node.localName
30 text = ' %s=%s' % (unicode(text), unicode(node.value))
31 elif node.nodeType == Node.COMMENT_NODE:
32 text = node.nodeValue.strip()
33 elif node.nodeType == Node.DOCUMENT_NODE:
34 chroots = len(display.view.chroots)
35 if chroots > 1:
36 text = "subtree (%d levels)" % chroots
37 elif chroots == 1:
38 text = 'subtree (one level)'
39 else:
40 text = display.view.model.uri
41 elif node.nodeName:
42 text = node.nodeName
43 elif node.nodeValue:
44 text = '<noname>' + node.nodeValue
45 else:
46 text = '<unknown>'
48 # PyGtk leaks PangoLayouts, so just reuse a single one
49 layout = display.surface_layout
50 layout.set_text(text)
51 width, height = layout.get_pixel_size()
52 x, y = map(int, pos)
54 text_x = x
55 if node.nodeType != Node.ATTRIBUTE_NODE:
56 text_x += 12
58 def draw_fn():
59 surface = display.pm
60 style = display.surface.style # Different surface ;-)
61 fg = style.fg_gc
62 bg = style.bg_gc
64 if node in display.selection:
65 state = g.STATE_SELECTED
66 else:
67 state = g.STATE_NORMAL
69 if node.nodeType != Node.ATTRIBUTE_NODE:
70 if node.nodeType == Node.ELEMENT_NODE:
71 surface.draw_rectangle(style.fg_gc[state], False, x, y, 7, height - 2)
72 surface.draw_rectangle(style.bg_gc[state], True, x + 1, y + 1, 6, height - 3)
73 elif node.nodeType == Node.DOCUMENT_NODE:
74 surface.draw_arc(style.fg_gc[state], False, x, y, 7, height - 2, 0, 64 * 360)
75 else:
76 # Text, etc
77 surface.draw_rectangle(style.text_gc[state], False, x, y, 7, height - 2)
78 surface.draw_rectangle(style.base_gc[state], True, x + 1, y + 1, 6, height - 3)
80 if node in display.view.model.hidden:
81 surface.draw_layout(fg[g.STATE_PRELIGHT], text_x + width + 2, y,
82 display.create_pango_layout('(%s)' % display.view.model.hidden[node]))
84 if node in display.selection:
85 surface.draw_rectangle(bg[g.STATE_SELECTED], True,
86 text_x, y, width - 1, height - 1)
87 surface.draw_layout(fg[g.STATE_SELECTED], text_x, y, layout)
88 else:
89 if node.nodeType == Node.TEXT_NODE:
90 gc = style.text_gc[g.STATE_NORMAL]
91 elif node.nodeType == Node.ATTRIBUTE_NODE:
92 gc = style.fg_gc[g.STATE_INSENSITIVE]
93 elif node.nodeType == Node.COMMENT_NODE:
94 gc = style.text_gc[g.STATE_INSENSITIVE]
95 else:
96 gc = style.fg_gc[g.STATE_NORMAL]
97 surface.draw_layout(gc, text_x, y, layout)
99 if node in display.view.marked:
100 surface.draw_rectangle(style.text_gc[g.STATE_PRELIGHT], False,
101 x - 1, y - 1, width + (text_x - x), height)
103 bbox = (x, y, text_x + width, y + height)
104 return bbox, draw_fn
106 class Display(g.HBox):
107 visible = 1 # Always visible
109 def __init__(self, window, view):
110 g.HBox.__init__(self, False, 0)
112 self.surface = g.EventBox()
113 self.surface_layout = self.surface.create_pango_layout('')
114 self.pack_start(self.surface, True, True, 0)
115 self.surface.show()
116 self.surface.set_app_paintable(True)
117 self.surface.set_double_buffered(False)
118 self.update_timeout = 0
119 self.cached_nodes = None
121 self.scroll_adj = g.Adjustment(lower = 0, upper = 100, step_incr = 1)
122 self.scroll_adj.connect('value-changed', self.scroll_to)
123 scale = g.VScrollbar(self.scroll_adj)
124 scale.unset_flags(g.CAN_FOCUS)
125 #scale.set_draw_value(False)
126 self.pack_start(scale, False, True, 0)
128 self.view = None
129 self.parent_window = window
130 self.pm = None
132 s = self.surface.get_style().copy()
133 s.bg[g.STATE_NORMAL] = g.gdk.color_parse('old lace')
134 s.text[g.STATE_NORMAL] = g.gdk.color_parse('blue')
135 s.text[g.STATE_PRELIGHT] = g.gdk.color_parse('orange') # Mark
136 s.text[g.STATE_INSENSITIVE] = g.gdk.color_parse('dark green')# Comment
137 s.fg[g.STATE_PRELIGHT] = g.gdk.color_parse('red') # Hidden
138 self.surface.set_style(s)
140 self.signals = [self.connect('destroy', self.destroyed)]
141 self.surface.connect('button-press-event', self.bg_event)
142 self.surface.connect('motion-notify-event', self.bg_motion)
143 self.surface.connect('button-release-event', self.bg_event)
145 # Display is relative to this node, which is the highest
146 # displayed node (possibly off the top of the screen)
147 self.ref_node = view.root
148 self.ref_pos = (0, 0)
150 self.drag_info = None
152 self.last_alloc = None
153 self.surface.connect('size-allocate', lambda w, a: self.size_allocate(a))
154 self.surface.connect('size-request', lambda w, r: self.size_request(r))
155 self.surface.connect('expose-event', lambda w, e: 1)
157 self.pan_timeout = None
158 self.h_limits = (0, 0)
159 self.set_view(view)
161 def destroyed(self, widget):
162 self.view.remove_display(self)
163 for s in self.signals:
164 self.disconnect(s)
165 if self.update_timeout:
166 g.timeout_remove(self.update_timeout)
167 self.update_timeout = 0
169 #del self.selection
170 del self.view
171 del self.parent_window
172 del self.ref_node
173 del self.surface_layout
174 del self.surface
175 del self.scroll_adj
176 del self.drawn
177 del self.pm
179 def size_allocate(self, alloc):
180 new = (alloc.width, alloc.height)
181 if self.last_alloc == new:
182 return
183 self.last_alloc = new
184 assert self.window
185 #print "Alloc", alloc.width, alloc.height
186 pm = g.gdk.Pixmap(self.surface.window, alloc.width, alloc.height, -1)
187 self.surface.window.set_back_pixmap(pm, False)
188 self.pm = pm
190 if self.update_timeout:
191 g.timeout_remove(self.update_timeout)
192 self.update_timeout = 0
193 self.update()
195 def update(self):
196 # Must be called either:
197 # - With no update_timeout running, or
198 # - From the timeout callback
200 self.update_timeout = 0
202 if not self.pm: return 0
203 #print "update"
205 self.pm.draw_rectangle(self.surface.style.bg_gc[g.STATE_NORMAL], True,
206 0, 0, self.last_alloc[0], self.last_alloc[1])
208 self.drawn = {} # xmlNode -> ((x1, y1, y2, y2), attrib_parent)
210 n = self.ref_node
211 p = self.view.root.parentNode
212 while n is not p:
213 n = n.parentNode
214 if not n:
215 print "(lost root)"
216 self.ref_node = self.view.root
217 self.ref_pos = (0, 0)
218 break
220 if self.view.current_attrib:
221 self.selection = {self.view.current_attrib: None}
222 else:
223 self.selection = {}
224 for n in self.view.current_nodes:
225 self.selection[n] = None
227 pos = list(self.ref_pos)
228 self.h_limits = (self.ref_pos[0], self.ref_pos[0]) # Left, Right
229 node = self.ref_node
230 attr_parent = None
231 drawn = 0
232 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
233 if bbox[1] > self.last_alloc[1]: break # Off-screen
234 if bbox[1] > -self.last_alloc[1]:
235 draw_fn()
236 drawn += 1
237 else:
238 pass#print 'Warning: Ref node way off:', bbox[1]
239 if node.nodeType == Node.ATTRIBUTE_NODE:
240 self.drawn[node] = (bbox, attr_parent)
241 else:
242 attr_parent = node
243 self.drawn[node] = (bbox, None)
245 if bbox[1] < 0 and node.nodeType != Node.ATTRIBUTE_NODE:
246 self.ref_node = node
247 self.ref_pos = bbox[:2]
248 self.h_limits = (min(self.h_limits[0], bbox[0]),
249 max(self.h_limits[1], bbox[2]))
250 else:
251 # Didn't have enough nodes to fill the screen
252 frac_filled = float(bbox[3]) / self.last_alloc[1]
253 if frac_filled:
254 drawn /= frac_filled
256 self.surface.window.clear()
258 # Update adjustment
259 self.ensure_cache()
260 try:
261 pos = self.cached_nodes.index(self.ref_node)
262 except:
263 pos = 0
264 print "Missing ref node!!"
265 self.scroll_adj.value = float(pos)
266 self.scroll_adj.upper = float(len(self.cached_nodes) + drawn)
268 self.scroll_adj.page_size = float(drawn)
269 self.scroll_adj.page_increment = float(drawn)
271 return 0
273 def ensure_cache(self):
274 "Find all the nodes in the document, in document order. Not attributes."
275 if self.cached_nodes is not None:
276 return
277 nodes = [self.view.root.parentNode]
278 node = self.view.root
279 while node:
280 nodes.append(node)
281 if node.childNodes:
282 node = node.childNodes[0]
283 else:
284 while not node.nextSibling:
285 node = node.parentNode
286 if not node:
287 self.cached_nodes = nodes
288 return
289 node = node.nextSibling
290 self.cached_nodes = nodes
292 def walk_tree(self, node, pos):
293 """Yield this (node, bbox), and all following ones in document order."""
294 pos = list(pos)
295 hidden = self.view.model.hidden
296 while node:
297 bbox, draw_fn = calc_node(self, node, pos)
298 yield (node, bbox, draw_fn)
300 if node.nodeType == Node.ELEMENT_NODE:
301 if node not in hidden:
302 apos = [bbox[2] + 4, bbox[1]]
303 for key in node.attributes:
304 a = node.attributes[key]
305 if a.namespaceURI == XMLNS_NAMESPACE:
306 continue
307 abbox, draw_fn = calc_node(self, a, apos)
308 apos[0] = abbox[2] + 4
309 yield (a, abbox, draw_fn)
311 pos[1] = bbox[3] + 2
312 if node.childNodes and node not in hidden:
313 node = node.childNodes[0]
314 pos[0] += 16
315 else:
316 while not node.nextSibling:
317 node = node.parentNode
318 if not node: return
319 pos[0] -= 16
320 node = node.nextSibling
322 def size_request(self, req):
323 req.width = 4
324 req.height = 4
326 def do_update_now(self):
327 # Update now, if we need to
328 if self.update_timeout:
329 g.timeout_remove(self.update_timeout)
330 self.update_timeout = 0
331 self.update()
333 def update_all(self, node = None):
334 self.cached_nodes = None
336 if self.update_timeout:
337 return # Going to update anyway...
339 if self.view.running():
340 self.update_timeout = g.timeout_add(2000, self.update)
341 else:
342 self.update_timeout = g.timeout_add(10, self.update)
344 def move_from(self, old = []):
345 if not self.pm: return
346 if self.view.current_nodes:
347 selection = {}
348 for n in self.view.current_nodes:
349 selection[n] = None
350 shown = False
351 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
352 if bbox[1] > self.last_alloc[1]: break # Off-screen
353 if bbox[3] > 0 and node in selection:
354 shown = True
355 break # A selected node is shown
356 if not shown:
357 #print "(selected nodes not shown)"
358 self.ref_node = self.view.current_nodes[0]
359 self.ref_pos = (40, self.last_alloc[1] / 2)
360 self.backup_ref_node()
361 self.update_all()
363 def set_view(self, view):
364 if self.view:
365 self.view.remove_display(self)
366 self.view = view
367 self.view.add_display(self)
368 self.update_all()
370 def show_menu(self, bev):
371 pass
373 def node_clicked(self, node, event):
374 pass
376 def xy_to_node(self, x, y):
377 "Return the node at this point and, if it's an attribute, its parent."
378 for (n, ((x1, y1, x2, y2), attrib_parent)) in self.drawn.iteritems():
379 if x >= x1 and x <= x2 and y >= y1 and y <= y2:
380 return n, attrib_parent
381 return None, None
383 def pan(self):
384 def scale(x):
385 val = (float(abs(x)) ** 1.4)
386 if x < 0:
387 return -val
388 else:
389 return val
390 def chop(x):
391 if x > 10: return x - 10
392 if x < -10: return x + 10
393 return 0
394 x, y, mask = self.surface.window.get_pointer()
395 sx, sy = self.pan_start
396 dx, dy = scale(chop(x - sx)) / 20, scale(chop(y - sy))
397 dx = max(dx, 10 - self.h_limits[1])
398 dx = min(dx, self.last_alloc[0] - 10 - self.h_limits[0])
399 new = [self.ref_pos[0] + dx, self.ref_pos[1] + dy]
401 if new == self.ref_pos:
402 return 1
404 self.ref_pos = new
406 self.backup_ref_node()
408 if self.update_timeout:
409 g.timeout_remove(self.update_timeout)
410 self.update_timeout = 0
411 self.update()
413 return 1
415 def backup_ref_node(self):
416 self.ref_pos = list(self.ref_pos)
417 # Walk up the parents until we get a ref node above the start of the screen
418 # (redraw will come back down)
419 while self.ref_pos[1] > 0:
420 src = self.ref_node
422 if self.ref_node.previousSibling:
423 self.ref_node = self.ref_node.previousSibling
424 elif self.ref_node.parentNode:
425 self.ref_node = self.ref_node.parentNode
426 else:
427 break
429 # Walk from the parent node to find how far it is to this node...
430 for node, bbox, draw_fn in self.walk_tree(self.ref_node, (0, 0)):
431 if node is src: break
432 else:
433 assert 0
435 self.ref_pos[0] -= bbox[0]
436 self.ref_pos[1] -= bbox[1]
438 #print "(start from %s at (%d,%d))" % (self.ref_node, self.ref_pos[0], self.ref_pos[1])
440 if self.ref_pos[1] > 10:
441 self.ref_pos[1] = 10
442 elif self.ref_pos[1] < -100:
443 for node, bbox, draw_fn in self.walk_tree(self.ref_node, self.ref_pos):
444 if bbox[3] > 10: break # Something is visible
445 else:
446 self.ref_pos[1] = -100
448 def bg_motion(self, widget, event):
449 if not self.drag_info:
450 return
451 node, attr_parent, x, y, in_progress = self.drag_info
452 if not in_progress:
453 if abs(event.x - x) > 5 or abs(event.y - y) > 5:
454 self.drag_info = (node, attr_parent, event.x, event.y, True)
455 self.window.set_cursor(drag_cursor)
456 else:
457 return False
458 return False
460 def do_drag(self, src, dst):
461 pass
463 def bg_event(self, widget, event):
464 if event.type == g.gdk.BUTTON_PRESS and event.button == 3:
465 self.show_menu(event)
466 elif event.type == g.gdk.BUTTON_PRESS or event.type == g.gdk._2BUTTON_PRESS:
467 self.do_update_now()
468 node, attr_parent = self.xy_to_node(event.x, event.y)
469 if event.button == 1:
470 self.drag_info = (node, attr_parent, event.x, event.y, False)
471 if node:
472 if attr_parent:
473 self.attrib_clicked(attr_parent, node, event)
474 else:
475 self.node_clicked(node, event)
476 elif event.button == 2:
477 assert self.pan_timeout is None
478 self.pan_start = (event.x, event.y)
479 self.pan_timeout = g.timeout_add(100, self.pan)
480 elif event.type == g.gdk.BUTTON_RELEASE:
481 if event.button == 2:
482 assert self.pan_timeout is not None
483 g.timeout_remove(self.pan_timeout)
484 self.pan_timeout = None
485 elif event.button == 1 and self.drag_info:
486 src_node, src_attr_parent, x, y, in_progress = self.drag_info
487 if not in_progress:
488 return True
489 dst_node, dst_attr_parent = self.xy_to_node(event.x, event.y)
490 self.window.set_cursor(None)
491 if not dst_node or (dst_node == src_node and dst_attr_parent == src_attr_parent):
492 return True
493 self.do_drag((src_node, src_attr_parent), (dst_node, dst_attr_parent))
494 else:
495 return False
496 return True
498 def marked_changed(self, nodes):
499 "nodes is a list of nodes to be rechecked."
500 self.update_all()
502 def options_changed(self):
503 if default_font.has_changed:
504 #self.modify_font(pango.FontDescription(default_font.value))
505 self.update_all()
507 def scroll_to(self, adj):
508 n = int(adj.value)
509 self.ensure_cache()
510 try:
511 node = self.cached_nodes[n]
512 except:
513 node = self.cached_nodes[-1]
514 if self.ref_node == node:
515 return
516 self.ref_node = node
517 x = 0
518 while node.parentNode:
519 x += 16
520 node = node.parentNode
521 self.ref_pos = (x, 0)
522 self.update_all()
524 def set_status(self, message):
525 self.parent_window.set_status(message)