When running a program, don't reset the exec stack if recording.
[dom-editor.git] / Dome / List.py
blob99c236a37f11264798bf3704978498120831d7c3
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 ('/Toggle Propagate Failure', 'op_toggle_propagate', '', ''),
42 ('/Remove node', 'op_del_node', '', '')
45 op_menu = Menu('op', [
46 ('/Edit node', 'op_edit', '', ''),
47 ('/Swap next\/fail', 'op_swap_nf', '', ''),
48 ('/Toggle Propagate Failure', 'op_toggle_propagate', '', ''),
49 ('/Remove node', 'op_del_node', '', '')
52 from GetArg import GetArg
53 from Program import Program, load, Block
55 box_size = 9
57 def trunc(text):
58 if len(text) < 28:
59 return text
60 return text[:26] + '...'
62 def connect(x1, y1, x2, y2):
63 """Chop 5 pixels off both ends of this line"""
64 gap = 5.0
65 dx = x2 - x1
66 dy = y2 - y1
67 l = math.hypot(dx, dy)
68 if l:
69 dx *= gap / l
70 dy *= gap / l
71 return (x1 + dx, y1 + dy, x2 - dx, y2 - dy)
73 DEFAULT_NEXT = (0, 25)
74 DEFAULT_FAIL = (20, 20)
76 expand_history = {} # Prog name -> expanded flag
78 def action_to_text(action):
79 text = action[0]
80 if text == 'Start': return ''
81 if text[:3] == 'do_':
82 text = text[3:]
83 text = string.capitalize(string.replace(text, '_', ' '))
84 if text == 'Global':
85 text = 'Select nodes'
87 if len(action) > 1:
88 if action[0] in ('do_search', 'xpath', 'move_selection'):
89 pat = str(action[1])
90 pat = string.replace(pat, 'following-sibling::', '>>')
91 pat = string.replace(pat, 'preceding-sibling::', '<<')
92 pat = string.replace(pat, 'child::', '')
93 pat = string.replace(pat, '[1]', '')
94 pat = string.replace(pat, 'text()[ext:match', '[')
95 details = ''
96 while len(pat) > 20:
97 i = string.rfind(pat[:20], '/')
98 if i == -1:
99 i = string.rfind(pat[:20], ':')
100 if i == -1:
101 i = 20
102 details = details + pat[:i + 1] + '\n'
103 pat = pat[i + 1:]
104 details = details + pat
105 elif action[0] == 'attribute':
106 details = trunc(str(action[2]))
107 elif action[0] == 'set_attrib':
108 details = trunc(str(action[1]))
109 elif action[0] == 'add_attrib':
110 details = trunc(str(action[2]))
111 elif action[0] == 'add_node':
112 details = trunc(action[2])
113 elif action[0] == 'subst':
114 details = action[1] + ' -> ' + action[2]
115 elif action[0] == 'play' or action[0] == 'map':
116 if len(action[1]) > 20:
117 details = '...' + str(action[1][-19:])
118 else:
119 details = str(action[1])
120 else:
121 if len(action) > 2:
122 details = `action[1:]`
123 else:
124 details = str(action[1])
125 if len(details) > 20:
126 details = trunc(`details`)
127 text = text + '\n' + details
128 return text
130 class List(g.VBox):
131 def __init__(self, view):
132 g.VBox.__init__(self)
134 def destroyed(widget):
135 #print "List destroy!!"
136 sel.disconnect(self.sel_changed_signal)
137 self.view.lists.remove(self)
138 self.view.model.root_program.watchers.remove(self)
139 self.connect('destroy', destroyed)
141 self.view = view
142 self.sub_windows = []
144 self.stack_frames = g.Label('')
145 self.pack_start(self.stack_frames, FALSE, TRUE, 0)
146 self.stack_frames.show()
147 self.update_stack(None)
149 pane = g.VPaned()
150 self.pack_start(pane, expand = 1, fill = 1)
152 swin = g.ScrolledWindow()
153 swin.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
154 pane.add1(swin)
155 self.prog_model = g.TreeStore(str, str)
156 tree = g.TreeView(self.prog_model)
157 tree.connect('button-press-event', self.button_press)
158 tree.unset_flags(g.CAN_FOCUS)
159 tree.set_headers_visible(FALSE)
160 self.tree = tree
162 cell = g.CellRendererText()
163 column = g.TreeViewColumn('Program', cell, text = 0)
164 tree.append_column(column)
166 sel = tree.get_selection()
167 # Doesn't get destroyed, so record signal number
168 self.sel_changed_signal = sel.connect('changed', self.change_prog)
170 self.chains = ChainDisplay(view)
171 self.prog_tree_changed()
172 v = g.Viewport()
173 v.add(tree)
174 swin.add(v)
175 v.set_shadow_type(g.SHADOW_NONE)
176 v.show_all()
178 swin = g.ScrolledWindow()
179 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
180 pane.add2(swin)
181 swin.add_with_viewport(self.chains)
182 swin.show_all()
184 pane.set_position(200)
186 sel.set_mode(g.SELECTION_BROWSE)
187 root_iter = self.prog_model.get_iter_first()
188 sel.select_iter(root_iter)
189 tree.expand_row(self.prog_model.get_path(root_iter), FALSE)
190 tree.show()
191 self.view.lists.append(self)
192 self.view.model.root_program.watchers.append(self)
194 def change_prog(self, sel):
195 selected = sel.get_selected()
196 if not selected:
197 return
198 model, iter = selected
199 if iter:
200 path = model.get_value(iter, 1)
201 self.chains.switch_to(self.view.name_to_prog(path))
202 else:
203 self.chains.switch_to(None)
205 def set_innermost_failure(self, op):
206 prog = op.get_program()
207 #print "list: set_innermost_failure:", prog
208 self.show_prog(prog)
210 def update_points(self):
211 self.chains.update_points()
212 for x in self.sub_windows:
213 x.update_points()
215 def program_changed(self, op):
216 pass
218 def prog_tree_changed(self):
219 self.prog_to_path = {}
220 self.prog_model.clear()
221 self.build_tree(self.view.model.root_program)
223 # Check for now deleted programs still being displayed
224 root = self.view.model.root_program
225 if self.chains and self.chains.prog and not self.chains.prog.parent:
226 self.chains.switch_to(None)
227 for x in self.sub_windows:
228 if x.disp.prog is not root and not x.disp.prog.parent:
229 x.destroy()
231 def build_tree(self, prog, iter = None):
232 child_iter = self.prog_model.append(iter)
233 self.prog_model.set(child_iter, 0, prog.name,
234 1, prog.get_path())
236 self.prog_to_path[prog] = self.prog_model.get_path(child_iter)
237 for p in prog.subprograms.values():
238 self.build_tree(p, child_iter)
240 def run_return(self, exit):
241 print "List execution finished:", exit
242 if exit != 'next':
243 #self.view.jump_to_innermost_failure()
244 def record():
245 if rox.confirm("Program failed - record a failure case?",
246 g.STOCK_NO, 'Record'):
247 self.view.record_at_point()
248 return False
249 g.idle_add(record)
251 def button_press(self, tree, event):
252 if event.button == 2 or event.button == 3:
253 ret = tree.get_path_at_pos(int(event.x), int(event.y))
254 if not ret:
255 return 1 # Click on blank area
256 path, col, cx, cy = ret
257 #print "Event on", path
258 iter = self.prog_model.get_iter(path)
259 path = self.prog_model.get_value(iter, 1)
260 if event.button == 3:
261 prog = self.view.name_to_prog(path)
262 self.show_menu(event, prog)
263 else:
264 if not self.view.rec_point:
265 self.view.run_new(self.run_return)
266 self.view.set_status("Running '%s'" % path)
267 if event.state & g.gdk.SHIFT_MASK:
268 self.view.may_record(['map', path])
269 else:
270 self.view.may_record(['play', path])
271 return 0
273 def menu_delete(self):
274 prog = self.prog_menu_prog
275 if not prog.parent:
276 rox.alert("Can't delete the root program!")
277 return
278 prog.parent.remove_sub(prog)
280 def menu_rename(self):
281 prog = self.prog_menu_prog
282 def rename(name, prog = prog):
283 prog.rename(name)
284 GetArg('Rename program', rename, ['Program name:'])
286 def menu_new_prog(self):
287 prog = self.prog_menu_prog
288 def create(name):
289 new = Program(name)
290 prog.add_sub(new)
291 GetArg('New program', create, ['Program name:'])
293 def menu_new_view(self):
294 prog = self.prog_menu_prog
295 cw = ChainWindow(self.view, prog)
296 cw.show()
297 self.sub_windows.append(cw)
298 def lost_cw(win):
299 self.sub_windows.remove(cw)
300 cw.connect('destroy', lost_cw)
302 def menu_map(self):
303 prog = self.prog_menu_prog
304 if not self.view.rec_point:
305 self.view.run_new(self.run_return)
306 self.view.set_status("Running '%s'" % prog.get_path())
307 self.view.may_record(['map', prog.get_path()])
309 def menu_play(self):
310 prog = self.prog_menu_prog
311 if not self.view.rec_point:
312 self.view.run_new(self.run_return)
313 self.view.set_status("Running '%s'" % prog.get_path())
314 self.view.may_record(['play', prog.get_path()])
316 def show_menu(self, event, prog):
317 self.prog_menu_prog = prog
318 prog_menu.popup(self, event)
320 def update_stack(self, op):
321 "The stack has changed - redraw 'op'"
322 if op and op.get_program() == self.chains.prog:
323 self.chains.update_all()
324 l = len(self.view.exec_stack) + len(self.view.foreach_stack)
325 if l == 0:
326 text = 'No stack'
327 elif l == 1:
328 text = '1 frame'
329 else:
330 text = '%d frames' % l
331 if self.view.chroots:
332 text += ' (%d enters)' % len(self.view.chroots)
333 self.stack_frames.set_text(text)
335 def show_prog(self, prog):
336 path = self.prog_to_path[prog]
337 partial = []
338 for p in path[:-1]:
339 partial.append(p)
340 self.tree.expand_row(tuple(partial), FALSE)
341 iter = self.prog_model.get_iter(path)
342 self.tree.get_selection().select_iter(iter)
344 def selected_program(self):
345 return self.chains.prog
347 class ChainDummy(g.TreeView):
348 def __init__(self, view, prog = None):
349 g.TreeView.__init__(self)
350 self.prog = prog
351 def switch_to(self, prog):
352 self.prog = prog
353 def update_points(self):
354 pass
356 class ChainNode:
357 "A visual object in the display."
358 def __init__(self, da, x, y):
359 self.x = x
360 self.y = y
361 self.da = da
363 def expose(self):
364 da = self.da
365 w = da.backing
366 w.draw_rectangle(da.style.black_gc, True, self.x, self.y, 10, 10)
368 def maybe_clicked(self, event):
369 return False
371 class ChainOp(ChainNode):
372 next_box = (0, 12)
373 fail_box = (12, 8)
375 def __init__(self, da, op, x, y):
376 self.op = op
377 ChainNode.__init__(self, da, x, y)
379 self.build_leaf()
381 da.op_to_object[op] = self
383 if op.next:
384 self.next = da.create_op(op.next, x, y + self.height + 4)
385 self.total_width = max(self.width, self.next.total_width)
386 else:
387 self.next = None
388 self.total_width = self.width
390 if op.fail:
391 if op.next:
392 indent = self.total_width + 20
393 else:
394 indent = 100
395 self.fail = da.create_op(op.fail, x + indent, y + self.height + 4)
396 self.total_width = indent + self.fail.total_width
397 else:
398 self.fail = None
400 def build_leaf(self):
401 text = str(action_to_text(self.op.action))
402 self.layout = self.da.create_pango_layout(text)
404 self.width, self.height = self.layout.get_pixel_size()
405 self.width += 12
406 self.height = max(self.height, 20)
408 def expose(self):
409 da = self.da
410 w = da.backing
411 op = self.op
413 w.draw_arc(da.style.white_gc, True, self.x, self.y, 10, 10, 0, 400 * 60)
414 w.draw_arc(da.style.black_gc, False, self.x, self.y, 10, 10, 0, 400 * 60)
415 w.draw_layout(da.style.black_gc, self.x + 12, self.y - 2, self.layout)
417 self.draw_link(self.next, 5, 10, 'black')
418 self.draw_link(self.fail or self.op.propagate_fail, 5, 12, 'red')
420 self.draw_breakpoints()
422 def draw_breakpoints(self):
423 da = self.da
424 w = da.backing
425 op = self.op
427 if (op, 'next') in self.da.view.breakpoints:
428 w.draw_arc(da.style.black_gc, True,
429 self.x + self.next_box[0] + 1,
430 self.y + self.next_box[1] + 1,
431 8, 8, 0, 400 * 60)
432 if (op, 'fail') in self.da.view.breakpoints:
433 w.draw_arc(da.style.black_gc, True,
434 self.x + self.fail_box[0] + 1,
435 self.y + self.fail_box[1] + 1,
436 8, 8, 0, 400 * 60)
438 def draw_link(self, dest, dx, dy, colour):
439 if not dest: return
441 if dest is not True:
442 dest.expose()
443 dest_x = dest.x
444 dest_y = dest.y
445 else:
446 dest_x = self.x + self.width
447 dest_y = self.y + dy
449 da = self.da
450 pen = da.style.white_gc
451 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
452 da.backing.draw_line(pen, self.x + dx, self.y + dy, dest_x + 5, self.y + dy)
453 da.backing.draw_line(pen, dest_x + 5, self.y + dy, dest_x + 5, dest_y)
454 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
456 def where(self, x, y):
457 "Identify where (x,y) falls on us -> None, 'op', 'next', 'fail'"
458 x -= self.x
459 if x < 0: return False
460 y -= self.y
461 if y < 0: return False
463 if x >= self.next_box[0] and y >= self.next_box[1] and \
464 x <= self.next_box[0] + box_size and y <= self.next_box[1] + box_size:
465 return 'next'
467 if x >= self.fail_box[0] and y >= self.fail_box[1] and \
468 x <= self.fail_box[0] + box_size and y <= self.fail_box[1] + box_size:
469 return 'fail'
471 if x < self.width and y < self.height: return 'op'
473 def maybe_clicked(self, event):
474 pos = self.where(event.x, event.y)
475 if not pos: return
476 if event.button == 1:
477 if pos in ('next', 'fail'):
478 self.da.view.set_exec((self.op, pos))
479 else:
480 if pos != 'op':
481 self.da.show_menu(event, self.op, pos)
482 else:
483 self.da.show_menu(event, self.op)
484 return True
486 def all_nodes(self):
487 yield self
488 if self.next:
489 for n in self.next.all_nodes(): yield n
490 if self.fail:
491 for n in self.fail.all_nodes(): yield n
494 class ChainBlock(ChainOp):
495 def __init__(self, da, block, x, y):
496 assert isinstance(block, Block)
497 ChainOp.__init__(self, da, block, x, y)
498 self.depth = 1
499 p = block.parent
500 while p and not isinstance(p, Program):
501 p = p.parent
502 self.depth += 1
503 self.next_box = (0, self.height)
504 self.fail_box = (self.width, self.height - box_size)
506 def build_leaf(self):
507 x = self.x
508 y = self.y
510 if self.op.comment:
511 self.layout = self.da.create_pango_layout(self.op.comment.replace('\\n', '\n'))
512 self.width, height = self.layout.get_pixel_size()
513 y += height + 4
514 else:
515 self.layout = None
516 self.width = 40
518 self.margin = (4 + self.op.foreach * 6, 4 + (self.op.enter + self.op.restore) * 6)
519 self.width += self.margin[0]
521 self.start = self.da.create_op(self.op.start, x + self.margin[0], y + self.margin[1])
523 self.height = 20
525 for node in self.start.all_nodes():
526 self.height = max(self.height, node.y + node.height - self.y)
527 self.width = max(self.width, self.start.total_width + 8)
529 self.width += 4
530 self.height += 4 + (self.op.enter + self.op.restore) * 6
532 def expose(self):
533 da = self.da
534 w = da.backing
535 w.draw_rectangle(da.style.black_gc, False, self.x, self.y, self.width, self.height)
536 pen = da.style.white_gc
537 width = self.width
538 x = self.x
539 y = self.y
541 d = 15 - min(self.depth, 7)
542 pen.set_rgb_fg_color(g.gdk.color_parse('#%x%x%x' % (d, d, d)))
543 w.draw_rectangle(pen, True, self.x + 1, self.y + 1, self.width - 1, self.height - 1)
545 op = self.op
546 if op.foreach:
547 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
548 w.draw_rectangle(pen, True, x + 1, y + 1, 6, self.height - 1)
549 x += 6
550 width -= 6
552 if op.enter:
553 pen.set_rgb_fg_color(g.gdk.color_parse('yellow'))
554 w.draw_rectangle(pen, True, x + 1, y + 1, width - 1, 6)
555 w.draw_rectangle(pen, True, x + 1, y + self.height - 6, width - 1, 6)
556 if op.restore:
557 pen.set_rgb_fg_color(g.gdk.color_parse('orange'))
558 margin = op.enter * 6
559 w.draw_rectangle(pen, True, x + 1, y + 1 + margin, width - 1, 6)
560 w.draw_rectangle(pen, True, x + 1, y + self.height - 6 - margin, width - 1, 6)
562 if self.layout:
563 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
564 w.draw_layout(pen, self.x + self.margin[0], self.y + self.margin[1], self.layout)
566 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
568 w.draw_line(pen, self.x + 1, self.y + 1, self.x + self.width - 2, self.y + 1)
569 w.draw_line(pen, self.x + 1, self.y + 1, self.x + 1, self.y + self.height - 2)
570 self.start.expose()
572 self.draw_link(self.next, 5, self.height, 'black')
573 self.draw_link(self.fail or self.op.propagate_fail, self.width, self.height, 'red')
575 self.draw_breakpoints()
577 def where(self, x, y):
578 if self.depth == 1: return None
580 pos = ChainOp.where(self, x, y)
581 if pos == 'op': pos = None
582 return pos
584 def contains(self, x, y):
585 return x >= self.x and y >= self.y and \
586 x < self.x + self.width and y < self.y + self.height
588 class ChainDisplay(g.EventBox):
589 "A graphical display of a chain of nodes."
590 def __init__(self, view, prog = None):
591 g.EventBox.__init__(self)
592 self.connect('destroy', self.destroyed)
593 self.set_app_paintable(True)
594 self.set_double_buffered(False)
595 self.connect('size-allocate', lambda w, a: self.size_allocate(a))
597 self.view = view
598 self.unset_flags(g.CAN_FOCUS)
600 self.drag_last_pos = None
602 self.exec_point = None # CanvasItem, or None
603 self.rec_point = None
605 self.set_active(1)
607 self.nodes = None
608 self.subs = None
609 self.set_size_request(100, 100)
611 self.prog = None
613 self.view.model.root_program.watchers.append(self)
615 self.connect('expose-event', self.expose)
617 self.drag_box = None
618 self.add_events(g.gdk.BUTTON_PRESS_MASK | g.gdk.BUTTON_RELEASE_MASK |
619 g.gdk.POINTER_MOTION_MASK)
620 self.connect('button-press-event', self.button_press)
621 self.connect('button-release-event', self.button_release)
622 self.hover = None
623 self.connect('motion-notify-event', self.motion)
625 self.switch_to(prog)
627 def set_active(self, active):
628 if mono:
629 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('white'))
630 elif active:
631 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#B3AA73'))
632 else:
633 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#FFC0C0'))
635 def update_points(self):
636 self.queue_draw()
638 if self.rec_point:
639 self.scroll_to_show(self.rec_point)
641 def scroll_to_show(self, item):
642 print "TODO: scroll_to_show"
644 def put_point(self, point):
645 if not point: return
646 w = self.window
647 op, exit = point
648 if op.get_program() != self.prog: return
649 try:
650 obj = self.op_to_object[op]
651 except:
652 print "Can't find %s!\n" % op
653 return
654 x = obj.x
655 y = obj.y
656 size = box_size
657 if point is self.view.rec_point:
658 colour = 'red'
659 else:
660 size -= 4
661 x += 2
662 y += 2
663 colour = 'yellow'
664 if exit == 'fail':
665 x += obj.fail_box[0]
666 y += obj.fail_box[1]
667 else:
668 x += obj.next_box[0]
669 y += obj.next_box[1]
670 pen = self.style.white_gc
671 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
672 w.draw_rectangle(self.style.black_gc, False, x, y, size, size)
673 w.draw_rectangle(pen, True, x + 1, y + 1, size - 1, size - 1)
674 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
676 def destroyed(self, widget):
677 self.view.model.root_program.watchers.remove(self)
679 def switch_to(self, prog):
680 if prog is self.prog:
681 return
682 self.prog = prog
683 self.update_all()
685 def prog_tree_changed(self):
686 pass
688 def program_changed(self, op):
689 if (not op) or op.get_program() == self.prog:
690 self.update_all()
692 def create_op(self, op, x, y):
693 if isinstance(op, Block):
694 return ChainBlock(self, op, x, y)
695 else:
696 return ChainOp(self, op, x, y)
698 def update_all(self):
699 self.op_to_object = {}
700 if self.prog:
701 self.root_object = self.create_op(self.prog.code, 4, 4)
702 self.set_size_request(self.root_object.width + 8, self.root_object.height + 8)
703 else:
704 self.root_object = None
705 self.set_size_request(-1, -1)
706 self.backing = None
707 self.queue_draw()
708 return 1
710 def size_allocate(self, alloc):
711 self.backing = None
712 self.window.clear()
714 def create_backing(self):
715 self.backing = g.gdk.Pixmap(self.window, self.allocation.width, self.allocation.height, -1)
716 self.window.set_back_pixmap(self.backing, False)
717 self.backing.draw_rectangle(self.style.bg_gc[g.STATE_NORMAL], True,
718 0, 0, self.allocation.width, self.allocation.height)
719 if self.root_object:
720 self.root_object.expose()
721 self.window.clear()
722 return
724 def expose(self, da, event):
725 if not self.backing: self.create_backing()
727 self.window.draw_drawable(self.style.white_gc, self.backing, 0, 0, 0, 0, -1, -1)
729 self.put_point(self.view.rec_point)
730 self.put_point(self.view.exec_point)
732 if self.drag_box:
733 pen = self.style.black_gc
734 x = min(self.drag_box[0], self.drag_box[2])
735 y = min(self.drag_box[1], self.drag_box[3])
736 width = abs(self.drag_box[0] - self.drag_box[2])
737 height = abs(self.drag_box[1] - self.drag_box[3])
738 self.window.draw_rectangle(pen, False, int(x), int(y),
739 int(width), int(height))
740 elif self.hover:
741 op, exit = self.hover
742 pen = self.style.black_gc
743 w = self.window
744 if exit == 'fail':
745 w.draw_rectangle(pen, False, op.x + op.fail_box[0], op.y + op.fail_box[1],
746 box_size, box_size)
747 else:
748 w.draw_rectangle(pen, False, op.x + op.next_box[0], op.y + op.next_box[1],
749 box_size, box_size)
751 def motion(self, box, event):
752 if self.drag_box:
753 if (event.x, event.y) == self.drag_box[2:]:
754 return
755 self.drag_box = (self.drag_box[0], self.drag_box[1],
756 event.x, event.y)
757 else:
758 hover = None
759 for op in self.op_to_object.itervalues():
760 pos = op.where(event.x, event.y)
761 if pos in ('next', 'fail'):
762 hover = (op, pos)
763 if hover == self.hover:
764 return
765 self.hover = hover
766 self.queue_draw()
768 def box(self, top, ops):
769 block = top.parent
770 print "Boxed in", block
772 assert ops
774 next = fail = None
776 for op in ops:
777 if op.next and op.next not in ops:
778 if next: rox.alert("New block can't have two next exits!")
779 next = op
780 if op.fail and op.fail not in ops:
781 if fail: rox.alert("New block can't have two fail exits!")
782 fail = op
784 if len(ops) == 1:
785 if not rox.confirm('Put this node in a new block?', 'Create block'):
786 return
787 elif not rox.confirm('Put these %d nodes in a new block?' % len(ops),
788 'Create block'):
789 return
791 new_exits = (next and next.op.next, fail and fail.op.fail)
793 if next: next.op.unlink('next', may_delete = False)
794 if fail: fail.op.unlink('fail', may_delete = False)
796 new = Block(block)
797 prev = top.prev
798 if prev.next == top: exit = 'next'
799 else: exit = 'fail'
800 prev.unlink(exit, may_delete = 0)
801 prev.link_to(new, exit)
803 top.set_parent(None)
804 top.set_parent(new)
805 new.start.link_to(top, 'next')
806 if new_exits[0]: new.link_to(new_exits[0], 'next')
807 if new_exits[1]: new.link_to(new_exits[1], 'fail')
809 def button_release(self, da, event):
810 if event.button != 1 or not self.drag_box:
811 return
813 x1, y1, x2, y2 = self.drag_box
814 if x1 > x2: x1,x2 = x2,x1
815 if y1 > y2: y1,y2 = y2,y1
817 ops = {}
818 top = None
819 for op in self.op_to_object.itervalues():
820 if isinstance(op, ChainBlock): continue
821 if not op.op.prev: continue
822 if op.x + 8 < x1 or op.x > x2: continue
823 if op.y + 8 < y1 or op.y > y2: continue
824 ops[op] = True
825 if top is None or op.y < top.y:
826 top = op
828 self.drag_box = None
829 self.queue_draw()
831 if ops:
832 block = top.op.parent
833 old = ops.copy()
834 for op in old:
835 if op.op.parent != block:
836 del ops[op]
837 print "Nested", op
838 if ops:
839 self.box(top.op, ops)
841 def button_press(self, da, event):
842 for op in self.op_to_object.itervalues():
843 if op.maybe_clicked(event): return
845 if event.type != g.gdk.BUTTON_PRESS or event.button != 1:
846 return
848 self.drag_box = (event.x, event.y, event.x, event.y)
850 def op_colour(self, op):
851 if op in self.view.exec_stack:
852 return 'cyan'
853 return 'blue'
855 def edit_op(self, op):
856 def modify():
857 if op.action[0] == 'do_search' or op.action[0] == 'do_global':
858 t = editables[0].get_text()
859 print "Checking", t
860 from Ft.Xml.XPath import XPathParser
861 if t.find('@CURRENT@') == -1:
862 try:
863 XPathParser.new().parse(t)
864 except:
865 alert('Invalid search pattern!')
866 return
867 i = 0
868 for e in editables:
869 i += 1
870 if e:
871 op.action[i] = e.get_text()
872 op.changed()
873 print "Done editing!"
874 win.destroy()
876 win = g.Dialog()
877 win.vbox.pack_start(g.Label(op.action[0]), TRUE, FALSE, 0)
878 editables = [] # [ Entry | None ]
879 focus = None
880 for x in op.action[1:]:
881 entry = g.Entry()
882 entry.set_text(str(x))
883 win.vbox.pack_start(entry, TRUE, FALSE, 0)
884 if type(x) == str or type(x) == unicode:
885 editables.append(entry)
886 entry.connect('activate', lambda e: modify())
887 if not focus:
888 focus = entry
889 entry.grab_focus()
890 else:
891 entry.set_editable(FALSE)
892 editables.append(None)
894 win.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
895 win.add_button(g.STOCK_OK, g.RESPONSE_OK)
897 def response(box, resp):
898 box.destroy()
899 if resp == g.RESPONSE_OK:
900 modify()
901 win.connect('response', response)
903 if not focus:
904 win.set_response_sensitive(g.RESPONSE_OK, FALSE)
906 win.show_all()
908 def edit_comment(self, block):
909 assert isinstance(block, Block)
911 def set(comment):
912 block.set_comment(comment)
913 GetArg('Comment', set, ['Comment:'],
914 message = '\\n for a newline', init = [block.comment])
916 def block_toggle_enter(self):
917 self.op_menu_op.toggle_enter()
919 def block_toggle_foreach(self):
920 self.op_menu_op.toggle_foreach()
922 def block_toggle_restore(self):
923 self.op_menu_op.toggle_restore()
925 def block_edit_comment(self):
926 self.edit_comment(self.op_menu_op)
928 def op_edit(self):
929 self.edit_op(self.op_menu_op)
931 def op_swap_nf(self):
932 self.op_menu_op.swap_nf()
934 def op_toggle_propagate(self):
935 op = self.op_menu_op
936 op.set_propagate_fail(not op.propagate_fail)
938 def op_del_node(self):
939 op = self.op_menu_op
940 if op.next and op.fail:
941 rox.alert("Can't delete a node with both exits in use")
942 return
943 self.clipboard = op.del_node()
945 def show_op_menu(self, event, op):
946 if op.action[0] == 'Start':
947 self.op_menu_op = op.parent
948 block_menu.popup(self, event)
949 else:
950 self.op_menu_op = op
951 op_menu.popup(self, event)
953 def paste_chain(self, op, exit):
954 print "Paste", self.clipboard
955 doc = self.clipboard
956 new = load(doc.documentElement, op.parent,
957 self.view.model.namespaces)
958 start = new.start.next
959 new.start.unlink('next', may_delete = 0)
960 start.set_parent(None)
961 op.link_to(start, exit)
963 def line_paste_chain(self):
964 op, exit = self.line_menu_line
965 self.paste_chain(op, exit)
967 def line_add_block(self):
968 op, exit = self.line_menu_line
969 box = rox.Dialog()
970 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
971 box.add_button(g.STOCK_ADD, g.RESPONSE_OK)
972 box.set_position(g.WIN_POS_MOUSE)
973 box.set_has_separator(False)
975 foreach = g.CheckButton('Foreach block')
976 box.vbox.pack_start(foreach)
977 enter = g.CheckButton('Enter-leave block')
978 box.vbox.pack_start(enter)
979 box.vbox.show_all()
981 resp = box.run()
982 box.destroy()
983 if resp != g.RESPONSE_OK:
984 return
986 b = Block(op.parent)
987 if foreach.get_active():
988 b.toggle_foreach()
989 if enter.get_active():
990 b.toggle_enter()
991 op.link_to(b, exit)
992 #if self.view.rec_point == (op, exit):
993 self.view.single_step = 1
994 if self.view.rec_point:
995 self.view.stop_recording()
996 self.view.set_exec((op, exit))
997 try:
998 self.view.do_one_step()
999 assert 0
1000 except View.InProgress:
1001 pass
1002 print self.exec_point
1003 self.view.record_at_point()
1004 self.view.breakpoints[(b, 'next')] = True
1006 def line_toggle_breakpoint(self):
1007 op, exit = self.line_menu_line
1008 bp = self.view.breakpoints
1009 if bp.has_key((op, exit)):
1010 del bp[(op, exit)]
1011 else:
1012 bp[(op, exit)] = False
1013 self.prog.changed()
1015 def line_yank_chain(self):
1016 op, exit = self.line_menu_line
1017 next = getattr(op, exit)
1018 if not next:
1019 rox.alert('Nothing to yank!')
1020 return
1021 self.clipboard = next.to_doc()
1022 print self.clipboard
1024 def line_del_chain(self):
1025 op, exit = self.line_menu_line
1026 next = getattr(op, exit)
1027 if not next:
1028 rox.alert('Nothing to delete!')
1029 return
1030 self.clipboard = next.to_doc()
1031 op.unlink(exit)
1033 def show_menu(self, event, op, exit = None):
1034 if exit:
1035 self.line_menu_line = (op, exit)
1036 line_menu.popup(self, event)
1037 else:
1038 self.show_op_menu(event, op)
1041 class ChainWindow(rox.Window):
1042 def __init__(self, view, prog):
1043 rox.Window.__init__(self)
1044 swin = g.ScrolledWindow()
1045 self.add(swin)
1046 disp = ChainDisplay(view, prog)
1047 swin.add(disp)
1049 swin.show_all()
1050 self.disp = disp
1051 self.set_default_size(-1, 200)
1052 self.set_title(prog.name)
1054 def update_points(self):
1055 self.disp.update_points()