Give blocks next and fail boxes.
[dom-editor.git] / Dome / List.py
blob6079e0ea0a309ba0a0381ee0e613000b55e63bdb
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 box_size = 9
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] in ('do_search', 'xpath', 'move_selection'):
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(int(event.x), int(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 def selected_program(self):
340 return self.chains.prog
342 class ChainDummy(g.TreeView):
343 def __init__(self, view, prog = None):
344 g.TreeView.__init__(self)
345 self.prog = prog
346 def switch_to(self, prog):
347 self.prog = prog
348 def update_points(self):
349 pass
351 class ChainNode:
352 "A visual object in the display."
353 def __init__(self, da, x, y):
354 self.x = x
355 self.y = y
356 self.da = da
358 def expose(self):
359 da = self.da
360 w = da.backing
361 w.draw_rectangle(da.style.black_gc, True, self.x, self.y, 10, 10)
363 def maybe_clicked(self, event):
364 return False
366 class ChainOp(ChainNode):
367 next_box = (0, 12)
368 fail_box = (12, 8)
370 def __init__(self, da, op, x, y):
371 self.op = op
372 ChainNode.__init__(self, da, x, y)
374 self.build_leaf()
376 da.op_to_object[op] = self
378 if op.next and op.next.prev[0] == op:
379 self.next = da.create_op(op.next, x, y + self.height + 4)
380 self.total_width = max(self.width, self.next.total_width)
381 else:
382 self.next = None
383 self.total_width = self.width
385 if op.fail and op.fail.prev[0] == op:
386 if op.next:
387 indent = self.total_width + 20
388 else:
389 indent = 100
390 self.fail = da.create_op(op.fail, x + indent, y + self.height + 4)
391 self.total_width = indent + self.fail.total_width
392 else:
393 self.fail = None
395 def build_leaf(self):
396 text = str(action_to_text(self.op.action))
397 self.layout = self.da.create_pango_layout(text)
399 self.width, self.height = self.layout.get_pixel_size()
400 self.width += 12
401 self.height = max(self.height, 20)
403 def expose(self):
404 da = self.da
405 w = da.backing
406 op = self.op
408 w.draw_arc(da.style.white_gc, True, self.x, self.y, 10, 10, 0, 400 * 60)
409 w.draw_arc(da.style.black_gc, False, self.x, self.y, 10, 10, 0, 400 * 60)
410 w.draw_layout(da.style.black_gc, self.x + 12, self.y - 2, self.layout)
412 self.draw_link(self.next, 5, 10, 'black')
413 self.draw_link(self.fail, 5, 12, 'red')
415 if (op, 'next') in self.da.view.breakpoints:
416 w.draw_arc(da.style.black_gc, True,
417 self.x + 2, self.y + 12, 6, 6, 0, 400 * 60)
418 if (op, 'fail') in self.da.view.breakpoints:
419 w.draw_arc(da.style.black_gc, True,
420 self.x + 14, self.y + 10, 6, 6, 0, 400 * 60)
422 def draw_link(self, dest, dx, dy, colour):
423 if not dest: return
425 dest.expose()
426 da = self.da
427 pen = da.style.white_gc
428 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
429 da.backing.draw_line(pen, self.x + dx, self.y + dy, dest.x + 5, self.y + dy)
430 da.backing.draw_line(pen, dest.x + 5, self.y + dy, dest.x + 5, dest.y)
431 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
433 def where(self, x, y):
434 "Identify where (x,y) falls on us -> None, 'op', 'next', 'fail'"
435 x -= self.x
436 if x < 0: return False
437 y -= self.y
438 if y < 0: return False
440 if x >= self.next_box[0] and y >= self.next_box[1] and \
441 x <= self.next_box[0] + box_size and y <= self.next_box[1] + box_size:
442 return 'next'
444 if x >= self.fail_box[0] and y >= self.fail_box[1] and \
445 x <= self.fail_box[0] + box_size and y <= self.fail_box[1] + box_size:
446 return 'fail'
448 if x < self.width and y < self.height: return 'op'
450 def maybe_clicked(self, event):
451 pos = self.where(event.x, event.y)
452 if not pos: return
453 if event.button == 1:
454 if pos in ('next', 'fail'):
455 self.da.view.set_exec((self.op, pos))
456 else:
457 if pos != 'op':
458 self.da.show_menu(event, self.op, pos)
459 else:
460 self.da.show_menu(event, self.op)
461 return True
463 def all_nodes(self):
464 yield self
465 if self.next:
466 for n in self.next.all_nodes(): yield n
467 if self.fail:
468 for n in self.fail.all_nodes(): yield n
471 class ChainBlock(ChainOp):
472 def __init__(self, da, block, x, y):
473 assert isinstance(block, Block)
474 ChainOp.__init__(self, da, block, x, y)
475 self.depth = 1
476 p = block.parent
477 while p and not isinstance(p, Program):
478 p = p.parent
479 self.depth += 1
480 self.next_box = (0, self.height)
481 self.fail_box = (self.width, self.height - box_size)
483 def build_leaf(self):
484 x = self.x
485 y = self.y
487 if self.op.comment:
488 self.layout = self.da.create_pango_layout(self.op.comment.replace('\\n', '\n'))
489 self.width, height = self.layout.get_pixel_size()
490 y += height + 4
491 else:
492 self.layout = None
493 self.width = 40
495 self.margin = (4 + self.op.foreach * 6, 4 + (self.op.enter + self.op.restore) * 6)
496 self.width += self.margin[0]
498 self.start = self.da.create_op(self.op.start, x + self.margin[0], y + self.margin[1])
500 self.height = 20
502 for node in self.start.all_nodes():
503 self.width = max(self.width, node.x + node.width - self.x)
504 self.height = max(self.height, node.y + node.height - self.y)
506 self.width += 4
507 self.height += 4 + (self.op.enter + self.op.restore) * 6
509 def expose(self):
510 da = self.da
511 w = da.backing
512 w.draw_rectangle(da.style.black_gc, False, self.x, self.y, self.width, self.height)
513 pen = da.style.white_gc
514 width = self.width
515 x = self.x
516 y = self.y
518 d = 15 - min(self.depth, 7)
519 pen.set_rgb_fg_color(g.gdk.color_parse('#%x%x%x' % (d, d, d)))
520 w.draw_rectangle(pen, True, self.x + 1, self.y + 1, self.width - 1, self.height - 1)
522 op = self.op
523 if op.foreach:
524 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
525 w.draw_rectangle(pen, True, x + 1, y + 1, 6, self.height - 1)
526 x += 6
527 width -= 6
529 if op.enter:
530 pen.set_rgb_fg_color(g.gdk.color_parse('yellow'))
531 w.draw_rectangle(pen, True, x + 1, y + 1, width - 1, 6)
532 w.draw_rectangle(pen, True, x + 1, y + self.height - 6, width - 1, 6)
533 if op.restore:
534 pen.set_rgb_fg_color(g.gdk.color_parse('orange'))
535 margin = op.enter * 6
536 w.draw_rectangle(pen, True, x + 1, y + 1 + margin, width - 1, 6)
537 w.draw_rectangle(pen, True, x + 1, y + self.height - 6 - margin, width - 1, 6)
539 if self.layout:
540 pen.set_rgb_fg_color(g.gdk.color_parse('blue'))
541 w.draw_layout(pen, self.x + self.margin[0], self.y + self.margin[1], self.layout)
543 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
545 w.draw_line(pen, self.x + 1, self.y + 1, self.x + self.width - 2, self.y + 1)
546 w.draw_line(pen, self.x + 1, self.y + 1, self.x + 1, self.y + self.height - 2)
548 self.start.expose()
550 self.draw_link(self.next, 5, self.height, 'black')
551 self.draw_link(self.fail, self.width, self.height, 'red')
553 def where(self, x, y):
554 if self.depth == 1: return None
556 pos = ChainOp.where(self, x, y)
557 if pos == 'op': pos = None
558 return pos
560 def contains(self, x, y):
561 return x >= self.x and y >= self.y and \
562 x < self.x + self.width and y < self.y + self.height
564 class ChainDisplay(g.EventBox):
565 "A graphical display of a chain of nodes."
566 def __init__(self, view, prog = None):
567 g.EventBox.__init__(self)
568 self.connect('destroy', self.destroyed)
569 self.set_app_paintable(True)
570 self.set_double_buffered(False)
571 self.connect('size-allocate', lambda w, a: self.size_allocate(a))
573 self.view = view
574 self.unset_flags(g.CAN_FOCUS)
576 self.drag_last_pos = None
578 self.exec_point = None # CanvasItem, or None
579 self.rec_point = None
581 self.set_active(1)
583 self.nodes = None
584 self.subs = None
585 self.set_size_request(100, 100)
587 self.prog = None
589 self.view.model.root_program.watchers.append(self)
591 self.connect('expose-event', self.expose)
593 self.drag_box = None
594 self.add_events(g.gdk.BUTTON_PRESS_MASK | g.gdk.BUTTON_RELEASE_MASK |
595 g.gdk.POINTER_MOTION_MASK)
596 self.connect('button-press-event', self.button_press)
597 self.connect('button-release-event', self.button_release)
598 self.hover = None
599 self.connect('motion-notify-event', self.motion)
601 self.switch_to(prog)
603 def set_active(self, active):
604 if mono:
605 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('white'))
606 elif active:
607 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#B3AA73'))
608 else:
609 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#FFC0C0'))
611 def update_points(self):
612 self.queue_draw()
614 if self.rec_point:
615 self.scroll_to_show(self.rec_point)
617 def scroll_to_show(self, item):
618 print "TODO: scroll_to_show"
620 def put_point(self, point):
621 if not point: return
622 w = self.window
623 op, exit = point
624 if op.get_program() != self.prog: return
625 try:
626 obj = self.op_to_object[op]
627 except:
628 print "Can't find %s!\n" % op
629 return
630 x = obj.x
631 y = obj.y
632 size = box_size
633 if point is self.view.rec_point:
634 colour = 'red'
635 else:
636 size -= 4
637 x += 2
638 y += 2
639 colour = 'yellow'
640 if exit == 'fail':
641 x += obj.fail_box[0]
642 y += obj.fail_box[1]
643 else:
644 x += obj.next_box[0]
645 y += obj.next_box[1]
646 pen = self.style.white_gc
647 pen.set_rgb_fg_color(g.gdk.color_parse(colour))
648 w.draw_rectangle(self.style.black_gc, False, x, y, size, size)
649 w.draw_rectangle(pen, True, x + 1, y + 1, size - 1, size - 1)
650 pen.set_rgb_fg_color(g.gdk.color_parse('white'))
652 def destroyed(self, widget):
653 self.view.model.root_program.watchers.remove(self)
655 def switch_to(self, prog):
656 if prog is self.prog:
657 return
658 self.prog = prog
659 self.update_all()
661 def prog_tree_changed(self):
662 pass
664 def program_changed(self, op):
665 if (not op) or op.get_program() == self.prog:
666 self.update_all()
668 def create_op(self, op, x, y):
669 if isinstance(op, Block):
670 return ChainBlock(self, op, x, y)
671 else:
672 return ChainOp(self, op, x, y)
674 def update_all(self):
675 self.op_to_object = {}
676 if self.prog:
677 self.root_object = self.create_op(self.prog.code, 4, 4)
678 self.set_size_request(self.root_object.width + 8, self.root_object.height + 8)
679 else:
680 self.root_object = None
681 self.set_size_request(-1, -1)
682 self.backing = None
683 self.queue_draw()
684 return 1
686 def size_allocate(self, alloc):
687 self.backing = None
688 self.window.clear()
690 def create_backing(self):
691 self.backing = g.gdk.Pixmap(self.window, self.allocation.width, self.allocation.height, -1)
692 self.window.set_back_pixmap(self.backing, False)
693 self.backing.draw_rectangle(self.style.bg_gc[g.STATE_NORMAL], True,
694 0, 0, self.allocation.width, self.allocation.height)
695 if self.root_object:
696 self.root_object.expose()
697 self.window.clear()
698 return
700 def expose(self, da, event):
701 if not self.backing: self.create_backing()
703 self.window.draw_drawable(self.style.white_gc, self.backing, 0, 0, 0, 0, -1, -1)
705 self.put_point(self.view.rec_point)
706 self.put_point(self.view.exec_point)
708 if self.drag_box:
709 pen = self.style.black_gc
710 x = min(self.drag_box[0], self.drag_box[2])
711 y = min(self.drag_box[1], self.drag_box[3])
712 width = abs(self.drag_box[0] - self.drag_box[2])
713 height = abs(self.drag_box[1] - self.drag_box[3])
714 self.window.draw_rectangle(pen, False, int(x), int(y),
715 int(width), int(height))
716 elif self.hover:
717 op, exit = self.hover
718 pen = self.style.black_gc
719 w = self.window
720 if exit == 'fail':
721 w.draw_rectangle(pen, False, op.x + op.fail_box[0], op.y + op.fail_box[1],
722 box_size, box_size)
723 else:
724 w.draw_rectangle(pen, False, op.x + op.next_box[0], op.y + op.next_box[1],
725 box_size, box_size)
727 def motion(self, box, event):
728 if self.drag_box:
729 if (event.x, event.y) == self.drag_box[2:]:
730 return
731 self.drag_box = (self.drag_box[0], self.drag_box[1],
732 event.x, event.y)
733 else:
734 hover = None
735 for op in self.op_to_object.itervalues():
736 pos = op.where(event.x, event.y)
737 if pos in ('next', 'fail'):
738 hover = (op, pos)
739 if hover == self.hover:
740 return
741 self.hover = hover
742 self.queue_draw()
744 def box(self, top, ops):
745 block = top.parent
746 print "Boxed in", block
748 assert ops
750 next = fail = None
752 for op in ops:
753 if op.next and op.next not in ops:
754 if next: rox.alert("New block can't have two next exits!")
755 next = op
756 if op.fail and op.fail not in ops:
757 if fail: rox.alert("New block can't have two fail exits!")
758 fail = op
760 if len(ops) == 1:
761 if not rox.confirm('Put this node in a new block?', 'Create block'):
762 return
763 elif not rox.confirm('Put these %d nodes in a new block?' % len(ops),
764 'Create block'):
765 return
767 new_exits = (next and next.op.next, fail and fail.op.fail)
769 if next: next.op.unlink('next', may_delete = False)
770 if fail: fail.op.unlink('fail', may_delete = False)
772 new = Block(block)
773 prev = top.prev[0]
774 if prev.next == top: exit = 'next'
775 else: exit = 'fail'
776 prev.unlink(exit, may_delete = 0)
777 prev.link_to(new, exit)
779 top.set_parent(None)
780 top.set_parent(new)
781 new.start.link_to(top, 'next')
782 if new_exits[0]: new.link_to(new_exits[0], 'next')
783 if new_exits[1]: new.link_to(new_exits[1], 'fail')
785 def button_release(self, da, event):
786 if event.button != 1 or not self.drag_box:
787 return
789 x1, y1, x2, y2 = self.drag_box
790 if x1 > x2: x1,x2 = x2,x1
791 if y1 > y2: y1,y2 = y2,y1
793 ops = {}
794 top = None
795 for op in self.op_to_object.itervalues():
796 if isinstance(op, ChainBlock): continue
797 if not op.op.prev: continue
798 if op.x + 8 < x1 or op.x > x2: continue
799 if op.y + 8 < y1 or op.y > y2: continue
800 ops[op] = True
801 if top is None or op.y < top.y:
802 top = op
804 self.drag_box = None
805 self.queue_draw()
807 if ops:
808 block = top.op.parent
809 old = ops.copy()
810 for op in old:
811 if op.op.parent != block:
812 del ops[op]
813 print "Nested", op
814 if ops:
815 self.box(top.op, ops)
817 def button_press(self, da, event):
818 for op in self.op_to_object.itervalues():
819 if op.maybe_clicked(event): return
821 if event.type != g.gdk.BUTTON_PRESS or event.button != 1:
822 return
824 self.drag_box = (event.x, event.y, event.x, event.y)
826 def op_colour(self, op):
827 if op in self.view.exec_stack:
828 return 'cyan'
829 return 'blue'
831 def edit_op(self, op):
832 def modify():
833 if op.action[0] == 'do_search' or op.action[0] == 'do_global':
834 t = editables[0].get_text()
835 print "Checking", t
836 from Ft.Xml.XPath import XPathParser
837 if t.find('@CURRENT@') == -1:
838 try:
839 XPathParser.new().parse(t)
840 except:
841 alert('Invalid search pattern!')
842 return
843 i = 0
844 for e in editables:
845 i += 1
846 if e:
847 op.action[i] = e.get_text()
848 op.changed()
849 print "Done editing!"
850 win.destroy()
852 win = g.Dialog()
853 win.vbox.pack_start(g.Label(op.action[0]), TRUE, FALSE, 0)
854 editables = [] # [ Entry | None ]
855 focus = None
856 for x in op.action[1:]:
857 entry = g.Entry()
858 entry.set_text(str(x))
859 win.vbox.pack_start(entry, TRUE, FALSE, 0)
860 if type(x) == str or type(x) == unicode:
861 editables.append(entry)
862 entry.connect('activate', lambda e: modify())
863 if not focus:
864 focus = entry
865 entry.grab_focus()
866 else:
867 entry.set_editable(FALSE)
868 editables.append(None)
870 win.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
871 win.add_button(g.STOCK_OK, g.RESPONSE_OK)
873 def response(box, resp):
874 box.destroy()
875 if resp == g.RESPONSE_OK:
876 modify()
877 win.connect('response', response)
879 if not focus:
880 win.set_response_sensitive(g.RESPONSE_OK, FALSE)
882 win.show_all()
884 def edit_comment(self, block):
885 assert isinstance(block, Block)
887 def set(comment):
888 block.set_comment(comment)
889 GetArg('Comment', set, ['Comment:'],
890 message = '\\n for a newline', init = [block.comment])
892 def block_toggle_enter(self):
893 self.op_menu_op.toggle_enter()
895 def block_toggle_foreach(self):
896 self.op_menu_op.toggle_foreach()
898 def block_toggle_restore(self):
899 self.op_menu_op.toggle_restore()
901 def block_edit_comment(self):
902 self.edit_comment(self.op_menu_op)
904 def op_edit(self):
905 self.edit_op(self.op_menu_op)
907 def op_swap_nf(self):
908 self.op_menu_op.swap_nf()
910 def op_del_node(self):
911 op = self.op_menu_op
912 if op.next and op.fail:
913 rox.alert("Can't delete a node with both exits in use")
914 return
915 self.clipboard = op.del_node()
917 def show_op_menu(self, event, op):
918 if op.action[0] == 'Start':
919 self.op_menu_op = op.parent
920 block_menu.popup(self, event)
921 else:
922 self.op_menu_op = op
923 op_menu.popup(self, event)
925 def paste_chain(self, op, exit):
926 print "Paste", self.clipboard
927 doc = self.clipboard
928 new = load(doc.documentElement, op.parent,
929 self.view.model.namespaces)
930 start = new.start.next
931 new.start.unlink('next', may_delete = 0)
932 start.set_parent(None)
933 op.link_to(start, exit)
935 def line_paste_chain(self):
936 op, exit = self.line_menu_line
937 self.paste_chain(op, exit)
939 def line_add_block(self):
940 op, exit = self.line_menu_line
941 box = rox.Dialog()
942 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
943 box.add_button(g.STOCK_ADD, g.RESPONSE_OK)
944 box.set_position(g.WIN_POS_MOUSE)
945 box.set_has_separator(False)
947 foreach = g.CheckButton('Foreach block')
948 box.vbox.pack_start(foreach)
949 enter = g.CheckButton('Enter-leave block')
950 box.vbox.pack_start(enter)
951 box.vbox.show_all()
953 resp = box.run()
954 box.destroy()
955 if resp != g.RESPONSE_OK:
956 return
958 b = Block(op.parent)
959 if foreach.get_active():
960 b.toggle_foreach()
961 if enter.get_active():
962 b.toggle_enter()
963 op.link_to(b, exit)
964 #if self.view.rec_point == (op, exit):
965 self.view.single_step = 1
966 if self.view.rec_point:
967 self.view.stop_recording()
968 self.view.set_exec((op, exit))
969 try:
970 self.view.do_one_step()
971 assert 0
972 except View.InProgress:
973 pass
974 print self.exec_point
975 self.view.record_at_point()
977 def line_toggle_breakpoint(self):
978 op, exit = self.line_menu_line
979 bp = self.view.breakpoints
980 if bp.has_key((op, exit)):
981 del bp[(op, exit)]
982 else:
983 bp[(op, exit)] = 1
984 self.prog.changed()
986 def line_yank_chain(self):
987 op, exit = self.line_menu_line
988 next = getattr(op, exit)
989 if not next:
990 rox.alert('Nothing to yank!')
991 return
992 self.clipboard = next.to_doc()
993 print self.clipboard
995 def line_del_chain(self):
996 op, exit = self.line_menu_line
997 next = getattr(op, exit)
998 if not next:
999 rox.alert('Nothing to delete!')
1000 return
1001 self.clipboard = next.to_doc()
1002 op.unlink(exit)
1004 def show_menu(self, event, op, exit = None):
1005 if exit:
1006 self.line_menu_line = (op, exit)
1007 line_menu.popup(self, event)
1008 else:
1009 self.show_op_menu(event, op)
1012 class ChainWindow(rox.Window):
1013 def __init__(self, view, prog):
1014 rox.Window.__init__(self)
1015 swin = g.ScrolledWindow()
1016 self.add(swin)
1017 disp = ChainDisplay(view, prog)
1018 swin.add(disp)
1020 swin.show_all()
1021 self.disp = disp
1022 self.set_default_size(-1, 200)
1023 self.set_title(prog.name)
1025 def update_points(self):
1026 self.disp.update_points()