Added hover insertion indicator (need to
[dom-editor.git] / Dome / List.py
bloba0a21614e424ca6440948695780a94a50eb88ebb
1 from __future__ import generators
3 import rox
4 from rox import g, TRUE, FALSE, alert
6 from support import *
7 import string
8 from StringIO import StringIO
9 import math
10 import View
12 from rox.Menu import Menu
14 import __main__
15 mono = __main__.mono
17 prog_menu = Menu('programs', [
18 ('/Play', 'menu_play', '', ''),
19 ('/Map', 'menu_map', '', ''),
20 ('/View', 'menu_new_view', '', ''),
21 ('/', '', '', '<separator>'),
22 ('/New program', 'menu_new_prog', '', ''),
23 ('/Rename', 'menu_rename', '', ''),
24 ('/Delete', 'menu_delete', '', ''),
27 line_menu = Menu('line', [
28 ('/Set\/clear breakpoint', 'line_toggle_breakpoint', '', ''),
29 ('/Yank chain', 'line_yank_chain', '', ''),
30 ('/Remove link', 'line_del_chain', '', ''),
31 ('/Paste chain', 'line_paste_chain', '', ''),
32 ('/Add block', 'line_add_block', '', '')
35 block_menu = Menu('op', [
36 ('/Toggle Enter\/Leave', 'block_toggle_enter', '', ''),
37 ('/Toggle Foreach','block_toggle_foreach', '', ''),
38 ('/Toggle Restore Mark','block_toggle_restore', '', ''),
39 ('/Edit comment', 'block_edit_comment', '', ''),
40 ('/Swap next\/fail', 'op_swap_nf', '', ''),
41 ('/Remove node', 'op_del_node', '', '')
44 op_menu = Menu('op', [
45 ('/Edit node', 'op_edit', '', ''),
46 ('/Swap next\/fail', 'op_swap_nf', '', ''),
47 ('/Remove node', 'op_del_node', '', '')
50 from GetArg import GetArg
51 from Program import Program, load, Block
53 no_cursor = g.gdk.Cursor(g.gdk.TCROSS)
55 def trunc(text):
56 if len(text) < 28:
57 return text
58 return text[:26] + '...'
60 def connect(x1, y1, x2, y2):
61 """Chop 5 pixels off both ends of this line"""
62 gap = 5.0
63 dx = x2 - x1
64 dy = y2 - y1
65 l = math.hypot(dx, dy)
66 if l:
67 dx *= gap / l
68 dy *= gap / l
69 return (x1 + dx, y1 + dy, x2 - dx, y2 - dy)
71 DEFAULT_NEXT = (0, 25)
72 DEFAULT_FAIL = (20, 20)
74 expand_history = {} # Prog name -> expanded flag
76 def action_to_text(action):
77 text = action[0]
78 if text == 'Start': return ''
79 if text[:3] == 'do_':
80 text = text[3:]
81 text = string.capitalize(string.replace(text, '_', ' '))
82 if text == 'Global':
83 text = 'Select nodes'
85 if len(action) > 1:
86 if action[0] == 'do_search' or action[0] == 'xpath':
87 pat = str(action[1])
88 pat = string.replace(pat, 'following-sibling::', '>>')
89 pat = string.replace(pat, 'preceding-sibling::', '<<')
90 pat = string.replace(pat, 'child::', '')
91 pat = string.replace(pat, '[1]', '')
92 pat = string.replace(pat, 'text()[ext:match', '[')
93 details = ''
94 while len(pat) > 20:
95 i = string.rfind(pat[:20], '/')
96 if i == -1:
97 i = string.rfind(pat[:20], ':')
98 if i == -1:
99 i = 20
100 details = details + pat[:i + 1] + '\n'
101 pat = pat[i + 1:]
102 details = details + pat
103 elif action[0] == 'attribute':
104 details = trunc(str(action[2]))
105 elif action[0] == 'set_attrib':
106 details = trunc(str(action[1]))
107 elif action[0] == 'add_attrib':
108 details = trunc(str(action[2]))
109 elif action[0] == 'add_node':
110 details = trunc(action[2])
111 elif action[0] == 'subst':
112 details = action[1] + ' -> ' + action[2]
113 elif action[0] == 'play' or action[0] == 'map':
114 if len(action[1]) > 20:
115 details = '...' + str(action[1][-19:])
116 else:
117 details = str(action[1])
118 else:
119 if len(action) > 2:
120 details = `action[1:]`
121 else:
122 details = str(action[1])
123 if len(details) > 20:
124 details = trunc(`details`)
125 text = text + '\n' + details
126 return text
128 class List(g.VBox):
129 def __init__(self, view):
130 g.VBox.__init__(self)
132 def destroyed(widget):
133 #print "List destroy!!"
134 sel.disconnect(self.sel_changed_signal)
135 self.view.lists.remove(self)
136 self.view.model.root_program.watchers.remove(self)
137 self.connect('destroy', destroyed)
139 self.view = view
140 self.sub_windows = []
142 self.stack_frames = g.Label('')
143 self.pack_start(self.stack_frames, FALSE, TRUE, 0)
144 self.stack_frames.show()
145 self.update_stack(None)
147 pane = g.VPaned()
148 self.pack_start(pane, expand = 1, fill = 1)
150 swin = g.ScrolledWindow()
151 swin.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
152 pane.add1(swin)
153 self.prog_model = g.TreeStore(str, str)
154 tree = g.TreeView(self.prog_model)
155 tree.connect('button-press-event', self.button_press)
156 tree.unset_flags(g.CAN_FOCUS)
157 tree.set_headers_visible(FALSE)
158 self.tree = tree
160 cell = g.CellRendererText()
161 column = g.TreeViewColumn('Program', cell, text = 0)
162 tree.append_column(column)
164 sel = tree.get_selection()
165 # Doesn't get destroyed, so record signal number
166 self.sel_changed_signal = sel.connect('changed', self.change_prog)
168 self.chains = ChainDisplay(view)
169 self.prog_tree_changed()
170 v = g.Viewport()
171 v.add(tree)
172 swin.add(v)
173 v.set_shadow_type(g.SHADOW_NONE)
174 v.show_all()
176 swin = g.ScrolledWindow()
177 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
178 pane.add2(swin)
179 swin.add_with_viewport(self.chains)
180 swin.show_all()
182 pane.set_position(200)
184 sel.set_mode(g.SELECTION_BROWSE)
185 root_iter = self.prog_model.get_iter_first()
186 sel.select_iter(root_iter)
187 tree.expand_row(self.prog_model.get_path(root_iter), FALSE)
188 tree.show()
189 self.view.lists.append(self)
190 self.view.model.root_program.watchers.append(self)
192 def change_prog(self, sel):
193 selected = sel.get_selected()
194 if not selected:
195 return
196 model, iter = selected
197 if iter:
198 path = model.get_value(iter, 1)
199 self.chains.switch_to(self.view.name_to_prog(path))
200 else:
201 self.chains.switch_to(None)
203 def set_innermost_failure(self, op):
204 prog = op.get_program()
205 #print "list: set_innermost_failure:", prog
206 self.show_prog(prog)
208 def update_points(self):
209 self.chains.update_points()
210 for x in self.sub_windows:
211 x.update_points()
213 def program_changed(self, op):
214 pass
216 def prog_tree_changed(self):
217 self.prog_to_path = {}
218 self.prog_model.clear()
219 self.build_tree(self.view.model.root_program)
221 # Check for now deleted programs still being displayed
222 root = self.view.model.root_program
223 if self.chains and self.chains.prog and not self.chains.prog.parent:
224 self.chains.switch_to(None)
225 for x in self.sub_windows:
226 if x.disp.prog is not root and not x.disp.prog.parent:
227 x.destroy()
229 def build_tree(self, prog, iter = None):
230 child_iter = self.prog_model.append(iter)
231 self.prog_model.set(child_iter, 0, prog.name,
232 1, prog.get_path())
234 self.prog_to_path[prog] = self.prog_model.get_path(child_iter)
235 for p in prog.subprograms.values():
236 self.build_tree(p, child_iter)
238 def run_return(self, exit):
239 print "List execution finished:", exit
240 if exit != 'next':
241 #self.view.jump_to_innermost_failure()
242 def record():
243 if rox.confirm("Program failed - record a failure case?",
244 g.STOCK_NO, 'Record'):
245 self.view.record_at_point()
246 return False
247 g.idle_add(record)
249 def button_press(self, tree, event):
250 if event.button == 2 or event.button == 3:
251 ret = tree.get_path_at_pos(event.x, event.y)
252 if not ret:
253 return 1 # Click on blank area
254 path, col, cx, cy = ret
255 #print "Event on", path
256 iter = self.prog_model.get_iter(path)
257 path = self.prog_model.get_value(iter, 1)
258 if event.button == 3:
259 prog = self.view.name_to_prog(path)
260 self.show_menu(event, prog)
261 else:
262 self.view.run_new(self.run_return)
263 self.view.set_status("Running '%s'" % path)
264 if event.state & g.gdk.SHIFT_MASK:
265 self.view.may_record(['map', path])
266 else:
267 self.view.may_record(['play', path])
268 return 0
270 def menu_delete(self):
271 prog = self.prog_menu_prog
272 if not prog.parent:
273 rox.alert("Can't delete the root program!")
274 return
275 prog.parent.remove_sub(prog)
277 def menu_rename(self):
278 prog = self.prog_menu_prog
279 def rename(name, prog = prog):
280 prog.rename(name)
281 GetArg('Rename program', rename, ['Program name:'])
283 def menu_new_prog(self):
284 prog = self.prog_menu_prog
285 def create(name):
286 new = Program(name)
287 prog.add_sub(new)
288 GetArg('New program', create, ['Program name:'])
290 def menu_new_view(self):
291 prog = self.prog_menu_prog
292 cw = ChainWindow(self.view, prog)
293 cw.show()
294 self.sub_windows.append(cw)
295 def lost_cw(win):
296 self.sub_windows.remove(cw)
297 cw.connect('destroy', lost_cw)
299 def menu_map(self):
300 prog = self.prog_menu_prog
301 self.view.run_new(self.run_return)
302 self.view.set_status("Running '%s'" % prog.get_path())
303 self.view.may_record(['map', prog.get_path()])
305 def menu_play(self):
306 prog = self.prog_menu_prog
307 self.view.run_new(self.run_return)
308 self.view.set_status("Running '%s'" % prog.get_path())
309 self.view.may_record(['play', prog.get_path()])
311 def show_menu(self, event, prog):
312 self.prog_menu_prog = prog
313 prog_menu.popup(self, event)
315 def update_stack(self, op):
316 "The stack has changed - redraw 'op'"
317 if op and op.get_program() == self.chains.prog:
318 self.chains.update_all()
319 l = len(self.view.exec_stack) + len(self.view.foreach_stack)
320 if l == 0:
321 text = 'No stack'
322 elif l == 1:
323 text = '1 frame'
324 else:
325 text = '%d frames' % l
326 if self.view.chroots:
327 text += ' (%d enters)' % len(self.view.chroots)
328 self.stack_frames.set_text(text)
330 def show_prog(self, prog):
331 path = self.prog_to_path[prog]
332 partial = []
333 for p in path[:-1]:
334 partial.append(p)
335 self.tree.expand_row(tuple(partial), FALSE)
336 iter = self.prog_model.get_iter(path)
337 self.tree.get_selection().select_iter(iter)
339 class ChainDummy(g.TreeView):
340 def __init__(self, view, prog = None):
341 g.TreeView.__init__(self)
342 self.prog = prog
343 def switch_to(self, prog):
344 self.prog = prog
345 def update_points(self):
346 pass
348 class ChainNode:
349 "A visual object in the display."
350 def __init__(self, da, x, y):
351 self.x = x
352 self.y = y
353 self.da = da
355 def expose(self):
356 da = self.da
357 w = da.backing
358 w.draw_rectangle(da.style.black_gc, True, self.x, self.y, 10, 10)
360 def maybe_clicked(self, event):
361 return False
363 class ChainOp(ChainNode):
364 def __init__(self, da, op, x, y):
365 self.op = op
366 ChainNode.__init__(self, da, x, y)
368 self.build_leaf()
370 da.op_to_object[op] = self
372 if op.next and op.next.prev[0] == op:
373 self.next = da.create_op(op.next, x, y + self.height + 4)
374 else:
375 self.next = None
377 if op.fail and op.fail.prev[0] == op:
378 self.fail = da.create_op(op.fail, x + 100, y + self.height + 4)
379 else:
380 self.fail = None
382 def build_leaf(self):
383 text = str(action_to_text(self.op.action))
384 self.layout = self.da.create_pango_layout(text)
386 self.width, self.height = self.layout.get_pixel_size()
387 self.width += 12
388 self.height = max(self.height, 20)
390 def expose(self):
391 da = self.da
392 w = da.backing
393 op = self.op
395 w.draw_arc(da.style.white_gc, True, self.x, self.y, 10, 10, 0, 400 * 60)
396 w.draw_arc(da.style.black_gc, False, self.x, self.y, 10, 10, 0, 400 * 60)
397 w.draw_layout(da.style.black_gc, self.x + 12, self.y, self.layout)
399 self.draw_link(self.next, 5, 10, 'black')
400 self.draw_link(self.fail, 10, 10, 'red')
402 def draw_link(self, dest, dx, dy, colour):
403 if not dest: return
405 dest.expose()
406 da = self.da
407 pen = da.style.white_gc
408 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
409 da.backing.draw_line(pen, self.x + dx, self.y + dy, dest.x + 5, dest.y)
410 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
412 def nearby(self, x, y):
413 return x > self.x - 4 and x < self.x + self.width + 20 and \
414 y > self.y and y < self.y + self.height + 20
416 def maybe_clicked(self, event):
417 x = event.x - self.x
418 if x < 0: return False
419 y = event.y - self.y
420 if y < 0: return False
422 if x > self.width or y > self.height:
423 return False
425 if event.button == 1:
426 self.da.view.set_exec((self.op, 'next'))
427 else:
428 if x < 10 and y > 10:
429 self.da.show_menu(event, self.op, 'next')
430 else:
431 self.da.show_menu(event, self.op)
432 return True
434 def all_nodes(self):
435 yield self
436 if self.next:
437 for n in self.next.all_nodes(): yield n
438 if self.fail:
439 for n in self.fail.all_nodes(): yield n
442 class ChainBlock(ChainOp):
443 def __init__(self, da, block, x, y):
444 assert isinstance(block, Block)
445 ChainOp.__init__(self, da, block, x, y)
446 self.depth = 1
447 p = block.parent
448 while p and not isinstance(p, Program):
449 p = p.parent
450 self.depth += 1
452 def build_leaf(self):
453 x = self.x
454 y = self.y
456 if self.op.comment:
457 self.layout = self.da.create_pango_layout(self.op.comment.replace('\\n', '\n'))
458 self.width, height = self.layout.get_pixel_size()
459 y += height + 4
460 else:
461 self.layout = None
462 self.width = 40
464 self.margin = (4 + self.op.foreach * 6, 4 + (self.op.enter + self.op.restore) * 6)
465 self.width += self.margin[0]
467 self.start = self.da.create_op(self.op.start, x + self.margin[0], y + self.margin[1])
469 self.height = 20
471 for node in self.start.all_nodes():
472 self.width = max(self.width, node.x + node.width - self.x)
473 self.height = max(self.height, node.y + node.height - self.y)
475 self.width += 4
476 self.height += 4 + (self.op.enter + self.op.restore) * 6
478 def expose(self):
479 da = self.da
480 w = da.backing
481 w.draw_rectangle(da.style.black_gc, False, self.x, self.y, self.width, self.height)
482 pen = da.style.white_gc
483 width = self.width
484 x = self.x
485 y = self.y
487 d = 15 - min(self.depth, 7)
488 pen.set_rgb_fg_color(g.gdk.color_parse('#%x%x%x' % (d, d, d)))
489 w.draw_rectangle(pen, True, self.x + 1, self.y + 1, self.width - 1, self.height - 1)
491 op = self.op
492 if op.foreach:
493 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
494 w.draw_rectangle(pen, True, x + 1, y + 1, 6, self.height - 1)
495 x += 6
496 width -= 6
498 if op.enter:
499 pen.set_rgb_fg_color(g.gdk.color_parse('yellow'))
500 w.draw_rectangle(pen, True, x + 1, y + 1, width - 1, 6)
501 w.draw_rectangle(pen, True, x + 1, y + self.height - 6, width - 1, 6)
502 if op.restore:
503 pen.set_rgb_fg_color(g.gdk.color_parse('orange'))
504 margin = op.enter * 6
505 w.draw_rectangle(pen, True, x + 1, y + 1 + margin, width - 1, 6)
506 w.draw_rectangle(pen, True, x + 1, y + self.height - 6 - margin, width - 1, 6)
508 if self.layout:
509 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
510 w.draw_layout(pen, self.x + self.margin[0], self.y + self.margin[1], self.layout)
512 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
514 w.draw_line(pen, self.x + 1, self.y + 1, self.x + self.width - 2, self.y + 1)
515 w.draw_line(pen, self.x + 1, self.y + 1, self.x + 1, self.y + self.height - 2)
517 self.start.expose()
519 self.draw_link(self.next, 5, self.height, 'black')
520 self.draw_link(self.fail, self.width, self.height, 'red')
522 def maybe_clicked(self, event):
523 return False
525 def nearby(self, x, y):
526 return False
528 class ChainDisplay(g.EventBox):
529 "A graphical display of a chain of nodes."
530 def __init__(self, view, prog = None):
531 g.EventBox.__init__(self)
532 self.connect('destroy', self.destroyed)
533 self.set_app_paintable(True)
534 self.set_double_buffered(False)
535 self.connect('size-allocate', lambda w, a: self.size_allocate(a))
537 self.view = view
538 self.unset_flags(g.CAN_FOCUS)
540 self.drag_last_pos = None
542 self.exec_point = None # CanvasItem, or None
543 self.rec_point = None
545 self.set_active(1)
547 self.nodes = None
548 self.subs = None
549 self.set_size_request(100, 100)
551 self.prog = None
553 self.view.model.root_program.watchers.append(self)
555 self.connect('expose-event', self.expose)
557 self.add_events(g.gdk.BUTTON_PRESS_MASK |
558 g.gdk.POINTER_MOTION_MASK)
559 self.connect('button-press-event', self.button_press)
560 self.hover = None
561 self.connect('motion-notify-event', self.motion)
563 self.switch_to(prog)
565 def set_active(self, active):
566 if mono:
567 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('white'))
568 elif active:
569 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#B3AA73'))
570 else:
571 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#FFC0C0'))
573 def update_points(self):
574 self.queue_draw()
576 if self.rec_point:
577 self.scroll_to_show(self.rec_point)
579 def scroll_to_show(self, item):
580 print "XXX"
581 return
583 (lx, ly, hx, hy) = item.get_bounds()
584 x, y = item.i2w(0, 0)
585 x, y = self.w2c(x, y)
586 lx += x
587 ly += y
588 hx += x
589 hy += y
590 lx -= 16
592 sx, sy = self.get_scroll_offsets()
593 if lx < sx:
594 sx = lx
595 if ly < sy:
596 sy = ly
598 (x, y, w, h) = self.get_allocation()
599 hx -= w
600 hy -= h
602 if hx > sx:
603 sx = hx
604 if hy > sy:
605 sy = hy
607 self.scroll_to(sx, sy)
609 def put_point(self, point):
610 if not point: return
611 w = self.window
612 op, exit = point
613 if op.get_program() != self.prog: return
614 try:
615 obj = self.op_to_object[op]
616 except:
617 print "Can't find %s!\n" % op
618 return
619 x = obj.x
620 if point is self.view.rec_point:
621 size = 11
622 colour = 'red'
623 else:
624 size = 6
625 x += 2
626 colour = 'yellow'
627 y = obj.y
628 if exit == 'fail':
629 x += 4
630 y += 4
631 else:
632 y += 5
633 pen = self.style.white_gc
634 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
635 w.draw_rectangle(self.style.black_gc, False, x, y, size, size)
636 w.draw_rectangle(pen, True, x + 1, y + 1, size - 1, size - 1)
637 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
639 def destroyed(self, widget):
640 self.view.model.root_program.watchers.remove(self)
642 def switch_to(self, prog):
643 if prog is self.prog:
644 return
645 self.prog = prog
646 self.update_all()
648 def prog_tree_changed(self):
649 pass
651 def program_changed(self, op):
652 if (not op) or op.get_program() == self.prog:
653 self.update_all()
655 def create_op(self, op, x, y):
656 if isinstance(op, Block):
657 return ChainBlock(self, op, x, y)
658 else:
659 return ChainOp(self, op, x, y)
661 def update_all(self):
662 self.op_to_object = {}
663 if self.prog:
664 self.root_object = self.create_op(self.prog.code, 4, 4)
665 self.set_size_request(self.root_object.width + 8, self.root_object.height + 8)
666 else:
667 self.root_object = None
668 self.set_size_request(-1, -1)
669 self.backing = None
670 self.queue_draw()
671 return 1
673 def size_allocate(self, alloc):
674 self.backing = None
675 self.window.clear()
677 def create_backing(self):
678 self.backing = g.gdk.Pixmap(self.window, self.allocation.width, self.allocation.height, -1)
679 self.window.set_back_pixmap(self.backing, False)
680 self.backing.draw_rectangle(self.style.bg_gc[g.STATE_NORMAL], True,
681 0, 0, self.allocation.width, self.allocation.height)
682 if self.root_object:
683 self.root_object.expose()
684 self.window.clear()
685 return
687 def expose(self, da, event):
688 if not self.backing: self.create_backing()
690 self.window.draw_drawable(self.style.white_gc, self.backing, 0, 0, 0, 0, -1, -1)
692 #self.update_links()
693 self.put_point(self.view.rec_point)
694 self.put_point(self.view.exec_point)
696 #self.set_bounds()
698 op = self.hover
699 if op:
700 pen = self.style.black_gc
701 w = self.window
702 if not op.fail:
703 w.draw_rectangle(pen, False, op.x + 14, op.y + 10, 5, 5)
704 if not op.next:
705 w.draw_rectangle(pen, False, op.x + 2, op.y + 14, 5, 5)
707 def motion(self, box, event):
708 hover = None
709 for op in self.op_to_object.itervalues():
710 if op.nearby(event.x, event.y):
711 hover = op
712 if hover == self.hover:
713 return
714 self.hover = hover
715 self.queue_draw()
717 def button_press(self, da, event):
718 for op in self.op_to_object.itervalues():
719 if op.maybe_clicked(event): break
721 def op_colour(self, op):
722 if op in self.view.exec_stack:
723 return 'cyan'
724 return 'blue'
726 def update_links(self, op = None):
727 """Walk through all nodes in the tree-version of the op graph,
728 making all the links (which already exist as stubs) point to
729 the right place."""
730 if not self.prog:
731 return
732 if not op:
733 op = self.prog.code
734 if op.next:
735 if op.next.prev[0] == op:
736 self.update_links(op.next)
737 else:
738 self.join_nodes(op, 'next')
739 if op.fail:
740 if op.fail.prev[0] == op:
741 self.update_links(op.fail)
742 else:
743 self.join_nodes(op, 'fail')
744 if isinstance(op, Block):
745 self.update_links(op.start)
747 def create_node(self, op, parent):
748 if op.is_toplevel():
749 return obj
751 def edit_op(self, op):
752 def modify():
753 if op.action[0] == 'do_search' or op.action[0] == 'do_global':
754 t = editables[0].get_text()
755 print "Checking", t
756 from Ft.Xml.XPath import XPathParser
757 if t.find('@CURRENT@') == -1:
758 try:
759 XPathParser.new().parse(t)
760 except:
761 alert('Invalid search pattern!')
762 return
763 i = 0
764 for e in editables:
765 i += 1
766 if e:
767 op.action[i] = e.get_text()
768 op.changed()
769 print "Done editing!"
770 win.destroy()
772 win = g.Dialog()
773 win.vbox.pack_start(g.Label(op.action[0]), TRUE, FALSE, 0)
774 editables = [] # [ Entry | None ]
775 focus = None
776 for x in op.action[1:]:
777 entry = g.Entry()
778 entry.set_text(str(x))
779 win.vbox.pack_start(entry, TRUE, FALSE, 0)
780 if type(x) == str or type(x) == unicode:
781 editables.append(entry)
782 entry.connect('activate', lambda e: modify())
783 if not focus:
784 focus = entry
785 entry.grab_focus()
786 else:
787 entry.set_editable(FALSE)
788 editables.append(None)
790 win.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
791 win.add_button(g.STOCK_OK, g.RESPONSE_OK)
793 def response(box, resp):
794 box.destroy()
795 if resp == g.RESPONSE_OK:
796 modify()
797 win.connect('response', response)
799 if not focus:
800 win.set_response_sensitive(g.RESPONSE_OK, FALSE)
802 win.show_all()
804 def join_nodes(self, op, exit):
805 try:
806 x1, y1, x2, y2 = self.get_arrow_ends(op, exit)
808 prev_group = self.op_to_group[op]
809 line = getattr(prev_group, exit + '_line')
810 line.set(points = connect(x1, y1, x2, y2))
811 except Block:
812 print "*** ERROR setting arc from %s:%s" % (op, exit)
814 def op_event(self, item, event, op):
815 if event.type == g.gdk.BUTTON_PRESS:
816 print "Prev %s = %s" % (op, map(str, op.prev))
817 if event.button == 1:
818 if op.parent.start != op or not op.parent.is_toplevel():
819 self.drag_last_pos = (event.x, event.y)
820 else:
821 self.drag_last_pos = None
822 else:
823 self.show_op_menu(event, op)
824 elif event.type == g.gdk.BUTTON_RELEASE:
825 if event.button == 1:
826 self.drag_last_pos = None
827 self.program_changed(None)
828 elif event.type == g.gdk.ENTER_NOTIFY:
829 item.set(fill_color = '#339900')
830 elif event.type == g.gdk.LEAVE_NOTIFY:
831 item.set(fill_color = self.op_colour(op))
832 elif event.type == g.gdk.MOTION_NOTIFY and self.drag_last_pos:
833 if not event.state & g.gdk.BUTTON1_MASK:
834 print "(stop drag!)"
835 self.drag_last_pos = None
836 self.program_changed(None)
837 return 1
838 x, y = (event.x, event.y)
839 dx, dy = x - self.drag_last_pos[0], y - self.drag_last_pos[1]
840 if abs(op.dx + dx) < 4:
841 dx = -op.dx
842 x = dx + self.drag_last_pos[0]
843 if abs(op.dy + dy) < 4:
844 dy = -op.dy
845 y = dy + self.drag_last_pos[1]
846 op.dx += dx
847 op.dy += dy
848 self.drag_last_pos = (x, y)
850 self.op_to_group[op].move(dx, dy)
851 for p in op.prev:
852 if p.next == op:
853 self.join_nodes(p, 'next')
854 if p.fail == op:
855 self.join_nodes(p, 'fail')
856 self.update_links()
857 #self.create_node(self.prog.start, self.nodes)
858 self.update_points()
859 elif event.type == g.gdk._2BUTTON_PRESS:
860 if op.action[0] == 'Start':
861 self.edit_comment(op.parent)
862 else:
863 self.edit_op(op)
864 print "(edit; stop drag!)"
865 self.drag_last_pos = None
866 self.program_changed(None)
867 return 1
869 def edit_comment(self, block):
870 assert isinstance(block, Block)
872 def set(comment):
873 block.set_comment(comment)
874 GetArg('Comment', set, ['Comment:'],
875 message = '\\n for a newline', init = [block.comment])
877 def block_toggle_enter(self):
878 self.op_menu_op.toggle_enter()
880 def block_toggle_foreach(self):
881 self.op_menu_op.toggle_foreach()
883 def block_toggle_restore(self):
884 self.op_menu_op.toggle_restore()
886 def block_edit_comment(self):
887 self.edit_comment(self.op_menu_op)
889 def op_edit(self):
890 self.edit_op(self.op_menu_op)
892 def op_swap_nf(self):
893 self.op_menu_op.swap_nf()
895 def op_del_node(self):
896 op = self.op_menu_op
897 if op.next and op.fail:
898 rox.alert("Can't delete a node with both exits in use")
899 return
900 self.clipboard = op.del_node()
902 def show_op_menu(self, event, op):
903 if op.action[0] == 'Start':
904 self.op_menu_op = op.parent
905 block_menu.popup(self, event)
906 else:
907 self.op_menu_op = op
908 op_menu.popup(self, event)
910 def paste_chain(self, op, exit):
911 print "Paste", self.clipboard
912 doc = self.clipboard
913 new = load(doc.documentElement, op.parent)
914 start = new.start.next
915 new.start.unlink('next', may_delete = 0)
916 start.set_parent(None)
917 op.link_to(start, exit)
919 def end_link_drag(self, item, event, src_op, exit):
920 # Scan all the nodes looking for one nearby...
921 x, y = event.x, event.y
923 def closest_node(op):
924 "Return the closest (node, dist) in this chain to (x, y)"
925 nx, ny = self.op_to_group[op].i2w(0, 0)
926 if op is src_op:
927 best = None
928 elif isinstance(op, Block):
929 best = None
930 else:
931 best = (op, math.hypot(nx - x, ny - y))
932 if op.next and op.next.prev[0] == op:
933 next = closest_node(op.next)
934 if next and (best is None or next[1] < best[1]):
935 best = next
936 if op.fail and op.fail.prev[0] == op:
937 fail = closest_node(op.fail)
938 if fail and (best is None or fail[1] < best[1]):
939 best = fail
940 if isinstance(op, Block):
941 sub = closest_node(op.start)
942 if sub and (best is None or sub[1] < best[1]):
943 best = sub
944 return best
946 result = closest_node(self.prog.code)
947 if result:
948 node, dist = result
949 else:
950 dist = 1000
951 if dist > 12:
952 # Too far... put the line back to the disconnected state...
953 self.join_nodes(src_op, exit)
954 return
955 try:
956 while node.action[0] == 'Start':
957 node = node.parent
958 src_op.link_to(node, exit)
959 finally:
960 self.update_all()
962 def line_paste_chain(self):
963 op, exit = self.line_menu_line
964 self.paste_chain(op, exit)
966 def line_add_block(self):
967 op, exit = self.line_menu_line
968 box = rox.Dialog()
969 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
970 box.add_button(g.STOCK_ADD, g.RESPONSE_OK)
971 box.set_position(g.WIN_POS_MOUSE)
972 box.set_has_separator(False)
974 foreach = g.CheckButton('Foreach block')
975 box.vbox.pack_start(foreach)
976 enter = g.CheckButton('Enter-leave block')
977 box.vbox.pack_start(enter)
978 box.vbox.show_all()
980 resp = box.run()
981 box.destroy()
982 if resp != g.RESPONSE_OK:
983 return
985 b = Block(op.parent)
986 if foreach.get_active():
987 b.toggle_foreach()
988 if enter.get_active():
989 b.toggle_enter()
990 op.link_to(b, exit)
991 #if self.view.rec_point == (op, exit):
992 self.view.single_step = 1
993 if self.view.rec_point:
994 self.view.stop_recording()
995 self.view.set_exec((op, exit))
996 try:
997 self.view.do_one_step()
998 assert 0
999 except View.InProgress:
1000 pass
1001 print self.exec_point
1002 self.view.record_at_point()
1004 def line_toggle_breakpoint(self):
1005 op, exit = self.line_menu_line
1006 bp = self.view.breakpoints
1007 if bp.has_key((op, exit)):
1008 del bp[(op, exit)]
1009 else:
1010 bp[(op, exit)] = 1
1011 self.prog.changed()
1013 def line_yank_chain(self):
1014 op, exit = self.line_menu_line
1015 next = getattr(op, exit)
1016 if not next:
1017 rox.alert('Nothing to yank!')
1018 return
1019 self.clipboard = next.to_doc()
1020 print self.clipboard
1022 def line_del_chain(self):
1023 op, exit = self.line_menu_line
1024 next = getattr(op, exit)
1025 if not next:
1026 rox.alert('Nothing to delete!')
1027 return
1028 self.clipboard = next.to_doc()
1029 op.unlink(exit)
1031 def show_menu(self, event, op, exit = None):
1032 if exit:
1033 self.line_menu_line = (op, exit)
1034 line_menu.popup(self, event)
1035 else:
1036 self.show_op_menu(event, op)
1038 def line_event(self, item, event, op, exit):
1039 # Item may be rec_point or exec_point...
1040 item = getattr(self.op_to_group[op], exit + '_line')
1042 if event.type == g.gdk.BUTTON_PRESS:
1043 if event.button == 1:
1044 if not getattr(op, exit):
1045 self.drag_last_pos = (event.x, event.y)
1046 #item.grab(BUTTON_RELEASE | MOTION_NOTIFY, no_cursor, event.time)
1047 elif event.button == 2:
1048 self.paste_chain(op, exit)
1049 elif event.button == 3:
1050 self.line_menu_line = (op, exit)
1051 line_menu.popup(self, event)
1052 elif event.type == g.gdk.BUTTON_RELEASE:
1053 if event.button == 1:
1054 print "Clicked exit %s of %s" % (exit, op)
1055 #item.ungrab(event.time)
1056 self.view.set_exec((op, exit))
1057 self.drag_last_pos = None
1058 if not getattr(op, exit):
1059 self.end_link_drag(item, event, op, exit)
1060 elif event.type == g.gdk.MOTION_NOTIFY and self.drag_last_pos:
1061 if not event.state & g.gdk.BUTTON1_MASK:
1062 print "(stop drag!)"
1063 self.drag_last_pos = None
1064 if not getattr(op, exit):
1065 self.end_link_drag(item, event, op, exit)
1066 return 1
1067 x, y = (event.x, event.y)
1068 dx, dy = x - self.drag_last_pos[0], y - self.drag_last_pos[1]
1070 if abs(dx) > 4 or abs(dy) > 4:
1071 sx, sy = self.get_arrow_start(op, exit)
1072 x, y = item.w2i(event.x, event.y)
1073 gr = self.op_to_group[op]
1074 if exit == 'fail':
1075 width = gr.width
1076 else:
1077 width = 0
1078 item.set(points = connect(sx, sy, x, y))
1079 elif event.type == g.gdk.ENTER_NOTIFY:
1080 item.set(fill_color = '#339900')
1081 elif event.type == g.gdk.LEAVE_NOTIFY:
1082 if exit == 'next':
1083 item.set(fill_color = 'black')
1084 else:
1085 item.set(fill_color = '#ff6666')
1086 return 1
1088 def get_arrow_start(self, op, exit):
1089 gr = self.op_to_group[op]
1090 return ((exit == 'fail' and gr.width) or 0, gr.height)
1092 def get_arrow_ends(self, op, exit):
1093 """Return coords of arrow, relative to op's group."""
1094 op2 = getattr(op, exit)
1096 prev_group = self.op_to_group[op]
1098 x1, y1 = self.get_arrow_start(op, exit)
1100 if op2:
1101 try:
1102 group = self.op_to_group[op2]
1103 except:
1104 x2 = x1 + 50
1105 y2 = y1 + 50
1106 else:
1107 x2, y2 = group.i2w(0, 0)
1108 x2, y2 = prev_group.w2i(x2, y2)
1109 elif exit == 'next':
1110 x2, y2 = DEFAULT_NEXT
1111 x2 += x1
1112 y2 += y1
1113 else:
1114 x2, y2 = DEFAULT_FAIL
1115 x2 += x1
1116 y2 += y1
1117 return (x1, y1, x2, y2)
1119 def set_bounds(self):
1120 #self.update_now() # GnomeCanvas bug?
1121 min_x, min_y, max_x, max_y = self.root().get_bounds()
1122 min_x -= 8
1123 max_x += 8
1124 min_y -= 8
1125 max_y += 8
1126 self.set_scroll_region(min_x, min_y, max_x, max_y)
1127 self.root().move(0, 0) # Magic!
1128 #self.set_usize(max_x - min_x, -1)
1130 class ChainWindow(rox.Window):
1131 def __init__(self, view, prog):
1132 rox.Window.__init__(self)
1133 swin = g.ScrolledWindow()
1134 self.add(swin)
1135 disp = ChainDisplay(view, prog)
1136 swin.add(disp)
1138 swin.show_all()
1139 self.disp = disp
1140 self.set_default_size(-1, 200)
1141 self.set_title(prog.name)
1143 def update_points(self):
1144 self.disp.update_points()