Renamed a 'with' variable; will be a keyword in Python 2.6.
[dom-editor.git] / Dome / GUIView.py
blob816cbab4ecbacdeba23b6d6298856876686878fd
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
16 menu = Menu('main', [
17 ('/File', None, '<Branch>', ''),
18 ('/File/Save', 'menu_save', '<StockItem>', '<Ctrl>S', g.STOCK_SAVE),
19 ('/File/Blank document', 'do_blank_all', '<StockItem>', '<Ctrl>N', g.STOCK_NEW),
20 ('/File/Clear undo buffer', 'menu_clear_undo', '<StockItem>', '', g.STOCK_CLEAR),
22 ('/Edit', None, '<Branch>', ''),
23 ('/Edit/Copy attributes', 'do_yank_attributes', '<StockItem>', '', g.STOCK_COPY),
24 ('/Edit/Paste attributes', 'do_paste_attribs', '<StockItem>', '', g.STOCK_PASTE),
25 ('/Edit/Copy attrib value', 'do_yank_value', '<StockItem>', '', g.STOCK_COPY),
26 ('/Edit/Rename attribute', 'menu_rename_attr', '<StockItem>', '', g.STOCK_PROPERTIES),
27 ('/Edit/', '', '', '<separator>'),
28 ('/Edit/Cut', 'do_delete_node', '<StockItem>', '<Ctrl>X', g.STOCK_DELETE),
29 ('/Edit/Delete', 'do_delete_node_no_clipboard', '<StockItem>', '', g.STOCK_DELETE),
30 ('/Edit/Shallow cut', 'do_delete_shallow', '<StockItem>', '<Shift>X', g.STOCK_DELETE),
31 ('/Edit/', '', '', '<separator>'),
32 ('/Edit/Copy', 'do_yank', '<StockItem>', '<Ctrl>C', g.STOCK_COPY),
33 ('/Edit/Shallow copy', 'do_shallow_yank', '<StockItem>', '<Shift>Y', g.STOCK_COPY),
34 ('/Edit/', '', '', '<separator>'),
35 ('/Edit/Paste (replace)','do_put_replace', '<StockItem>', '<Ctrl>V', g.STOCK_PASTE),
36 ('/Edit/Paste (inside)', 'do_put_as_child', '<StockItem>', 'bracketright', g.STOCK_GO_FORWARD),
37 ('/Edit/Paste (before)', 'do_put_before', '<StockItem>', '<Shift>P', g.STOCK_GO_UP),
38 ('/Edit/Paste (after)', 'do_put_after', '<StockItem>', 'p', g.STOCK_GO_DOWN),
39 ('/Edit/', '', '', '<separator>'),
40 ('/Edit/Edit value', 'toggle_edit', '<StockItem>', 'Return', g.STOCK_PROPERTIES),
41 ('/Edit/', '', '', '<separator>'),
42 ('/Edit/Undo', 'do_undo', '<StockItem>', '<Ctrl>Z', g.STOCK_UNDO),
43 ('/Edit/Redo', 'do_redo', '<StockItem>', '<Ctrl>Y', g.STOCK_REDO),
45 ('/Move', None, '<Branch>', ''),
46 ('/Move/XPath search', 'menu_show_search', '<StockItem>', 'slash', g.STOCK_FIND),
47 ('/Move/Text search', 'menu_show_text_search', '<StockItem>', 'T', g.STOCK_FIND),
48 ('/Move/Enter', 'do_enter', '', '<Shift>greater'),
49 ('/Move/Leave', 'do_leave', '', '<Shift>less'),
51 ('/Move/Root node', 'move_home', '<StockItem>', 'Home', g.STOCK_HOME),
52 ('/Move/Previous sibling', 'move_prev_sib', '<StockItem>', 'Up', g.STOCK_GO_UP),
53 ('/Move/Next sibling', 'move_next_sib', '<StockItem>', 'Down', g.STOCK_GO_DOWN),
54 ('/Move/Parent', 'move_left', '<StockItem>', 'Left', g.STOCK_GO_BACK),
55 ('/Move/First child', 'move_right', '<StockItem>', 'Right', g.STOCK_GO_FORWARD),
56 ('/Move/Last child', 'move_end', '<StockItem>', 'End', g.STOCK_GOTO_LAST),
58 ('/Move/To attribute', 'menu_select_attrib', '<StockItem>', 'At', g.STOCK_JUMP_TO),
60 ('/Select', None, '<Branch>', ''),
61 ('/Select/Matching Nodes', 'menu_show_global', '', 'numbersign'),
62 ('/Select/Duplicate Siblings', 'do_select_dups', '', ''),
63 ('/Select/To Mark', 'do_select_marked', '', 'minus'),
64 ('/Select/Child Nodes', 'do_select_children', '', 'asterisk'),
66 ('/Mark', None, '<Branch>', ''),
67 ('/Mark/Mark Selection', 'do_mark_selection', '', 'm'),
68 ('/Mark/Switch with Selection', 'do_mark_switch', '', 'comma'),
69 ('/Mark/Clear Mark', 'do_clear_mark', '<StockItem>', '', g.STOCK_CLEAR),
70 ('/Mark/Move marked here', 'do_move_marked', '<StockItem>', '', g.STOCK_GO_FORWARD),
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', '<StockItem>', 'I', g.STOCK_ADD),
79 ('/Create/Append node', 'menu_append_element', '<StockItem>', 'A', g.STOCK_ADD),
80 ('/Create/Open node inside', 'menu_open_element', '<StockItem>', 'O', g.STOCK_ADD),
81 ('/Create/Open node at end', 'menu_open_element_end', '<StockItem>', 'E', g.STOCK_ADD),
83 ('/Process', None, '<Branch>', ''),
84 ('/Process/Substitute...', 'menu_show_subst', '', 's'),
85 ('/Process/Split at...', 'menu_show_split', '', ''),
86 ('/Process/Python expression...', 'menu_show_pipe', '', '<Shift>exclam'),
87 ('/Process/XPath expression...', 'menu_show_xpath', '', ''),
88 ('/Process/Normalise', 'do_normalise', '', ''),
89 ('/Process/Remove default namespaces', 'do_remove_ns', '', 'r'),
90 ('/Process/Convert to text', 'do_convert_to_text', '', ''),
91 ('/Process/Convert to comment', 'do_convert_to_comment', '', ''),
92 ('/Process/Convert to element', 'do_convert_to_element', '', ''),
94 ('/Program', None, '<Branch>', ''),
95 ('/Program/Input', 'menu_show_ask', '', 'question'),
96 ('/Program/Compare', 'do_compare', '', 'equal'),
97 ('/Program/Pass', 'do_pass', '', ''),
98 ('/Program/Fail', 'do_fail', '', ''),
99 ('/Program/Assert', 'menu_assert', '', ''),
100 ('/Program/Repeat last', 'do_again', '', 'dot'),
102 ('/View', None, '<Branch>', ''),
103 ('/View/Toggle hidden', 'do_toggle_hidden', '', '<Ctrl>H'),
104 ('/View/Hide with label', 'menu_hide_with_expr', '', ''),
105 ('/View/Show as HTML', 'do_show_html', '', ''),
106 ('/View/Show as canvas', 'do_show_canvas', '', ''),
107 ('/View/Show namespaces', 'show_namespaces', '', '<Ctrl>semicolon'),
108 ('/View/Close Window', 'menu_close_window', '<StockItem>', '<Ctrl>Q', g.STOCK_CLOSE),
110 #('/Options...', 'menu_options', '', '<Ctrl>O'),
113 def make_do(action):
114 return lambda(self): self.view.may_record([action])
116 class GUIView(Display, XDSLoader):
117 def __init__(self, window, view):
118 Display.__init__(self, window, view)
119 XDSLoader.__init__(self, ['application/x-dome', 'text/xml',
120 'application/xml'])
121 window.connect('key-press-event', self.key_press)
122 self.cursor_node = None
123 self.edit_dialog = None
124 self.update_state()
126 menu.attach(window, self)
128 def destroyed(self, widget):
129 print "GUIView destroyed!"
130 Display.destroyed(self, widget)
131 del self.cursor_node
133 def update_state(self):
134 if self.view.rec_point:
135 state = "(recording)"
136 elif self.view.idle_cb or self.view.op_in_progress:
137 state = "(playing)"
138 else:
139 state = ""
140 self.parent_window.set_state(state)
141 self.do_update_now()
143 def xds_load_from_stream(self, path, type, stream):
144 if not path:
145 raise Exception('Can only load from files... sorry!')
146 if path.endswith('.html'):
147 self.view.load_html(path)
148 else:
149 self.view.load_xml(path)
150 if self.view.root == self.view.model.get_root():
151 self.parent_window.uri = path
152 self.parent_window.update_title()
154 def key_press(self, widget, kev):
155 focus = widget.focus_widget
156 if focus and focus is not widget and focus.get_toplevel() is widget:
157 if focus.event(kev):
158 return TRUE # Handled
160 if self.cursor_node:
161 return 0
162 if kev.keyval == keysyms.Up:
163 self.view.may_record(['move_prev_sib'])
164 elif kev.keyval == keysyms.Down:
165 self.view.may_record(['move_next_sib'])
166 elif kev.keyval == keysyms.Left:
167 self.view.may_record(['move_left'])
168 elif kev.keyval == keysyms.Right:
169 self.view.may_record(['move_right'])
170 elif kev.keyval == keysyms.KP_Add:
171 self.menu_show_add_attrib()
172 elif kev.keyval == keysyms.Tab:
173 self.toggle_edit()
174 else:
175 return 0
176 return 1
178 def do_drag(self, src, dst):
179 if src[1] or dst[1]:
180 return # Don't do attribute drags yet
181 src, dst = src[0], dst[0]
182 path = make_relative_path(src, dst, None, self.view.model.namespaces)
183 self.view.may_record(['move_selection', path])
185 def node_clicked(self, node, bev):
186 #print "Clicked", node.namespaceURI, node.localName
187 if node:
188 if bev.type == g.gdk.BUTTON_PRESS:
189 if len(self.view.current_nodes) == 0:
190 src = self.view.root
191 else:
192 src = self.view.current_nodes[-1]
193 shift = bev.state & g.gdk.SHIFT_MASK
194 add = bev.state & g.gdk.CONTROL_MASK
195 select_region = shift and node.nodeType == Node.ELEMENT_NODE
196 lit = shift and not select_region
198 path = make_relative_path(src, node, lit, self.view.model.namespaces)
199 if path == '.' and self.view.current_nodes and not self.view.current_attrib:
200 return
201 if select_region:
202 self.view.may_record(["select_region", path, "unused"])
203 else:
204 self.view.may_record(["do_search", path, "unused", add])
205 else:
206 self.view.may_record(["toggle_hidden"])
208 def attrib_clicked(self, element, attrib, event):
209 if len(self.view.current_nodes) == 0:
210 src = self.view.root
211 else:
212 src = self.view.current_nodes[-1]
214 print "attrib_clicked", attrib, attrib.namespaceURI, attrib.localName
215 path = make_relative_path(src, element, FALSE, self.view.model.namespaces)
216 if path != '.':
217 self.view.may_record(["do_search", path, "unused", FALSE])
218 self.view.may_record(["attribute", attrib.namespaceURI, attrib.localName])
220 def menu_save(self):
221 self.parent_window.save()
223 def show_menu(self, bev):
224 menu.popup(self, bev)
226 def playback(self, macro, map):
227 "Called when the user clicks on a macro button."
228 Exec.exec_state.clean()
229 if map:
230 self.view.may_record(['map', macro.uri])
231 else:
232 self.view.may_record(['play', macro.uri])
234 def menu_show_ask(self):
235 def do_ask(q, self = self):
236 action = ["ask", q]
237 self.view.may_record(action)
238 GetArg('Input:', do_ask, ('Prompt:',))
240 def menu_show_subst(self):
241 def do_subst(args, self = self):
242 action = ["subst", args[0], args[1]]
243 self.view.may_record(action)
244 GetArg('Substitute:', do_subst, ('Replace:', 'With:'))
246 def show_namespaces(self):
247 import Namespaces
248 Namespaces.GUI(self.view.model).show()
250 def move_from(self, old = []):
251 self.hide_editbox()
252 Display.move_from(self, old)
254 def hide_editbox(self):
255 if self.cursor_node:
256 if self.cursor_attrib:
257 self.cursor_hidden_text.set(text = '%s=%s' %
258 (self.cursor_attrib.name, self.cursor_attrib.value))
259 self.cursor_hidden_text.show()
260 self.auto_highlight(self.cursor_node)
261 self.cursor_node = None
262 self.edit_box_item.destroy()
264 def toggle_edit(self):
265 node = self.view.current_nodes[0]
266 attrib = self.view.current_attrib
268 if self.edit_dialog:
269 self.edit_dialog.destroy()
270 self.edit_dialog = rox.Dialog()
271 eb = self.edit_dialog
273 if node.nodeType == Node.ELEMENT_NODE:
274 if attrib:
275 text = unicode(attrib.value)
276 else:
277 text = node.nodeName
278 entry = g.Entry()
279 entry.set_text(text)
280 entry.set_activates_default(True)
281 def get_text(): return entry.get_text()
282 else:
283 text = node.nodeValue
284 entry = g.TextView()
285 buffer = entry.get_buffer()
286 buffer.insert_at_cursor(text)
287 entry.set_size_request(400, 200)
289 def get_text():
290 start = buffer.get_start_iter()
291 end = buffer.get_end_iter()
292 return buffer.get_text(start, end, True)
293 eb.vbox.pack_start(entry, TRUE, FALSE, 0)
295 eb.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
296 eb.add_button(g.STOCK_APPLY, g.RESPONSE_OK)
297 eb.set_default_response(g.RESPONSE_OK)
298 entry.grab_focus()
299 def destroy(eb):
300 self.edit_dialog = None
301 eb.connect('destroy', destroy)
302 def response(eb, resp):
303 if resp == g.RESPONSE_CANCEL:
304 eb.destroy()
305 elif resp == g.RESPONSE_OK:
306 new = get_text()
307 if new != text:
308 if attrib:
309 self.view.may_record(['set_attrib', new])
310 else:
311 self.view.may_record(['change_node', new])
312 eb.destroy()
313 eb.connect('response', response)
315 eb.show_all()
317 def menu_select_attrib(self):
318 def do_attrib(name):
319 if ':' in name:
320 (prefix, localName) = name.split(':', 1)
321 namespaceURI = self.view.model.prefix_to_namespace(self.view.get_current(), prefix)
322 else:
323 (prefix, localName) = (None, name)
324 namespaceURI = None
325 action = ["attribute", namespaceURI, localName]
326 self.view.may_record(action)
327 GetArg('Select attribute:', do_attrib, ['Name:'])
329 def menu_show_add_attrib(self):
330 def do_it(name):
331 action = ["add_attrib", "UNUSED", name]
332 self.view.may_record(action)
333 GetArg('Create attribute:', do_it, ['Name:'])
335 def menu_show_split(self):
336 def do_split(expr):
337 action = ["split", expr]
338 self.view.may_record(action)
339 GetArg('Split at:', do_split, ['Separator:'], "Split text node at:",
340 hints = ((':', 'Split colon-separated items'),
341 ('\\n', 'Split at line breaks'),
342 ('', 'Split into words (empty pattern)')
345 def menu_show_pipe(self):
346 def do_pipe(expr):
347 action = ["python", expr]
348 self.view.may_record(action)
349 GetArg('Python expression:', do_pipe, ['Eval:'], "'x' is the old text...")
351 def menu_show_xpath(self):
352 def go(expr):
353 action = ["xpath", expr]
354 self.view.may_record(action)
355 GetArg('XPath expression:', go, ['Eval:'], "Result goes on the clipboard")
357 def menu_assert(self):
358 def go(expr):
359 action = ["assert_xpath", expr]
360 self.view.may_record(action)
361 GetArg('Assert:', go, ['Test:'], "Check this is true:",
362 hints = (('. > 0', 'greater than zero'),
363 ('. = "Green"', "value is 'Green'"),
364 ('@href', "href attribute present"),
365 ('.//error', 'there are any <error>s in the subtree'),
368 def menu_hide_with_expr(self):
369 def go(expr):
370 action = ["toggle_hidden", expr]
371 self.view.may_record(action)
372 GetArg('Hide with label:', go, ['Pattern:'], "Generate each label from:",
373 hints = (('title', 'the <title> child element'),
374 ('@title', "the 'title' attribute"),
375 ('count(.//*)', 'the number of elements in the subtree'),
376 ('substring(para[1], 1, 40)', 'the start of the first paragraph'),
379 def menu_show_global(self):
380 def do_global(pattern):
381 action = ["do_global", pattern]
382 self.view.may_record(action)
383 GetArg('Select matching nodes', do_global, ['Pattern:'],
384 'Select nodes which match:',
385 hints = (('img', 'all <img> child elements'),
386 ('//img', 'all <img> elements anywhere'),
387 ('.//img', 'all <img> descendant elements'),
388 ('//img[@src]', 'all <img> elements with "src" attributes'),
389 ('//chapter[title="Intro"]',
390 'Any <chapter> with a <title>Intro</title> child'),
391 ('img[1]', 'the first child <img> element'),
392 ('text()', 'all child text nodes'),
393 ('//img[@src="@CURRENT@"]',
394 "all <img>s whose src is the current node's value"),
395 ('*[not(self::img)]',
396 "all child elements except <img> elements"),
399 def menu_show_text_search(self):
400 def do_text_search(pattern):
401 action = ["do_text_search", pattern]
402 self.view.may_record(action)
403 GetArg('Search for:', do_text_search, ['Text pattern:'],
404 '(@CURRENT@ is the current node\'s value)\n')
406 def menu_show_search(self):
407 def do_search(pattern):
408 action = ["do_search", pattern]
409 self.view.may_record(action)
410 GetArg('Search for:',
411 do_search, ['XPath:'],
412 '(@CURRENT@ is the current node\'s value)')
414 def menu_rename_attr(self):
415 def do(name):
416 action = ["rename_attrib", name]
417 self.view.may_record(action)
418 GetArg('Rename to:', do, ['New name:'])
421 def do_create(self, action, nodeType, data):
422 action = action[0]
423 qname = True
424 if nodeType == Node.ELEMENT_NODE:
425 action += 'e'
426 elif nodeType == Node.TEXT_NODE:
427 action += 't'
428 qname = False
429 elif nodeType == Node.ATTRIBUTE_NODE:
430 action += 'a'
431 else:
432 raise Exception("Unknown nodeType %d" % nodeType)
434 if qname:
435 # Check name is valid
436 # XXX: Should be more strict
437 data = data.strip()
438 assert '\n' not in data
439 assert ' ' not in data
441 self.view.may_record(['add_node', action, data])
443 def show_add_box(self, action):
444 if action[0] == 'i':
445 text = 'Insert'
446 elif action[0] == 'a':
447 text = 'Append'
448 elif action[0] == 'o':
449 text = 'Open'
450 elif action[0] == 'e':
451 text = 'Open at end'
452 else:
453 assert 0
454 if action[1] == 'e':
455 text += ' element'
456 prompt = 'Node name'
457 elif action[1] == 't':
458 text += ' text'
459 prompt = 'Text'
460 else:
461 assert 0
463 box = g.Dialog()
464 text = g.TextView()
465 box.vbox.pack_start(text, TRUE, FALSE, 0)
466 text.set_size_request(400, 200)
467 box.set_has_separator(False)
469 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
470 box.add_button('Add _Attribute', Node.ATTRIBUTE_NODE)
471 box.add_button('Add _Text', Node.TEXT_NODE)
472 box.add_button('Add _Element', Node.ELEMENT_NODE)
473 box.set_default_response(g.RESPONSE_OK)
474 text.grab_focus()
475 def response(box, resp):
476 if resp in (g.RESPONSE_CANCEL, g.RESPONSE_DELETE_EVENT):
477 box.destroy()
478 return
479 buffer = text.get_buffer()
480 start = buffer.get_start_iter()
481 end = buffer.get_end_iter()
482 new = buffer.get_text(start, end, True)
483 if new:
484 self.do_create(action, resp, new)
485 box.destroy()
486 box.connect('response', response)
488 box.show_all()
490 def new_name(self):
491 cur = self.view.get_current()
492 if cur.nodeType == Node.ELEMENT_NODE:
493 return cur.nodeName
494 return cur.parentNode.nodeName
496 def menu_insert_element(self):
497 "Insert element"
498 self.show_add_box('ie')
500 def menu_append_element(self):
501 "Append element"
502 self.show_add_box('ae')
504 def menu_open_element(self):
505 "Open element"
506 self.show_add_box('oe')
508 def menu_open_element_end(self):
509 "Open element at end"
510 self.show_add_box('ee')
512 def menu_insert_text(self):
513 "Insert text"
514 self.show_add_box('it')
516 def menu_append_text(self):
517 "Append text"
518 self.show_add_box('at')
520 def menu_open_text(self):
521 "Open text"
522 self.show_add_box('ot')
524 def menu_open_text_end(self):
525 "Open text at end"
526 self.show_add_box('et')
528 def menu_close_window(self):
529 self.parent_window.destroy()
531 def menu_options(self):
532 rox.edit_options()
534 def menu_clear_undo(self):
535 if rox.confirm('Really clear the undo buffer?',
536 g.STOCK_CLEAR):
537 self.view.model.clear_undo()
539 do_blank_all = make_do('blank_all')
540 do_enter = make_do('enter')
541 do_leave = make_do('leave')
542 do_suck = make_do('suck')
543 do_http_post = make_do('http_post')
544 do_soap_send = make_do('soap_send')
545 do_select_dups = make_do('select_dups')
546 do_paste_attribs = make_do('paste_attribs')
547 do_yank_value = make_do('yank_value')
548 do_yank_attributes = make_do('yank_attribs')
549 do_delete_node = make_do('delete_node')
550 do_delete_node_no_clipboard = make_do('delete_node_no_clipboard')
551 do_delete_shallow = make_do('delete_shallow')
552 do_yank = make_do('yank')
553 do_shallow_yank = make_do('shallow_yank')
554 do_put_replace = make_do('put_replace')
555 do_put_as_child = make_do('put_as_child')
556 do_put_before = make_do('put_before')
557 do_put_after = make_do('put_after')
558 do_undo = make_do('undo')
559 do_redo = make_do('redo')
560 do_fail = make_do('fail')
561 do_pass = make_do('do_pass')
562 do_toggle_hidden = make_do('toggle_hidden')
563 do_show_html = make_do('show_html')
564 do_show_canvas = make_do('show_canvas')
565 do_compare = make_do('compare')
566 do_again = make_do('again')
567 do_normalise = make_do('normalise')
568 do_convert_to_text = make_do('convert_to_text')
569 do_convert_to_comment = make_do('convert_to_comment')
570 do_convert_to_element = make_do('convert_to_element')
571 do_convert_to_pi = make_do('convert_to_pi')
572 do_remove_ns = make_do('remove_ns')
574 do_clear_mark = make_do('clear_mark')
575 do_move_marked = make_do('move_marked')
576 do_mark_switch = make_do('mark_switch')
577 do_mark_selection = make_do('mark_selection')
578 do_select_marked = make_do('select_marked_region')
579 do_select_children = make_do('select_children')
581 move_home = make_do('move_home')
582 move_end = make_do('move_end')
583 move_left = make_do('move_left')
584 move_right = make_do('move_right')
585 move_next_sib = make_do('move_next_sib')
586 move_prev_sib = make_do('move_prev_sib')