Start recording automatically after creating a new block.
[dom-editor.git] / Dome / List.py
blob88ea23aa9f47e19594c202aaddb1750d500f0d1b
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 maybe_clicked(self, event):
413 x = event.x - self.x
414 if x < 0: return False
415 y = event.y - self.y
416 if y < 0: return False
418 if x > self.width or y > self.height:
419 return False
421 if event.button == 1:
422 self.da.view.set_exec((self.op, 'next'))
423 else:
424 if x < 10 and y > 10:
425 self.da.show_menu(event, self.op, 'next')
426 else:
427 self.da.show_menu(event, self.op)
428 return True
430 def all_nodes(self):
431 yield self
432 if self.next:
433 for n in self.next.all_nodes(): yield n
434 if self.fail:
435 for n in self.fail.all_nodes(): yield n
438 class ChainBlock(ChainOp):
439 def __init__(self, da, block, x, y):
440 assert isinstance(block, Block)
441 ChainOp.__init__(self, da, block, x, y)
442 self.depth = 1
443 p = block.parent
444 while p and not isinstance(p, Program):
445 p = p.parent
446 self.depth += 1
448 def build_leaf(self):
449 x = self.x
450 y = self.y
452 if self.op.comment:
453 self.layout = self.da.create_pango_layout(self.op.comment.replace('\\n', '\n'))
454 self.width, height = self.layout.get_pixel_size()
455 y += height + 4
456 else:
457 self.layout = None
458 self.width = 40
460 self.margin = (4 + self.op.foreach * 6, 4 + (self.op.enter + self.op.restore) * 6)
461 self.width += self.margin[0]
463 self.start = self.da.create_op(self.op.start, x + self.margin[0], y + self.margin[1])
465 self.height = 20
467 for node in self.start.all_nodes():
468 self.width = max(self.width, node.x + node.width - self.x)
469 self.height = max(self.height, node.y + node.height - self.y)
471 self.width += 4
472 self.height += 4 + (self.op.enter + self.op.restore) * 6
474 def expose(self):
475 da = self.da
476 w = da.backing
477 w.draw_rectangle(da.style.black_gc, False, self.x, self.y, self.width, self.height)
478 pen = da.style.white_gc
479 width = self.width
480 x = self.x
481 y = self.y
483 d = 15 - min(self.depth, 7)
484 pen.set_rgb_fg_color(g.gdk.color_parse('#%x%x%x' % (d, d, d)))
485 w.draw_rectangle(pen, True, self.x + 1, self.y + 1, self.width - 1, self.height - 1)
487 op = self.op
488 if op.foreach:
489 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
490 w.draw_rectangle(pen, True, x + 1, y + 1, 6, self.height - 1)
491 x += 6
492 width -= 6
494 if op.enter:
495 pen.set_rgb_fg_color(g.gdk.color_parse('yellow'))
496 w.draw_rectangle(pen, True, x + 1, y + 1, width - 1, 6)
497 w.draw_rectangle(pen, True, x + 1, y + self.height - 6, width - 1, 6)
498 if op.restore:
499 pen.set_rgb_fg_color(g.gdk.color_parse('orange'))
500 margin = op.enter * 6
501 w.draw_rectangle(pen, True, x + 1, y + 1 + margin, width - 1, 6)
502 w.draw_rectangle(pen, True, x + 1, y + self.height - 6 - margin, width - 1, 6)
504 if self.layout:
505 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
506 w.draw_layout(pen, self.x + self.margin[0], self.y + self.margin[1], self.layout)
508 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
510 w.draw_line(pen, self.x + 1, self.y + 1, self.x + self.width - 2, self.y + 1)
511 w.draw_line(pen, self.x + 1, self.y + 1, self.x + 1, self.y + self.height - 2)
513 self.start.expose()
515 self.draw_link(self.next, 5, self.height, 'black')
516 self.draw_link(self.fail, self.width, self.height, 'red')
518 def maybe_clicked(self, event):
519 return False
521 class ChainDisplay(g.EventBox):
522 "A graphical display of a chain of nodes."
523 def __init__(self, view, prog = None):
524 g.EventBox.__init__(self)
525 self.connect('destroy', self.destroyed)
526 self.set_app_paintable(True)
527 self.set_double_buffered(False)
528 self.connect('size-allocate', lambda w, a: self.size_allocate(a))
530 self.view = view
531 self.unset_flags(g.CAN_FOCUS)
533 self.drag_last_pos = None
535 self.exec_point = None # CanvasItem, or None
536 self.rec_point = None
538 self.set_active(1)
540 self.nodes = None
541 self.subs = None
542 self.set_size_request(100, 100)
544 self.prog = None
546 self.view.model.root_program.watchers.append(self)
548 self.connect('expose-event', self.expose)
549 self.add_events(g.gdk.BUTTON_PRESS_MASK)
550 self.connect('button-press-event', self.button_press)
552 self.switch_to(prog)
554 def set_active(self, active):
555 if mono:
556 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('white'))
557 elif active:
558 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#B3AA73'))
559 else:
560 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#FFC0C0'))
562 def update_points(self):
563 self.queue_draw()
565 if self.rec_point:
566 self.scroll_to_show(self.rec_point)
568 def scroll_to_show(self, item):
569 print "XXX"
570 return
572 (lx, ly, hx, hy) = item.get_bounds()
573 x, y = item.i2w(0, 0)
574 x, y = self.w2c(x, y)
575 lx += x
576 ly += y
577 hx += x
578 hy += y
579 lx -= 16
581 sx, sy = self.get_scroll_offsets()
582 if lx < sx:
583 sx = lx
584 if ly < sy:
585 sy = ly
587 (x, y, w, h) = self.get_allocation()
588 hx -= w
589 hy -= h
591 if hx > sx:
592 sx = hx
593 if hy > sy:
594 sy = hy
596 self.scroll_to(sx, sy)
598 def put_point(self, point):
599 if not point: return
600 w = self.window
601 op, exit = point
602 if op.get_program() != self.prog: return
603 try:
604 obj = self.op_to_object[op]
605 except:
606 print "Can't find %s!\n" % op
607 return
608 x = obj.x
609 if point is self.view.rec_point:
610 size = 11
611 colour = 'red'
612 else:
613 size = 6
614 x += 2
615 colour = 'yellow'
616 y = obj.y
617 if exit == 'fail':
618 x += 4
619 y += 4
620 else:
621 y += 5
622 pen = self.style.white_gc
623 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
624 w.draw_rectangle(self.style.black_gc, False, x, y, size, size)
625 w.draw_rectangle(pen, True, x + 1, y + 1, size - 1, size - 1)
626 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
628 def destroyed(self, widget):
629 self.view.model.root_program.watchers.remove(self)
631 def switch_to(self, prog):
632 if prog is self.prog:
633 return
634 self.prog = prog
635 self.update_all()
637 def prog_tree_changed(self):
638 pass
640 def program_changed(self, op):
641 if (not op) or op.get_program() == self.prog:
642 self.update_all()
644 def create_op(self, op, x, y):
645 if isinstance(op, Block):
646 return ChainBlock(self, op, x, y)
647 else:
648 return ChainOp(self, op, x, y)
650 def update_all(self):
651 self.op_to_object = {}
652 if self.prog:
653 self.root_object = self.create_op(self.prog.code, 4, 4)
654 self.set_size_request(self.root_object.width + 8, self.root_object.height + 8)
655 else:
656 self.root_object = None
657 self.set_size_request(-1, -1)
658 self.backing = None
659 self.queue_draw()
660 return 1
662 def size_allocate(self, alloc):
663 self.backing = None
664 self.window.clear()
666 def create_backing(self):
667 self.backing = g.gdk.Pixmap(self.window, self.allocation.width, self.allocation.height, -1)
668 self.window.set_back_pixmap(self.backing, False)
669 self.backing.draw_rectangle(self.style.bg_gc[g.STATE_NORMAL], True,
670 0, 0, self.allocation.width, self.allocation.height)
671 if self.root_object:
672 self.root_object.expose()
673 self.window.clear()
674 return
676 def expose(self, da, event):
677 if not self.backing: self.create_backing()
679 self.window.draw_drawable(self.style.white_gc, self.backing, 0, 0, 0, 0, -1, -1)
681 #self.update_links()
682 self.put_point(self.view.rec_point)
683 self.put_point(self.view.exec_point)
685 #self.set_bounds()
687 def button_press(self, da, event):
688 for op in self.op_to_object.itervalues():
689 if op.maybe_clicked(event): break
691 def op_colour(self, op):
692 if op in self.view.exec_stack:
693 return 'cyan'
694 return 'blue'
696 def update_links(self, op = None):
697 """Walk through all nodes in the tree-version of the op graph,
698 making all the links (which already exist as stubs) point to
699 the right place."""
700 if not self.prog:
701 return
702 if not op:
703 op = self.prog.code
704 if op.next:
705 if op.next.prev[0] == op:
706 self.update_links(op.next)
707 else:
708 self.join_nodes(op, 'next')
709 if op.fail:
710 if op.fail.prev[0] == op:
711 self.update_links(op.fail)
712 else:
713 self.join_nodes(op, 'fail')
714 if isinstance(op, Block):
715 self.update_links(op.start)
717 def create_node(self, op, parent):
718 if op.is_toplevel():
719 return obj
721 def edit_op(self, op):
722 def modify():
723 if op.action[0] == 'do_search' or op.action[0] == 'do_global':
724 t = editables[0].get_text()
725 print "Checking", t
726 from Ft.Xml.XPath import XPathParser
727 if t.find('@CURRENT@') == -1:
728 try:
729 XPathParser.new().parse(t)
730 except:
731 alert('Invalid search pattern!')
732 return
733 i = 0
734 for e in editables:
735 i += 1
736 if e:
737 op.action[i] = e.get_text()
738 op.changed()
739 print "Done editing!"
740 win.destroy()
742 win = g.Dialog()
743 win.vbox.pack_start(g.Label(op.action[0]), TRUE, FALSE, 0)
744 editables = [] # [ Entry | None ]
745 focus = None
746 for x in op.action[1:]:
747 entry = g.Entry()
748 entry.set_text(str(x))
749 win.vbox.pack_start(entry, TRUE, FALSE, 0)
750 if type(x) == str or type(x) == unicode:
751 editables.append(entry)
752 entry.connect('activate', lambda e: modify())
753 if not focus:
754 focus = entry
755 entry.grab_focus()
756 else:
757 entry.set_editable(FALSE)
758 editables.append(None)
760 win.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
761 win.add_button(g.STOCK_OK, g.RESPONSE_OK)
763 def response(box, resp):
764 box.destroy()
765 if resp == g.RESPONSE_OK:
766 modify()
767 win.connect('response', response)
769 if not focus:
770 win.set_response_sensitive(g.RESPONSE_OK, FALSE)
772 win.show_all()
774 def join_nodes(self, op, exit):
775 try:
776 x1, y1, x2, y2 = self.get_arrow_ends(op, exit)
778 prev_group = self.op_to_group[op]
779 line = getattr(prev_group, exit + '_line')
780 line.set(points = connect(x1, y1, x2, y2))
781 except Block:
782 print "*** ERROR setting arc from %s:%s" % (op, exit)
784 def op_event(self, item, event, op):
785 if event.type == g.gdk.BUTTON_PRESS:
786 print "Prev %s = %s" % (op, map(str, op.prev))
787 if event.button == 1:
788 if op.parent.start != op or not op.parent.is_toplevel():
789 self.drag_last_pos = (event.x, event.y)
790 else:
791 self.drag_last_pos = None
792 else:
793 self.show_op_menu(event, op)
794 elif event.type == g.gdk.BUTTON_RELEASE:
795 if event.button == 1:
796 self.drag_last_pos = None
797 self.program_changed(None)
798 elif event.type == g.gdk.ENTER_NOTIFY:
799 item.set(fill_color = '#339900')
800 elif event.type == g.gdk.LEAVE_NOTIFY:
801 item.set(fill_color = self.op_colour(op))
802 elif event.type == g.gdk.MOTION_NOTIFY and self.drag_last_pos:
803 if not event.state & g.gdk.BUTTON1_MASK:
804 print "(stop drag!)"
805 self.drag_last_pos = None
806 self.program_changed(None)
807 return 1
808 x, y = (event.x, event.y)
809 dx, dy = x - self.drag_last_pos[0], y - self.drag_last_pos[1]
810 if abs(op.dx + dx) < 4:
811 dx = -op.dx
812 x = dx + self.drag_last_pos[0]
813 if abs(op.dy + dy) < 4:
814 dy = -op.dy
815 y = dy + self.drag_last_pos[1]
816 op.dx += dx
817 op.dy += dy
818 self.drag_last_pos = (x, y)
820 self.op_to_group[op].move(dx, dy)
821 for p in op.prev:
822 if p.next == op:
823 self.join_nodes(p, 'next')
824 if p.fail == op:
825 self.join_nodes(p, 'fail')
826 self.update_links()
827 #self.create_node(self.prog.start, self.nodes)
828 self.update_points()
829 elif event.type == g.gdk._2BUTTON_PRESS:
830 if op.action[0] == 'Start':
831 self.edit_comment(op.parent)
832 else:
833 self.edit_op(op)
834 print "(edit; stop drag!)"
835 self.drag_last_pos = None
836 self.program_changed(None)
837 return 1
839 def edit_comment(self, block):
840 assert isinstance(block, Block)
842 def set(comment):
843 block.set_comment(comment)
844 GetArg('Comment', set, ['Comment:'],
845 message = '\\n for a newline', init = [block.comment])
847 def block_toggle_enter(self):
848 self.op_menu_op.toggle_enter()
850 def block_toggle_foreach(self):
851 self.op_menu_op.toggle_foreach()
853 def block_toggle_restore(self):
854 self.op_menu_op.toggle_restore()
856 def block_edit_comment(self):
857 self.edit_comment(self.op_menu_op)
859 def op_edit(self):
860 self.edit_op(self.op_menu_op)
862 def op_swap_nf(self):
863 self.op_menu_op.swap_nf()
865 def op_del_node(self):
866 op = self.op_menu_op
867 if op.next and op.fail:
868 rox.alert("Can't delete a node with both exits in use")
869 return
870 self.clipboard = op.del_node()
872 def show_op_menu(self, event, op):
873 if op.action[0] == 'Start':
874 self.op_menu_op = op.parent
875 block_menu.popup(self, event)
876 else:
877 self.op_menu_op = op
878 op_menu.popup(self, event)
880 def paste_chain(self, op, exit):
881 print "Paste", self.clipboard
882 doc = self.clipboard
883 new = load(doc.documentElement, op.parent)
884 start = new.start.next
885 new.start.unlink('next', may_delete = 0)
886 start.set_parent(None)
887 op.link_to(start, exit)
889 def end_link_drag(self, item, event, src_op, exit):
890 # Scan all the nodes looking for one nearby...
891 x, y = event.x, event.y
893 def closest_node(op):
894 "Return the closest (node, dist) in this chain to (x, y)"
895 nx, ny = self.op_to_group[op].i2w(0, 0)
896 if op is src_op:
897 best = None
898 elif isinstance(op, Block):
899 best = None
900 else:
901 best = (op, math.hypot(nx - x, ny - y))
902 if op.next and op.next.prev[0] == op:
903 next = closest_node(op.next)
904 if next and (best is None or next[1] < best[1]):
905 best = next
906 if op.fail and op.fail.prev[0] == op:
907 fail = closest_node(op.fail)
908 if fail and (best is None or fail[1] < best[1]):
909 best = fail
910 if isinstance(op, Block):
911 sub = closest_node(op.start)
912 if sub and (best is None or sub[1] < best[1]):
913 best = sub
914 return best
916 result = closest_node(self.prog.code)
917 if result:
918 node, dist = result
919 else:
920 dist = 1000
921 if dist > 12:
922 # Too far... put the line back to the disconnected state...
923 self.join_nodes(src_op, exit)
924 return
925 try:
926 while node.action[0] == 'Start':
927 node = node.parent
928 src_op.link_to(node, exit)
929 finally:
930 self.update_all()
932 def line_paste_chain(self):
933 op, exit = self.line_menu_line
934 self.paste_chain(op, exit)
936 def line_add_block(self):
937 op, exit = self.line_menu_line
938 box = rox.Dialog()
939 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
940 box.add_button(g.STOCK_ADD, g.RESPONSE_OK)
942 foreach = g.CheckButton('Foreach block')
943 box.vbox.pack_start(foreach)
944 enter = g.CheckButton('Enter-leave block')
945 box.vbox.pack_start(enter)
946 box.vbox.show_all()
948 resp = box.run()
949 box.destroy()
950 if resp != g.RESPONSE_OK:
951 return
953 b = Block(op.parent)
954 if foreach.get_active():
955 b.toggle_foreach()
956 if enter.get_active():
957 b.toggle_enter()
958 op.link_to(b, exit)
959 #if self.view.rec_point == (op, exit):
960 self.view.single_step = 1
961 if self.view.rec_point:
962 self.view.stop_recording()
963 self.view.set_exec((op, exit))
964 try:
965 self.view.do_one_step()
966 assert 0
967 except View.InProgress:
968 pass
969 print self.exec_point
970 self.view.record_at_point()
972 def line_toggle_breakpoint(self):
973 op, exit = self.line_menu_line
974 bp = self.view.breakpoints
975 if bp.has_key((op, exit)):
976 del bp[(op, exit)]
977 else:
978 bp[(op, exit)] = 1
979 self.prog.changed()
981 def line_yank_chain(self):
982 op, exit = self.line_menu_line
983 next = getattr(op, exit)
984 if not next:
985 rox.alert('Nothing to yank!')
986 return
987 self.clipboard = next.to_doc()
988 print self.clipboard
990 def line_del_chain(self):
991 op, exit = self.line_menu_line
992 next = getattr(op, exit)
993 if not next:
994 rox.alert('Nothing to delete!')
995 return
996 self.clipboard = next.to_doc()
997 op.unlink(exit)
999 def show_menu(self, event, op, exit = None):
1000 if exit:
1001 self.line_menu_line = (op, exit)
1002 line_menu.popup(self, event)
1003 else:
1004 self.show_op_menu(event, op)
1006 def line_event(self, item, event, op, exit):
1007 # Item may be rec_point or exec_point...
1008 item = getattr(self.op_to_group[op], exit + '_line')
1010 if event.type == g.gdk.BUTTON_PRESS:
1011 if event.button == 1:
1012 if not getattr(op, exit):
1013 self.drag_last_pos = (event.x, event.y)
1014 #item.grab(BUTTON_RELEASE | MOTION_NOTIFY, no_cursor, event.time)
1015 elif event.button == 2:
1016 self.paste_chain(op, exit)
1017 elif event.button == 3:
1018 self.line_menu_line = (op, exit)
1019 line_menu.popup(self, event)
1020 elif event.type == g.gdk.BUTTON_RELEASE:
1021 if event.button == 1:
1022 print "Clicked exit %s of %s" % (exit, op)
1023 #item.ungrab(event.time)
1024 self.view.set_exec((op, exit))
1025 self.drag_last_pos = None
1026 if not getattr(op, exit):
1027 self.end_link_drag(item, event, op, exit)
1028 elif event.type == g.gdk.MOTION_NOTIFY and self.drag_last_pos:
1029 if not event.state & g.gdk.BUTTON1_MASK:
1030 print "(stop drag!)"
1031 self.drag_last_pos = None
1032 if not getattr(op, exit):
1033 self.end_link_drag(item, event, op, exit)
1034 return 1
1035 x, y = (event.x, event.y)
1036 dx, dy = x - self.drag_last_pos[0], y - self.drag_last_pos[1]
1038 if abs(dx) > 4 or abs(dy) > 4:
1039 sx, sy = self.get_arrow_start(op, exit)
1040 x, y = item.w2i(event.x, event.y)
1041 gr = self.op_to_group[op]
1042 if exit == 'fail':
1043 width = gr.width
1044 else:
1045 width = 0
1046 item.set(points = connect(sx, sy, x, y))
1047 elif event.type == g.gdk.ENTER_NOTIFY:
1048 item.set(fill_color = '#339900')
1049 elif event.type == g.gdk.LEAVE_NOTIFY:
1050 if exit == 'next':
1051 item.set(fill_color = 'black')
1052 else:
1053 item.set(fill_color = '#ff6666')
1054 return 1
1056 def get_arrow_start(self, op, exit):
1057 gr = self.op_to_group[op]
1058 return ((exit == 'fail' and gr.width) or 0, gr.height)
1060 def get_arrow_ends(self, op, exit):
1061 """Return coords of arrow, relative to op's group."""
1062 op2 = getattr(op, exit)
1064 prev_group = self.op_to_group[op]
1066 x1, y1 = self.get_arrow_start(op, exit)
1068 if op2:
1069 try:
1070 group = self.op_to_group[op2]
1071 except:
1072 x2 = x1 + 50
1073 y2 = y1 + 50
1074 else:
1075 x2, y2 = group.i2w(0, 0)
1076 x2, y2 = prev_group.w2i(x2, y2)
1077 elif exit == 'next':
1078 x2, y2 = DEFAULT_NEXT
1079 x2 += x1
1080 y2 += y1
1081 else:
1082 x2, y2 = DEFAULT_FAIL
1083 x2 += x1
1084 y2 += y1
1085 return (x1, y1, x2, y2)
1087 def set_bounds(self):
1088 #self.update_now() # GnomeCanvas bug?
1089 min_x, min_y, max_x, max_y = self.root().get_bounds()
1090 min_x -= 8
1091 max_x += 8
1092 min_y -= 8
1093 max_y += 8
1094 self.set_scroll_region(min_x, min_y, max_x, max_y)
1095 self.root().move(0, 0) # Magic!
1096 #self.set_usize(max_x - min_x, -1)
1098 class ChainWindow(rox.Window):
1099 def __init__(self, view, prog):
1100 rox.Window.__init__(self)
1101 swin = g.ScrolledWindow()
1102 self.add(swin)
1103 disp = ChainDisplay(view, prog)
1104 swin.add(disp)
1106 swin.show_all()
1107 self.disp = disp
1108 self.set_default_size(-1, 200)
1109 self.set_title(prog.name)
1111 def update_points(self):
1112 self.disp.update_points()