Send rox-changed instead of row-deleted/row-inserted.
[dom-editor.git] / Dome / GUIView.py
blobfee25dc0faab446324aab6991cc56f860a22a02a
1 from xml.dom import Node
2 from rox.loading import XDSLoader
4 import rox
5 from rox import g, TRUE, FALSE
6 keysyms = g.keysyms
8 from View import View
9 from Display2 import Display
10 from Beep import Beep
11 from GetArg import GetArg
12 from Path import make_relative_path
14 from rox.Menu import Menu
15 from gnome import canvas
17 menu = Menu('main', [
18 ('/File', None, '<Branch>', ''),
19 ('/File/Save', 'menu_save', '', '<Ctrl>S'),
20 ('/File/Blank document', 'do_blank_all', '', '<Ctrl>N'),
21 ('/File/Clear undo buffer', 'menu_clear_undo', '', ''),
23 ('/Edit', None, '<Branch>', ''),
24 ('/Edit/Copy attributes', 'do_yank_attributes', '', ''),
25 ('/Edit/Paste attributes', 'do_paste_attribs', '', ''),
26 ('/Edit/Copy attrib value', 'do_yank_value', '', ''),
27 ('/Edit/Rename attribute', 'menu_rename_attr', '', ''),
28 ('/Edit/', '', '', '<separator>'),
29 ('/Edit/Cut', 'do_delete_node', '', '<Ctrl>X'),
30 ('/Edit/Delete', 'do_delete_node_no_clipboard', '', ''),
31 ('/Edit/Shallow cut', 'do_delete_shallow', '', '<Shift>X'),
32 ('/Edit/', '', '', '<separator>'),
33 ('/Edit/Copy', 'do_yank', '', '<Ctrl>C'),
34 ('/Edit/Shallow copy', 'do_shallow_yank', '', '<Shift>Y'),
35 ('/Edit/', '', '', '<separator>'),
36 ('/Edit/Paste (replace)','do_put_replace', '', '<Ctrl>V'),
37 ('/Edit/Paste (inside)', 'do_put_as_child', '', 'bracketright'),
38 ('/Edit/Paste (before)', 'do_put_before', '', '<Shift>P'),
39 ('/Edit/Paste (after)', 'do_put_after', '', 'p'),
40 ('/Edit/', '', '', '<separator>'),
41 ('/Edit/Edit value', 'toggle_edit', '', 'Return'),
42 ('/Edit/', '', '', '<separator>'),
43 ('/Edit/Undo', 'do_undo', '', '<Ctrl>Z'),
44 ('/Edit/Redo', 'do_redo', '', '<Ctrl>Y'),
46 ('/Move', None, '<Branch>', ''),
47 ('/Move/XPath search', 'menu_show_search', '', 'slash'),
48 ('/Move/Text search', 'menu_show_text_search', '', 'T'),
49 ('/Move/Enter', 'do_enter', '', '<Shift>greater'),
50 ('/Move/Leave', 'do_leave', '', '<Shift>less'),
52 ('/Move/Root node', 'move_home', '', 'Home'),
53 ('/Move/Previous sibling', 'move_prev_sib', '', 'Up'),
54 ('/Move/Next sibling', 'move_next_sib', '', 'Down'),
55 ('/Move/Parent', 'move_left', '', 'Left'),
56 ('/Move/First child', 'move_right', '', 'Right'),
57 ('/Move/Last child', 'move_end', '', 'End'),
59 ('/Move/To attribute', 'menu_select_attrib', '', 'At'),
61 ('/Select', None, '<Branch>', ''),
62 ('/Select/By XPath', 'menu_show_global', '', 'numbersign'),
63 ('/Select/Duplicate Siblings', 'do_select_dups', '', ''),
64 ('/Select/To Mark', 'do_select_marked', '', 'minus'),
65 ('/Select/Child Nodes', 'do_select_children', '', 'asterisk'),
67 ('/Mark', None, '<Branch>', ''),
68 ('/Mark/Mark Selection', 'do_mark_selection', '', 'm'),
69 ('/Mark/Switch with Selection', 'do_mark_switch', '', 'comma'),
70 ('/Mark/Clear Mark', 'do_clear_mark', '', ''),
72 ('/Network', None, '<Branch>', ''),
73 ('/Network/HTTP GET', 'do_suck', '', '<Shift>asciicircum'),
74 ('/Network/HTTP POST', 'do_http_post', '', ''),
75 ('/Network/Send SOAP message', 'do_soap_send', '', ''),
77 ('/Create', None, '<Branch>', ''),
78 ('/Create/Insert node', 'menu_insert_element', '', 'I'),
79 ('/Create/Append node', 'menu_append_element', '', 'A'),
80 ('/Create/Open node inside', 'menu_open_element', '', 'O'),
81 ('/Create/Open node at end', 'menu_open_element_end', '', 'E'),
83 ('/Process', None, '<Branch>', ''),
84 ('/Process/Substitute', 'menu_show_subst', '', 's'),
85 ('/Process/Python expression', 'menu_show_pipe', '', '<Shift>exclam'),
86 ('/Process/XPath expression', 'menu_show_xpath', '', ''),
87 ('/Process/Normalise', 'do_normalise', '', ''),
88 ('/Process/Remove default namespaces', 'do_remove_ns', '', 'r'),
89 ('/Process/Convert to text', 'do_convert_to_text', '', ''),
90 ('/Process/Convert to comment', 'do_convert_to_comment', '', ''),
91 ('/Process/Convert to element', 'do_convert_to_element', '', ''),
93 ('/Program', None, '<Branch>', ''),
94 ('/Program/Input', 'menu_show_ask', '', 'question'),
95 ('/Program/Compare', 'do_compare', '', 'equal'),
96 ('/Program/Fail', 'do_fail', '', ''),
97 ('/Program/Pass', 'do_pass', '', ''),
98 ('/Program/Repeat last', 'do_again', '', 'dot'),
100 ('/View', None, '<Branch>', ''),
101 ('/View/Toggle hidden', 'do_toggle_hidden', '', '<Ctrl>H'),
102 ('/View/Show as HTML', 'do_show_html', '', ''),
103 ('/View/Show as canvas', 'do_show_canvas', '', ''),
104 ('/View/Show namespaces', 'show_namespaces', '', '<Ctrl>;'),
105 ('/View/Close Window', 'menu_close_window', '', '<Ctrl>Q'),
107 #('/Options...', 'menu_options', '', '<Ctrl>O'),
110 def make_do(action):
111 return lambda(self): self.view.may_record([action])
113 class GUIView(Display, XDSLoader):
114 def __init__(self, window, view):
115 Display.__init__(self, window, view)
116 XDSLoader.__init__(self, ['application/x-dome', 'text/xml',
117 'application/xml'])
118 window.connect('key-press-event', self.key_press)
119 self.cursor_node = None
120 self.edit_dialog = None
121 self.update_state()
123 menu.attach(window, self)
125 def destroyed(self, widget):
126 print "GUIView destroyed!"
127 Display.destroyed(self, widget)
128 del self.cursor_node
130 def update_state(self):
131 if self.view.rec_point:
132 state = "(recording)"
133 elif self.view.idle_cb or self.view.op_in_progress:
134 state = "(playing)"
135 else:
136 state = ""
137 self.parent_window.set_state(state)
138 self.do_update_now()
140 def xds_load_from_stream(self, path, type, stream):
141 if not path:
142 raise Exception('Can only load from files... sorry!')
143 if path.endswith('.html'):
144 self.view.load_html(path)
145 else:
146 self.view.load_xml(path)
147 if self.view.root == self.view.model.get_root():
148 self.parent_window.uri = path
149 self.parent_window.update_title()
151 def key_press(self, widget, kev):
152 focus = widget.focus_widget
153 if focus and focus is not widget and focus.get_toplevel() is widget:
154 if focus.event(kev):
155 return TRUE # Handled
157 if self.cursor_node:
158 return 0
159 if kev.keyval == keysyms.Up:
160 self.view.may_record(['move_prev_sib'])
161 elif kev.keyval == keysyms.Down:
162 self.view.may_record(['move_next_sib'])
163 elif kev.keyval == keysyms.Left:
164 self.view.may_record(['move_left'])
165 elif kev.keyval == keysyms.Right:
166 self.view.may_record(['move_right'])
167 elif kev.keyval == keysyms.KP_Add:
168 self.menu_show_add_attrib()
169 elif kev.keyval == keysyms.Tab:
170 self.toggle_edit()
171 else:
172 return 0
173 return 1
175 def node_clicked(self, node, bev):
176 print "Clicked", node.namespaceURI, node.localName
177 if node:
178 if bev.type == g.gdk.BUTTON_PRESS:
179 if len(self.view.current_nodes) == 0:
180 src = self.view.root
181 else:
182 src = self.view.current_nodes[-1]
183 shift = bev.state & g.gdk.SHIFT_MASK
184 add = bev.state & g.gdk.CONTROL_MASK
185 select_region = shift and node.nodeType == Node.ELEMENT_NODE
186 lit = shift and not select_region
188 ns = {}
189 path = make_relative_path(src, node, lit, ns)
190 if path == '.' and self.view.current_nodes and not self.view.current_attrib:
191 return
192 if select_region:
193 self.view.may_record(["select_region", path, ns])
194 else:
195 self.view.may_record(["do_search", path, ns, add])
196 else:
197 self.view.may_record(["toggle_hidden"])
199 def attrib_clicked(self, element, attrib, event):
200 if len(self.view.current_nodes) == 0:
201 src = self.view.root
202 else:
203 src = self.view.current_nodes[-1]
204 ns = {}
206 print "attrib_clicked", attrib, attrib.namespaceURI, attrib.localName
207 path = make_relative_path(src, element, FALSE, ns)
208 if path != '.':
209 self.view.may_record(["do_search", path, ns, FALSE])
210 self.view.may_record(["attribute", attrib.namespaceURI, attrib.localName])
212 def menu_save(self):
213 self.parent_window.save()
215 def show_menu(self, bev):
216 menu.popup(self, bev)
218 def playback(self, macro, map):
219 "Called when the user clicks on a macro button."
220 Exec.exec_state.clean()
221 if map:
222 self.view.may_record(['map', macro.uri])
223 else:
224 self.view.may_record(['play', macro.uri])
226 def menu_show_ask(self):
227 def do_ask(q, self = self):
228 action = ["ask", q]
229 self.view.may_record(action)
230 GetArg('Input:', do_ask, ('Prompt:',))
232 def menu_show_subst(self):
233 def do_subst(args, self = self):
234 action = ["subst", args[0], args[1]]
235 self.view.may_record(action)
236 GetArg('Substitute:', do_subst, ('Replace:', 'With:'))
238 def show_namespaces(self):
239 import Namespaces
240 Namespaces.GUI(self.view.model).show()
242 def move_from(self, old = []):
243 self.hide_editbox()
244 Display.move_from(self, old)
246 def hide_editbox(self):
247 if self.cursor_node:
248 if self.cursor_attrib:
249 self.cursor_hidden_text.set(text = '%s=%s' %
250 (self.cursor_attrib.name, self.cursor_attrib.value))
251 self.cursor_hidden_text.show()
252 self.auto_highlight(self.cursor_node)
253 self.cursor_node = None
254 self.edit_box_item.destroy()
256 def show_editbox(self):
257 "Edit the current node/attribute"
258 self.do_update_now()
260 if self.cursor_node:
261 self.hide_editbox()
263 if not self.visible:
264 raise Exception("Can't edit while display is hidden!")
266 self.cursor_node = self.view.current_nodes[0]
267 group = self.node_to_group[self.cursor_node]
268 self.cursor_attrib = self.view.current_attrib
270 self.highlight(group, FALSE)
272 if self.cursor_attrib:
273 group = group.attrib_to_group[self.cursor_attrib]
275 self.cursor_hidden_text = group.text
276 if not self.cursor_attrib:
277 # Don't hide for attributes, so we can still see the name
278 group.text.hide()
279 else:
280 group.text.set(text = str(self.cursor_attrib.name) + '=')
282 self.update_now() # GnomeCanvas bug?
283 lx, ly, hx, hy = group.text.get_bounds()
284 x, y = group.i2w(lx, ly)
286 text = g.TextView()
287 text.show()
289 eb = g.Frame()
290 eb.add(text)
291 self.edit_box = eb
292 self.edit_box_text = text
293 m = 3
295 #s = eb.get_style().copy()
296 #s.font = load_font('fixed')
297 #eb.set_style(s)
298 #if self.cursor_attrib:
299 # name_width = s.font.measure(self.cursor_attrib.name + '=') + 1
300 #else:
301 # name_width = 0
302 name_width = 0
304 self.edit_box_item = self.root().add(canvas.CanvasWidget, widget = eb,
305 x = x - m + name_width, y = y - m,
306 anchor = g.ANCHOR_NW)
308 #text.set_editable(TRUE)
309 text.get_buffer().insert_at_cursor(self.get_edit_text())
310 text.set_wrap_mode(g.WRAP_NONE)
311 text.get_buffer().connect('changed', self.eb_changed)
312 text.connect('key-press-event', self.eb_key)
313 eb.show()
314 text.realize()
315 self.size_eb()
316 text.grab_focus()
317 #eb.select_region(0, -1)
318 eb.show()
320 def get_edit_text(self):
321 if node.nodeType == Node.ELEMENT_NODE:
322 if self.cursor_attrib:
323 return str(self.cursor_attrib.value)
324 return node.nodeName
325 else:
326 return node.nodeValue
328 def eb_key(self, eb, kev):
329 key = kev.keyval
330 if key == g.keysyms.KP_Enter:
331 key = g.keysyms.Return
332 if key == g.keysyms.Escape:
333 self.hide_editbox()
334 elif key == g.keysyms.Return and kev.state & g.gdk.CONTROL_MASK:
335 eb.insert_defaults('\n')
336 self.size_eb()
337 elif key == g.keysyms.Tab or key == g.keysyms.Return:
338 buffer = eb.get_buffer()
339 s = buffer.get_start_iter()
340 e = buffer.get_end_iter()
341 text = buffer.get_text(s, e, TRUE)
342 try:
343 if text != self.get_edit_text():
344 self.commit_edit(text)
345 finally:
346 self.hide_editbox()
347 return 0
349 def commit_edit(self, new):
350 if self.cursor_attrib:
351 self.view.may_record(['set_attrib', new])
352 else:
353 self.view.may_record(['change_node', new])
355 def eb_changed(self, eb):
356 self.size_eb()
358 def size_eb(self):
359 def cb():
360 req = self.edit_box_text.size_request()
361 print "Wants", req
362 width = max(req[0], 10)
363 height = max(req[1], 10)
364 self.edit_box_item.set(width = width + 12, height = height + 4)
365 g.idle_add(cb)
367 def toggle_edit(self):
368 node = self.view.current_nodes[0]
369 attrib = self.view.current_attrib
371 if self.edit_dialog:
372 self.edit_dialog.destroy()
373 self.edit_dialog = rox.Dialog()
374 eb = self.edit_dialog
376 if node.nodeType == Node.ELEMENT_NODE:
377 if attrib:
378 text = unicode(attrib.value)
379 else:
380 text = node.nodeName
381 entry = g.Entry()
382 entry.set_text(text)
383 entry.set_activates_default(True)
384 def get_text(): return entry.get_text()
385 else:
386 text = node.nodeValue
387 entry = g.TextView()
388 buffer = entry.get_buffer()
389 buffer.insert_at_cursor(text)
390 entry.set_size_request(400, 200)
392 def get_text():
393 start = buffer.get_start_iter()
394 end = buffer.get_end_iter()
395 return buffer.get_text(start, end, True)
396 eb.vbox.pack_start(entry, TRUE, FALSE, 0)
398 eb.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
399 eb.add_button(g.STOCK_APPLY, g.RESPONSE_OK)
400 eb.set_default_response(g.RESPONSE_OK)
401 entry.grab_focus()
402 def destroy(eb):
403 self.edit_dialog = None
404 eb.connect('destroy', destroy)
405 def response(eb, resp):
406 if resp == g.RESPONSE_CANCEL:
407 eb.destroy()
408 elif resp == g.RESPONSE_OK:
409 new = get_text()
410 if new != text:
411 if attrib:
412 self.view.may_record(['set_attrib', new])
413 else:
414 self.view.may_record(['change_node', new])
415 eb.destroy()
416 eb.connect('response', response)
418 eb.show_all()
420 #if self.cursor_node:
421 # self.hide_editbox()
422 #else:
423 # self.show_editbox()
425 def menu_select_attrib(self):
426 def do_attrib(name):
427 if ':' in name:
428 (prefix, localName) = name.split(':', 1)
429 namespaceURI = self.view.model.prefix_to_namespace(self.view.get_current(), prefix)
430 else:
431 (prefix, localName) = (None, name)
432 namespaceURI = None
433 action = ["attribute", namespaceURI, localName]
434 self.view.may_record(action)
435 GetArg('Select attribute:', do_attrib, ['Name:'])
437 def menu_show_add_attrib(self):
438 def do_it(name):
439 action = ["add_attrib", "UNUSED", name]
440 self.view.may_record(action)
441 GetArg('Create attribute:', do_it, ['Name:'])
443 def menu_show_pipe(self):
444 def do_pipe(expr):
445 action = ["python", expr]
446 self.view.may_record(action)
447 GetArg('Python expression:', do_pipe, ['Eval:'], "'x' is the old text...")
449 def menu_show_xpath(self):
450 def go(expr):
451 action = ["xpath", expr]
452 self.view.may_record(action)
453 GetArg('XPath expression:', go, ['Eval:'], "Result goes on the clipboard")
455 def menu_show_global(self):
456 def do_global(pattern):
457 action = ["do_global", pattern]
458 self.view.may_record(action)
459 GetArg('Global:', do_global, ['Pattern:'],
460 '(@CURRENT@ is the current node\'s value)\n' +
461 'Perform next action on all nodes matching')
463 def menu_show_text_search(self):
464 def do_text_search(pattern):
465 action = ["do_text_search", pattern]
466 self.view.may_record(action)
467 GetArg('Search for:', do_text_search, ['Text pattern:'],
468 '(@CURRENT@ is the current node\'s value)\n')
470 def menu_show_search(self):
471 def do_search(pattern):
472 action = ["do_search", pattern]
473 self.view.may_record(action)
474 GetArg('Search for:',
475 do_search, ['XPath:'],
476 '(@CURRENT@ is the current node\'s value)')
478 def menu_rename_attr(self):
479 def do(name):
480 action = ["rename_attrib", name]
481 self.view.may_record(action)
482 GetArg('Rename to:', do, ['New name:'])
485 def do_create(self, action, nodeType, data):
486 action = action[0]
487 qname = True
488 if nodeType == Node.ELEMENT_NODE:
489 action += 'e'
490 elif nodeType == Node.TEXT_NODE:
491 action += 't'
492 qname = False
493 elif nodeType == Node.ATTRIBUTE_NODE:
494 action += 'a'
496 if qname:
497 # Check name is valid
498 # XXX: Should be more strict
499 data = data.strip()
500 assert '\n' not in data
501 assert ' ' not in data
503 self.view.may_record(['add_node', action, data])
505 def show_add_box(self, action):
506 if action[0] == 'i':
507 text = 'Insert'
508 elif action[0] == 'a':
509 text = 'Append'
510 elif action[0] == 'o':
511 text = 'Open'
512 elif action[0] == 'e':
513 text = 'Open at end'
514 else:
515 assert 0
516 if action[1] == 'e':
517 text += ' element'
518 prompt = 'Node name'
519 elif action[1] == 't':
520 text += ' text'
521 prompt = 'Text'
522 else:
523 assert 0
525 box = g.Dialog()
526 text = g.TextView()
527 box.vbox.pack_start(text, TRUE, FALSE, 0)
528 text.set_size_request(400, 200)
529 box.set_has_separator(False)
531 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
532 box.add_button('Add _Attribute', Node.ATTRIBUTE_NODE)
533 box.add_button('Add _Text', Node.TEXT_NODE)
534 box.add_button('Add _Element', Node.ELEMENT_NODE)
535 box.set_default_response(g.RESPONSE_OK)
536 text.grab_focus()
537 def response(box, resp):
538 if resp == g.RESPONSE_CANCEL:
539 box.destroy()
540 return
541 buffer = text.get_buffer()
542 start = buffer.get_start_iter()
543 end = buffer.get_end_iter()
544 new = buffer.get_text(start, end, True)
545 if new:
546 self.do_create(action, resp, new)
547 box.destroy()
548 box.connect('response', response)
550 box.show_all()
552 def new_name(self):
553 cur = self.view.get_current()
554 if cur.nodeType == Node.ELEMENT_NODE:
555 return cur.nodeName
556 return cur.parentNode.nodeName
558 def menu_insert_element(self):
559 "Insert element"
560 self.show_add_box('ie')
562 def menu_append_element(self):
563 "Append element"
564 self.show_add_box('ae')
566 def menu_open_element(self):
567 "Open element"
568 self.show_add_box('oe')
570 def menu_open_element_end(self):
571 "Open element at end"
572 self.show_add_box('ee')
574 def menu_insert_text(self):
575 "Insert text"
576 self.show_add_box('it')
578 def menu_append_text(self):
579 "Append text"
580 self.show_add_box('at')
582 def menu_open_text(self):
583 "Open text"
584 self.show_add_box('ot')
586 def menu_open_text_end(self):
587 "Open text at end"
588 self.show_add_box('et')
590 def menu_close_window(self):
591 self.parent_window.destroy()
593 def menu_options(self):
594 rox.edit_options()
596 def menu_clear_undo(self):
597 if rox.confirm('Really clear the undo buffer?',
598 g.STOCK_CLEAR):
599 self.view.model.clear_undo()
601 do_blank_all = make_do('blank_all')
602 do_enter = make_do('enter')
603 do_leave = make_do('leave')
604 do_suck = make_do('suck')
605 do_http_post = make_do('http_post')
606 do_soap_send = make_do('soap_send')
607 do_select_dups = make_do('select_dups')
608 do_paste_attribs = make_do('paste_attribs')
609 do_yank_value = make_do('yank_value')
610 do_yank_attributes = make_do('yank_attribs')
611 do_delete_node = make_do('delete_node')
612 do_delete_node_no_clipboard = make_do('delete_node_no_clipboard')
613 do_delete_shallow = make_do('delete_shallow')
614 do_yank = make_do('yank')
615 do_shallow_yank = make_do('shallow_yank')
616 do_put_replace = make_do('put_replace')
617 do_put_as_child = make_do('put_as_child')
618 do_put_before = make_do('put_before')
619 do_put_after = make_do('put_after')
620 do_undo = make_do('undo')
621 do_redo = make_do('redo')
622 do_fail = make_do('fail')
623 do_pass = make_do('do_pass')
624 do_toggle_hidden = make_do('toggle_hidden')
625 do_show_html = make_do('show_html')
626 do_show_canvas = make_do('show_canvas')
627 do_compare = make_do('compare')
628 do_again = make_do('again')
629 do_normalise = make_do('normalise')
630 do_convert_to_text = make_do('convert_to_text')
631 do_convert_to_comment = make_do('convert_to_comment')
632 do_convert_to_element = make_do('convert_to_element')
633 do_convert_to_pi = make_do('convert_to_pi')
634 do_remove_ns = make_do('remove_ns')
636 do_clear_mark = make_do('clear_mark')
637 do_mark_switch = make_do('mark_switch')
638 do_mark_selection = make_do('mark_selection')
639 do_select_marked = make_do('select_marked_region')
640 do_select_children = make_do('select_children')
642 move_home = make_do('move_home')
643 move_end = make_do('move_end')
644 move_left = make_do('move_left')
645 move_right = make_do('move_right')
646 move_next_sib = make_do('move_next_sib')
647 move_prev_sib = make_do('move_prev_sib')