Started rewriting ChainDisplay to not use gnome canvas.
[dom-editor.git] / Dome / List.py
blob3d81929abecf7ebf791d6cf14ebac6c1e4d68841
1 import rox
2 from rox import g, TRUE, FALSE, alert
3 from gnome import canvas
5 from support import *
6 import string
7 from StringIO import StringIO
8 import math
9 import View
11 from rox.Menu import Menu
13 import __main__
14 mono = __main__.mono
16 prog_menu = Menu('programs', [
17 ('/Play', 'menu_play', '', ''),
18 ('/Map', 'menu_map', '', ''),
19 ('/View', 'menu_new_view', '', ''),
20 ('/', '', '', '<separator>'),
21 ('/New program', 'menu_new_prog', '', ''),
22 ('/Rename', 'menu_rename', '', ''),
23 ('/Delete', 'menu_delete', '', ''),
26 line_menu = Menu('line', [
27 ('/Set\/clear breakpoint', 'line_toggle_breakpoint', '', ''),
28 ('/Yank chain', 'line_yank_chain', '', ''),
29 ('/Remove link', 'line_del_chain', '', ''),
30 ('/Paste chain', 'line_paste_chain', '', ''),
31 ('/Add block', 'line_add_block', '', '')
34 block_menu = Menu('op', [
35 ('/Toggle Enter\/Leave', 'block_toggle_enter', '', ''),
36 ('/Toggle Foreach','block_toggle_foreach', '', ''),
37 ('/Toggle Restore Mark','block_toggle_restore', '', ''),
38 ('/Edit comment', 'block_edit_comment', '', ''),
39 ('/Swap next\/fail', 'op_swap_nf', '', ''),
40 ('/Remove node', 'op_del_node', '', '')
43 op_menu = Menu('op', [
44 ('/Edit node', 'op_edit', '', ''),
45 ('/Swap next\/fail', 'op_swap_nf', '', ''),
46 ('/Remove node', 'op_del_node', '', '')
49 from GetArg import GetArg
50 from Program import Program, load, Block
52 no_cursor = g.gdk.Cursor(g.gdk.TCROSS)
54 def trunc(text):
55 if len(text) < 28:
56 return text
57 return text[:26] + '...'
59 def connect(x1, y1, x2, y2):
60 """Chop 5 pixels off both ends of this line"""
61 gap = 5.0
62 dx = x2 - x1
63 dy = y2 - y1
64 l = math.hypot(dx, dy)
65 if l:
66 dx *= gap / l
67 dy *= gap / l
68 return (x1 + dx, y1 + dy, x2 - dx, y2 - dy)
70 DEFAULT_NEXT = (0, 25)
71 DEFAULT_FAIL = (20, 20)
73 expand_history = {} # Prog name -> expanded flag
75 def action_to_text(action):
76 text = action[0]
77 if text[:3] == 'do_':
78 text = text[3:]
79 text = string.capitalize(string.replace(text, '_', ' '))
80 if text == 'Global':
81 text = 'Select nodes'
83 if len(action) > 1:
84 if action[0] == 'do_search' or action[0] == 'xpath':
85 pat = str(action[1])
86 pat = string.replace(pat, 'following-sibling::', '>>')
87 pat = string.replace(pat, 'preceding-sibling::', '<<')
88 pat = string.replace(pat, 'child::', '')
89 pat = string.replace(pat, '[1]', '')
90 pat = string.replace(pat, 'text()[ext:match', '[')
91 details = ''
92 while len(pat) > 20:
93 i = string.rfind(pat[:20], '/')
94 if i == -1:
95 i = string.rfind(pat[:20], ':')
96 if i == -1:
97 i = 20
98 details = details + pat[:i + 1] + '\n'
99 pat = pat[i + 1:]
100 details = details + pat
101 elif action[0] == 'attribute':
102 details = trunc(str(action[2]))
103 elif action[0] == 'set_attrib':
104 details = trunc(str(action[1]))
105 elif action[0] == 'add_attrib':
106 details = trunc(str(action[2]))
107 elif action[0] == 'add_node':
108 details = trunc(action[2])
109 elif action[0] == 'subst':
110 details = action[1] + ' -> ' + action[2]
111 elif action[0] == 'play' or action[0] == 'map':
112 if len(action[1]) > 20:
113 details = '...' + str(action[1][-19:])
114 else:
115 details = str(action[1])
116 else:
117 if len(action) > 2:
118 details = `action[1:]`
119 else:
120 details = str(action[1])
121 if len(details) > 20:
122 details = trunc(`details`)
123 text = text + '\n' + details
124 return text
126 class List(g.VBox):
127 def __init__(self, view):
128 g.VBox.__init__(self)
130 def destroyed(widget):
131 print "List destroy!!"
132 sel.disconnect(self.sel_changed_signal)
133 self.view.lists.remove(self)
134 self.view.model.root_program.watchers.remove(self)
135 self.connect('destroy', destroyed)
137 self.view = view
138 self.sub_windows = []
140 self.stack_frames = g.Label('')
141 self.pack_start(self.stack_frames, FALSE, TRUE, 0)
142 self.stack_frames.show()
143 self.update_stack(None)
145 pane = g.VPaned()
146 self.pack_start(pane, expand = 1, fill = 1)
148 swin = g.ScrolledWindow()
149 swin.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
150 pane.add1(swin)
151 self.prog_model = g.TreeStore(str, str)
152 tree = g.TreeView(self.prog_model)
153 tree.connect('button-press-event', self.button_press)
154 tree.unset_flags(g.CAN_FOCUS)
155 tree.set_headers_visible(FALSE)
156 self.tree = tree
158 cell = g.CellRendererText()
159 column = g.TreeViewColumn('Program', cell, text = 0)
160 tree.append_column(column)
162 sel = tree.get_selection()
163 # Doesn't get destroyed, so record signal number
164 self.sel_changed_signal = sel.connect('changed', self.change_prog)
166 self.chains = ChainDisplay(view)
167 self.prog_tree_changed()
168 v = g.Viewport()
169 v.add(tree)
170 swin.add(v)
171 v.set_shadow_type(g.SHADOW_NONE)
172 v.show_all()
174 swin = g.ScrolledWindow()
175 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
176 pane.add2(swin)
177 swin.add_with_viewport(self.chains)
178 swin.show_all()
180 pane.set_position(200)
182 sel.set_mode(g.SELECTION_BROWSE)
183 root_iter = self.prog_model.get_iter_first()
184 sel.select_iter(root_iter)
185 tree.expand_row(self.prog_model.get_path(root_iter), FALSE)
186 tree.show()
187 self.view.lists.append(self)
188 self.view.model.root_program.watchers.append(self)
190 def change_prog(self, sel):
191 selected = sel.get_selected()
192 if not selected:
193 return
194 model, iter = selected
195 if iter:
196 path = model.get_value(iter, 1)
197 self.chains.switch_to(self.view.name_to_prog(path))
198 else:
199 self.chains.switch_to(None)
201 def set_innermost_failure(self, op):
202 prog = op.get_program()
203 print "list: set_innermost_failure:", prog
204 self.show_prog(prog)
206 def update_points(self):
207 self.chains.update_points()
208 for x in self.sub_windows:
209 x.update_points()
211 def program_changed(self, op):
212 pass
214 def prog_tree_changed(self):
215 self.prog_to_path = {}
216 self.prog_model.clear()
217 self.build_tree(self.view.model.root_program)
219 # Check for now deleted programs still being displayed
220 root = self.view.model.root_program
221 if self.chains and self.chains.prog and not self.chains.prog.parent:
222 self.chains.switch_to(None)
223 for x in self.sub_windows:
224 if x.disp.prog is not root and not x.disp.prog.parent:
225 x.destroy()
227 def build_tree(self, prog, iter = None):
228 child_iter = self.prog_model.append(iter)
229 self.prog_model.set(child_iter, 0, prog.name,
230 1, prog.get_path())
232 self.prog_to_path[prog] = self.prog_model.get_path(child_iter)
233 for p in prog.subprograms.values():
234 self.build_tree(p, child_iter)
236 def run_return(self, exit):
237 if exit != 'next':
238 print "run_return: failure!"
239 self.view.jump_to_innermost_failure()
240 def cb(choice, self = self):
241 if choice == 0:
242 self.view.record_at_point()
243 if rox.confirm("Program failed - record a failure case?", g.STOCK_NO, 'Record'):
244 self.view.record_at_point()
245 print "List: execution done!"
247 def button_press(self, tree, event):
248 if event.button == 2 or event.button == 3:
249 ret = tree.get_path_at_pos(event.x, event.y)
250 if not ret:
251 return 1 # Click on blank area
252 path, col, cx, cy = ret
253 print "Event on", path
254 iter = self.prog_model.get_iter(path)
255 path = self.prog_model.get_value(iter, 1)
256 if event.button == 3:
257 prog = self.view.name_to_prog(path)
258 self.show_menu(event, prog)
259 else:
260 self.view.run_new(self.run_return)
261 if event.state & g.gdk.SHIFT_MASK:
262 self.view.may_record(['map', path])
263 else:
264 self.view.may_record(['play', path])
265 return 0
267 def menu_delete(self):
268 prog = self.prog_menu_prog
269 if not prog.parent:
270 rox.alert("Can't delete the root program!")
271 return
272 prog.parent.remove_sub(prog)
274 def menu_rename(self):
275 prog = self.prog_menu_prog
276 def rename(name, prog = prog):
277 prog.rename(name)
278 GetArg('Rename program', rename, ['Program name:'])
280 def menu_new_prog(self):
281 prog = self.prog_menu_prog
282 def create(name):
283 new = Program(name)
284 prog.add_sub(new)
285 GetArg('New program', create, ['Program name:'])
287 def menu_new_view(self):
288 prog = self.prog_menu_prog
289 cw = ChainWindow(self.view, prog)
290 cw.show()
291 self.sub_windows.append(cw)
292 def lost_cw(win):
293 self.sub_windows.remove(cw)
294 print "closed"
295 cw.connect('destroy', lost_cw)
297 def menu_map(self):
298 prog = self.prog_menu_prog
299 self.view.run_new(self.run_return)
300 self.view.may_record(['map', prog.get_path()])
302 def menu_play(self):
303 prog = self.prog_menu_prog
304 self.view.run_new(self.run_return)
305 self.view.may_record(['play', prog.get_path()])
307 def show_menu(self, event, prog):
308 self.prog_menu_prog = prog
309 prog_menu.popup(self, event)
311 def update_stack(self, op):
312 "The stack has changed - redraw 'op'"
313 if op and op.get_program() == self.chains.prog:
314 self.chains.update_all()
315 l = len(self.view.exec_stack) + len(self.view.foreach_stack)
316 if l == 0:
317 text = 'No stack'
318 elif l == 1:
319 text = '1 frame'
320 else:
321 text = '%d frames' % l
322 if self.view.chroots:
323 text += ' (%d enters)' % len(self.view.chroots)
324 self.stack_frames.set_text(text)
326 def show_prog(self, prog):
327 path = self.prog_to_path[prog]
328 partial = []
329 for p in path[:-1]:
330 partial.append(p)
331 self.tree.expand_row(tuple(partial), FALSE)
332 iter = self.prog_model.get_iter(path)
333 self.tree.get_selection().select_iter(iter)
335 class ChainDummy(g.TreeView):
336 def __init__(self, view, prog = None):
337 g.TreeView.__init__(self)
338 self.prog = prog
339 def switch_to(self, prog):
340 self.prog = prog
341 def update_points(self):
342 pass
344 def create_op(op, x, y):
345 if isinstance(op, Block):
346 return ChainBlock(op, x, y)
347 else:
348 return ChainOp(op, x, y)
350 class ChainNode:
351 "A visual object in the display."
352 def __init__(self, x, y):
353 self.x = x
354 self.y = y
356 def expose(self, da):
357 w = da.window
358 print "draw"
359 w.draw_rectangle(da.style.black_gc, True, self.x, self.y, 10, 10)
361 class ChainOp(ChainNode):
362 def __init__(self, op, x, y):
363 self.op = op
364 ChainNode.__init__(self, x, y)
365 self.layout = None
367 if op.next: self.next = create_op(op.next, x, y + 20)
368 else: self.next = None
370 if op.fail: self.fail = create_op(op.fail, x + 100, y + 20)
371 else: self.fail = None
373 def expose(self, da):
374 w = da.window
375 op = self.op
376 if not self.layout:
377 text = str(action_to_text(op.action))
378 self.layout = da.create_pango_layout(text)
380 w.draw_arc(da.style.black_gc, False, self.x, self.y, 10, 10, 0, 360 * 60)
381 w.draw_layout(da.style.black_gc, self.x + 12, self.y, self.layout)
383 if self.next: self.next.expose(da)
384 if self.fail: self.fail.expose(da)
385 return
387 text_font = 'verdana 10'
388 text_col = 'black'
389 if op.action[0] == 'Start':
390 text = str(op.parent.comment.replace('\\n', '\n'))
391 text_y = 0
392 if mono:
393 text_font = 'verdana bold 10'
394 else:
395 text_col = 'dark blue'
396 else:
397 text = str(action_to_text(op.action))
398 text_y = -8
400 group.ellipse = group.add(canvas.CanvasEllipse,
401 fill_color = self.op_colour(op),
402 outline_color = 'black',
403 x1 = -4, x2 = 4,
404 y1 = -4, y2 = 4,
405 width_pixels = 1)
406 group.ellipse.connect('event', self.op_event, op)
407 if text:
408 label = group.add(canvas.CanvasText,
409 x = -8,
410 y = text_y,
411 anchor = g.ANCHOR_NE,
412 justification = 'right',
413 fill_color = text_col,
414 font = text_font,
415 text = text)
417 #self.update_now() # GnomeCanvas bug?
418 (lx, ly, hx, hy) = label.get_bounds()
419 next_off_y = hy
420 else:
421 next_off_y = 0
422 group.width, group.height = 0, 0
424 class ChainBlock(ChainOp):
425 def __init__(self, block, x, y):
426 assert isinstance(block, Block)
427 ChainOp.__init__(self, block, x, y)
429 self.width = 100
430 self.height = 100
432 self.start = create_op(block.start, x + 4, y + 4)
434 def expose(self, da):
435 w = da.window
436 w.draw_rectangle(da.style.black_gc, False, self.x, self.y, self.width, self.height)
438 op = self.op
439 if op.foreach:
440 w.draw_rectangle(da.style.white_gc, True, self.x + 1, self.y + 1, 6, self.height - 2)
441 if op.enter:
442 w.draw_rectangle(da.style.white_gc, True, self.x + 1, self.y + 1, self.width, 6)
443 w.draw_rectangle(da.style.white_gc, True, self.x + 1, self.y + self.height - 5, 6)
444 if op.restore:
445 colour = 'orange'
446 margin = op.enter * 8
447 w.draw_rectangle(da.style.white_gc, True, self.x + 1, self.y + 1 + margin, self.width, 6)
448 w.draw_rectangle(da.style.white_gc, True, self.x + 1, self.y + self.height - 5 - margin, 6)
450 self.start.expose(da)
452 class ChainDisplay(g.DrawingArea):
453 "A graphical display of a chain of nodes."
454 def __init__(self, view, prog = None):
455 g.DrawingArea.__init__(self)
456 self.connect('destroy', self.destroyed)
458 self.view = view
459 self.unset_flags(g.CAN_FOCUS)
461 self.drag_last_pos = None
463 self.exec_point = None # CanvasItem, or None
464 self.rec_point = None
466 self.set_active(1)
468 self.nodes = None
469 self.subs = None
470 self.set_size_request(100, 100)
472 self.prog = None
474 self.view.model.root_program.watchers.append(self)
476 self.connect('expose-event', self.expose)
477 self.add_events(g.gdk.BUTTON_PRESS_MASK)
478 self.connect('button-press-event', self.button_press)
480 self.switch_to(prog)
482 def set_active(self, active):
483 if mono:
484 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('white'))
485 elif active:
486 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#F7F7F7'))
487 else:
488 self.modify_bg(g.STATE_NORMAL, g.gdk.color_parse('#FFC0C0'))
490 def update_points(self):
491 self.put_point('rec_point')
492 self.put_point('exec_point')
494 if self.rec_point:
495 self.scroll_to_show(self.rec_point)
497 def scroll_to_show(self, item):
498 print "XXX"
499 return
501 (lx, ly, hx, hy) = item.get_bounds()
502 x, y = item.i2w(0, 0)
503 x, y = self.w2c(x, y)
504 lx += x
505 ly += y
506 hx += x
507 hy += y
508 lx -= 16
510 sx, sy = self.get_scroll_offsets()
511 if lx < sx:
512 sx = lx
513 if ly < sy:
514 sy = ly
516 (x, y, w, h) = self.get_allocation()
517 hx -= w
518 hy -= h
520 if hx > sx:
521 sx = hx
522 if hy > sy:
523 sy = hy
525 self.scroll_to(sx, sy)
527 def put_point(self, point):
528 print "XXX"; return
530 item = getattr(self, point)
531 if item:
532 item.destroy()
533 setattr(self, point, None)
535 if not self.prog:
536 return
538 opexit = getattr(self.view, point)
539 if point == 'exec_point' and self.view.op_in_progress:
540 opexit = (self.view.op_in_progress, None)
541 if opexit:
542 g = None
543 (op, exit) = opexit
544 if op.get_program() != self.prog:
545 return
546 try:
547 g = self.op_to_group[op]
548 except KeyError:
549 pass
550 if point == 'rec_point':
551 c = 'red'
552 s = 6
553 else:
554 c = 'yellow'
555 s = 3
556 item = self.root().add(canvas.CanvasRect,
557 x1 = -s, x2 = s, y1 = -s, y2 = s,
558 fill_color = c,
559 outline_color = 'black', width_pixels = 1)
560 setattr(self, point, item)
561 item.connect('event', self.line_event, op, exit)
563 if g and exit:
564 # TODO: cope with exit == None
565 x1, y1, x2, y2 = self.get_arrow_ends(op, exit)
566 x1, y1 = g.i2w(x1, y1)
567 x2, y2 = g.i2w(x2, y2)
568 item.move((x1 + x2) / 2, (y1 + y2) / 2)
570 def destroyed(self, widget):
571 self.view.model.root_program.watchers.remove(self)
572 print "(ChainDisplay destroyed)"
574 def switch_to(self, prog):
575 if prog is self.prog:
576 return
577 self.prog = prog
578 self.update_all()
580 def prog_tree_changed(self):
581 pass
583 def program_changed(self, op):
584 if (not op) or op.get_program() == self.prog:
585 self.update_all()
587 def update_all(self):
588 self.op_to_obj = {}
589 if self.prog:
590 self.root_object = create_op(self.prog.code, 4, 4)
591 else:
592 self.root_object = None
593 self.queue_draw()
594 return 1
596 def expose(self, da, event):
597 print "expose"
598 if self.root_object:
599 self.root_object.expose(da)
600 #self.update_links()
601 #self.update_points()
603 #self.set_bounds()
605 def button_press(self, da, event):
606 print "click"
607 self.view.set_exec((self.root_object.op.start, 'next'))
609 def op_colour(self, op):
610 if op in self.view.exec_stack:
611 return 'cyan'
612 return 'blue'
614 def update_links(self, op = None):
615 """Walk through all nodes in the tree-version of the op graph,
616 making all the links (which already exist as stubs) point to
617 the right place."""
618 if not self.prog:
619 return
620 if not op:
621 op = self.prog.code
622 if op.next:
623 if op.next.prev[0] == op:
624 self.update_links(op.next)
625 else:
626 self.join_nodes(op, 'next')
627 if op.fail:
628 if op.fail.prev[0] == op:
629 self.update_links(op.fail)
630 else:
631 self.join_nodes(op, 'fail')
632 if isinstance(op, Block):
633 self.update_links(op.start)
635 def create_node(self, op, parent):
636 if op.is_toplevel():
637 return obj
638 return
640 if op.next and op.next.prev[0] == op:
641 sx, sy = self.get_arrow_start(op, 'next')
642 gr = group.add(canvas.CanvasGroup, x = 0, y = 0)
643 self.create_node(op.next, gr)
644 #self.update_now() # GnomeCanvas bug?
645 (lx, ly, hx, hy) = gr.get_bounds()
646 drop = max(20, next_off_y + 10)
647 y = drop - ly
648 next = op.next
649 while isinstance(next, Block):
650 next = next.start
651 x = next.dx
652 y += next.dy
653 gr.move(sx + x, sy + y)
655 group.next_line = group.add(canvas.CanvasLine,
656 fill_color = 'black',
657 points = connect(0, 0, 1, 1),
658 width_pixels = 4,
659 last_arrowhead = 1,
660 arrow_shape_a = 5,
661 arrow_shape_b = 5,
662 arrow_shape_c = 5)
663 group.next_line.connect('event', self.line_event, op, 'next')
665 (x, y) = DEFAULT_FAIL
666 if op.fail and op.fail.prev[0] == op:
667 sx, sy = self.get_arrow_start(op, 'fail')
668 y = 46
669 gr = group.add(canvas.CanvasGroup, x = 0, y = 0)
670 self.create_node(op.fail, gr)
671 #self.update_now() # GnomeCanvas bug?
672 (lx, ly, hx, hy) = gr.get_bounds()
673 x = 20 - lx
674 fail = op.fail
675 while isinstance(fail, Block):
676 fail = fail.start
677 x += fail.dx
678 y += fail.dy
679 gr.move(sx + x, sy + y)
680 group.fail_line = group.add(canvas.CanvasLine,
681 fill_color = '#ff6666',
682 points = connect(0, 0, 1, 1),
683 width_pixels = 4,
684 last_arrowhead = 1,
685 arrow_shape_a = 5,
686 arrow_shape_b = 5,
687 arrow_shape_c = 5)
688 group.fail_line.lower_to_bottom()
689 group.fail_line.connect('event', self.line_event, op, 'fail')
690 if op.action[0] == 'Start':
691 group.fail_line.hide()
693 self.join_nodes(op, 'next')
694 self.join_nodes(op, 'fail')
696 if self.view.breakpoints.has_key((op, 'next')):
697 group.next_line.set(line_style = g.gdk.LINE_ON_OFF_DASH)
698 if self.view.breakpoints.has_key((op, 'fail')):
699 group.fail_line.set(line_style = g.gdk.LINE_ON_OFF_DASH)
701 def edit_op(self, op):
702 def modify():
703 if op.action[0] == 'do_search' or op.action[0] == 'do_global':
704 t = editables[0].get_text()
705 print "Checking", t
706 from Ft.Xml.XPath import XPathParser
707 if t.find('@CURRENT@') == -1:
708 try:
709 XPathParser.new().parse(t)
710 except:
711 alert('Invalid search pattern!')
712 return
713 i = 0
714 for e in editables:
715 i += 1
716 if e:
717 op.action[i] = e.get_text()
718 op.changed()
719 print "Done editing!"
720 win.destroy()
722 win = g.Dialog()
723 win.vbox.pack_start(g.Label(op.action[0]), TRUE, FALSE, 0)
724 editables = [] # [ Entry | None ]
725 focus = None
726 for x in op.action[1:]:
727 entry = g.Entry()
728 entry.set_text(str(x))
729 win.vbox.pack_start(entry, TRUE, FALSE, 0)
730 if type(x) == str or type(x) == unicode:
731 editables.append(entry)
732 entry.connect('activate', lambda e: modify())
733 if not focus:
734 focus = entry
735 entry.grab_focus()
736 else:
737 entry.set_editable(FALSE)
738 editables.append(None)
740 win.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
741 win.add_button(g.STOCK_OK, g.RESPONSE_OK)
743 def response(box, resp):
744 box.destroy()
745 if resp == g.RESPONSE_OK:
746 modify()
747 win.connect('response', response)
749 if not focus:
750 win.set_response_sensitive(g.RESPONSE_OK, FALSE)
752 win.show_all()
754 def join_nodes(self, op, exit):
755 try:
756 x1, y1, x2, y2 = self.get_arrow_ends(op, exit)
758 prev_group = self.op_to_group[op]
759 line = getattr(prev_group, exit + '_line')
760 line.set(points = connect(x1, y1, x2, y2))
761 except Block:
762 print "*** ERROR setting arc from %s:%s" % (op, exit)
764 def op_event(self, item, event, op):
765 if event.type == g.gdk.BUTTON_PRESS:
766 print "Prev %s = %s" % (op, map(str, op.prev))
767 if event.button == 1:
768 if op.parent.start != op or not op.parent.is_toplevel():
769 self.drag_last_pos = (event.x, event.y)
770 else:
771 self.drag_last_pos = None
772 else:
773 self.show_op_menu(event, op)
774 elif event.type == g.gdk.BUTTON_RELEASE:
775 if event.button == 1:
776 self.drag_last_pos = None
777 self.program_changed(None)
778 elif event.type == g.gdk.ENTER_NOTIFY:
779 item.set(fill_color = '#339900')
780 elif event.type == g.gdk.LEAVE_NOTIFY:
781 item.set(fill_color = self.op_colour(op))
782 elif event.type == g.gdk.MOTION_NOTIFY and self.drag_last_pos:
783 if not event.state & g.gdk.BUTTON1_MASK:
784 print "(stop drag!)"
785 self.drag_last_pos = None
786 self.program_changed(None)
787 return 1
788 x, y = (event.x, event.y)
789 dx, dy = x - self.drag_last_pos[0], y - self.drag_last_pos[1]
790 if abs(op.dx + dx) < 4:
791 dx = -op.dx
792 x = dx + self.drag_last_pos[0]
793 if abs(op.dy + dy) < 4:
794 dy = -op.dy
795 y = dy + self.drag_last_pos[1]
796 op.dx += dx
797 op.dy += dy
798 self.drag_last_pos = (x, y)
800 self.op_to_group[op].move(dx, dy)
801 for p in op.prev:
802 if p.next == op:
803 self.join_nodes(p, 'next')
804 if p.fail == op:
805 self.join_nodes(p, 'fail')
806 self.update_links()
807 #self.create_node(self.prog.start, self.nodes)
808 self.update_points()
809 elif event.type == g.gdk._2BUTTON_PRESS:
810 if op.action[0] == 'Start':
811 self.edit_comment(op.parent)
812 else:
813 self.edit_op(op)
814 print "(edit; stop drag!)"
815 self.drag_last_pos = None
816 self.program_changed(None)
817 return 1
819 def edit_comment(self, block):
820 assert isinstance(block, Block)
822 def set(comment):
823 block.set_comment(comment)
824 GetArg('Comment', set, ['Comment:'],
825 message = '\\n for a newline', init = [block.comment])
827 def block_toggle_enter(self):
828 self.op_menu_op.toggle_enter()
830 def block_toggle_foreach(self):
831 self.op_menu_op.toggle_foreach()
833 def block_toggle_restore(self):
834 self.op_menu_op.toggle_restore()
836 def block_edit_comment(self):
837 self.edit_comment(self.op_menu_op)
839 def op_edit(self):
840 self.edit_op(self.op_menu_op)
842 def op_swap_nf(self):
843 self.op_menu_op.swap_nf()
845 def op_del_node(self):
846 op = self.op_menu_op
847 if op.next and op.fail:
848 rox.alert("Can't delete a node with both exits in use")
849 return
850 self.clipboard = op.del_node()
852 def show_op_menu(self, event, op):
853 if op.action[0] == 'Start':
854 self.op_menu_op = op.parent
855 block_menu.popup(self, event)
856 else:
857 self.op_menu_op = op
858 op_menu.popup(self, event)
860 def paste_chain(self, op, exit):
861 print "Paste", self.clipboard
862 doc = self.clipboard
863 new = load(doc.documentElement, op.parent)
864 start = new.start.next
865 new.start.unlink('next', may_delete = 0)
866 start.set_parent(None)
867 op.link_to(start, exit)
869 def end_link_drag(self, item, event, src_op, exit):
870 # Scan all the nodes looking for one nearby...
871 x, y = event.x, event.y
873 def closest_node(op):
874 "Return the closest (node, dist) in this chain to (x, y)"
875 nx, ny = self.op_to_group[op].i2w(0, 0)
876 if op is src_op:
877 best = None
878 elif isinstance(op, Block):
879 best = None
880 else:
881 best = (op, math.hypot(nx - x, ny - y))
882 if op.next and op.next.prev[0] == op:
883 next = closest_node(op.next)
884 if next and (best is None or next[1] < best[1]):
885 best = next
886 if op.fail and op.fail.prev[0] == op:
887 fail = closest_node(op.fail)
888 if fail and (best is None or fail[1] < best[1]):
889 best = fail
890 if isinstance(op, Block):
891 sub = closest_node(op.start)
892 if sub and (best is None or sub[1] < best[1]):
893 best = sub
894 return best
896 result = closest_node(self.prog.code)
897 if result:
898 node, dist = result
899 else:
900 dist = 1000
901 if dist > 12:
902 # Too far... put the line back to the disconnected state...
903 self.join_nodes(src_op, exit)
904 return
905 try:
906 while node.action[0] == 'Start':
907 node = node.parent
908 src_op.link_to(node, exit)
909 finally:
910 self.update_all()
912 def line_paste_chain(self):
913 op, exit = self.line_menu_line
914 self.paste_chain(op, exit)
916 def line_add_block(self):
917 op, exit = self.line_menu_line
918 box = rox.Dialog()
919 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
920 box.add_button(g.STOCK_ADD, g.RESPONSE_OK)
922 foreach = g.CheckButton('Foreach block')
923 box.vbox.pack_start(foreach)
924 enter = g.CheckButton('Enter-leave block')
925 box.vbox.pack_start(enter)
926 box.vbox.show_all()
928 resp = box.run()
929 box.destroy()
930 if resp == g.RESPONSE_OK:
931 b = Block(op.parent)
932 if foreach.get_active():
933 b.toggle_foreach()
934 if enter.get_active():
935 b.toggle_enter()
936 op.link_to(b, exit)
937 if self.view.rec_point == (op, exit):
938 self.view.single_step = 1
939 self.view.stop_recording()
940 try:
941 self.view.do_one_step()
942 except View.InProgress:
943 pass
945 def line_toggle_breakpoint(self):
946 op, exit = self.line_menu_line
947 bp = self.view.breakpoints
948 if bp.has_key((op, exit)):
949 del bp[(op, exit)]
950 else:
951 bp[(op, exit)] = 1
952 self.prog.changed()
954 def line_yank_chain(self):
955 op, exit = self.line_menu_line
956 next = getattr(op, exit)
957 if not next:
958 rox.alert('Nothing to yank!')
959 return
960 self.clipboard = next.to_doc()
961 print self.clipboard
963 def line_del_chain(self):
964 op, exit = self.line_menu_line
965 next = getattr(op, exit)
966 if not next:
967 rox.alert('Nothing to delete!')
968 return
969 self.clipboard = next.to_doc()
970 op.unlink(exit)
972 def line_event(self, item, event, op, exit):
973 # Item may be rec_point or exec_point...
974 item = getattr(self.op_to_group[op], exit + '_line')
976 if event.type == g.gdk.BUTTON_PRESS:
977 if event.button == 1:
978 if not getattr(op, exit):
979 self.drag_last_pos = (event.x, event.y)
980 #item.grab(BUTTON_RELEASE | MOTION_NOTIFY, no_cursor, event.time)
981 elif event.button == 2:
982 self.paste_chain(op, exit)
983 elif event.button == 3:
984 self.line_menu_line = (op, exit)
985 line_menu.popup(self, event)
986 elif event.type == g.gdk.BUTTON_RELEASE:
987 if event.button == 1:
988 print "Clicked exit %s of %s" % (exit, op)
989 #item.ungrab(event.time)
990 self.view.set_exec((op, exit))
991 self.drag_last_pos = None
992 if not getattr(op, exit):
993 self.end_link_drag(item, event, op, exit)
994 elif event.type == g.gdk.MOTION_NOTIFY and self.drag_last_pos:
995 if not event.state & g.gdk.BUTTON1_MASK:
996 print "(stop drag!)"
997 self.drag_last_pos = None
998 if not getattr(op, exit):
999 self.end_link_drag(item, event, op, exit)
1000 return 1
1001 x, y = (event.x, event.y)
1002 dx, dy = x - self.drag_last_pos[0], y - self.drag_last_pos[1]
1004 if abs(dx) > 4 or abs(dy) > 4:
1005 sx, sy = self.get_arrow_start(op, exit)
1006 x, y = item.w2i(event.x, event.y)
1007 gr = self.op_to_group[op]
1008 if exit == 'fail':
1009 width = gr.width
1010 else:
1011 width = 0
1012 item.set(points = connect(sx, sy, x, y))
1013 elif event.type == g.gdk.ENTER_NOTIFY:
1014 item.set(fill_color = '#339900')
1015 elif event.type == g.gdk.LEAVE_NOTIFY:
1016 if exit == 'next':
1017 item.set(fill_color = 'black')
1018 else:
1019 item.set(fill_color = '#ff6666')
1020 return 1
1022 def get_arrow_start(self, op, exit):
1023 gr = self.op_to_group[op]
1024 return ((exit == 'fail' and gr.width) or 0, gr.height)
1026 def get_arrow_ends(self, op, exit):
1027 """Return coords of arrow, relative to op's group."""
1028 op2 = getattr(op, exit)
1030 prev_group = self.op_to_group[op]
1032 x1, y1 = self.get_arrow_start(op, exit)
1034 if op2:
1035 try:
1036 group = self.op_to_group[op2]
1037 except:
1038 x2 = x1 + 50
1039 y2 = y1 + 50
1040 else:
1041 x2, y2 = group.i2w(0, 0)
1042 x2, y2 = prev_group.w2i(x2, y2)
1043 elif exit == 'next':
1044 x2, y2 = DEFAULT_NEXT
1045 x2 += x1
1046 y2 += y1
1047 else:
1048 x2, y2 = DEFAULT_FAIL
1049 x2 += x1
1050 y2 += y1
1051 return (x1, y1, x2, y2)
1053 def set_bounds(self):
1054 #self.update_now() # GnomeCanvas bug?
1055 min_x, min_y, max_x, max_y = self.root().get_bounds()
1056 min_x -= 8
1057 max_x += 8
1058 min_y -= 8
1059 max_y += 8
1060 self.set_scroll_region(min_x, min_y, max_x, max_y)
1061 self.root().move(0, 0) # Magic!
1062 #self.set_usize(max_x - min_x, -1)
1064 def canvas_to_world(self, (x, y)):
1065 "Canvas routine seems to be broken..."
1066 mx, my, maxx, maxy = self.get_scroll_region()
1067 sx = self.get_hadjustment().value
1068 sy = self.get_hadjustment().value
1069 return (x + mx + sx , y + my + sy)
1071 class ChainWindow(rox.Window):
1072 def __init__(self, view, prog):
1073 rox.Window.__init__(self)
1074 swin = g.ScrolledWindow()
1075 self.add(swin)
1076 disp = ChainDisplay(view, prog)
1077 swin.add(disp)
1079 swin.show_all()
1080 self.disp = disp
1081 self.set_default_size(-1, 200)
1082 self.set_title(prog.name)
1084 def update_points(self):
1085 self.disp.update_points()