Fix relative URI handling.
[dom-editor.git] / Dome / GUIView.py
bloba801fc5407fbcb9eaf9c11353e8ef721c7f70f69
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 Display 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', '', 'F3'),
20 ('/File/Blank document', 'do_blank_all', '', '<Ctrl>N'),
21 ('/File/Clear undo buffer', 'menu_clear_undo', '', ''),
23 ('/Edit', None, '<Branch>', ''),
24 ('/Edit/Yank attributes', 'do_yank_attributes', '', ''),
25 ('/Edit/Paste attributes', 'do_paste_attribs', '', ''),
26 ('/Edit/Yank attrib value', 'do_yank_value', '', ''),
27 ('/Edit/Rename attribute', 'menu_rename_attr', '', ''),
28 ('/Edit/', '', '', '<separator>'),
29 ('/Edit/Cut', 'do_delete_node', '', 'x'),
30 ('/Edit/Delete', 'do_delete_node_no_clipboard', '', '<Ctrl>X'),
31 ('/Edit/Shallow cut', 'do_delete_shallow', '', '<Shift>X'),
32 ('/Edit/', '', '', '<separator>'),
33 ('/Edit/Yank', 'do_yank', '', 'y'),
34 ('/Edit/Shallow yank', 'do_shallow_yank', '', '<Shift>Y'),
35 ('/Edit/', '', '', '<separator>'),
36 ('/Edit/Paste (replace)','do_put_replace', '', '<Shift>R'),
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', '', 'u'),
44 ('/Edit/Redo', 'do_redo', '', '<Ctrl>R'),
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 element', 'menu_insert_element', '', '<Shift>I'),
79 ('/Create/Append element', 'menu_append_element', '', '<Shift>A'),
80 ('/Create/Open element', 'menu_open_element', '', '<Shift>O'),
81 ('/Create/Open element at end', 'menu_open_element_end', '', '<Shift>E'),
83 ('/Create/', '', '', '<separator>'),
85 ('/Create/Insert text node', 'menu_insert_text', '', 'I'),
86 ('/Create/Append text node', 'menu_append_text', '', 'A'),
87 ('/Create/Open text node', 'menu_open_text', '', 'O'),
88 ('/Create/Open text node at end', 'menu_open_text_end', '', 'E'),
90 ('/Create/', '', '', '<separator>'),
92 ('/Create/Attribute', 'menu_show_add_attrib', '', '<Shift>plus'),
94 ('/Process', None, '<Branch>', ''),
95 ('/Process/Substitute', 'menu_show_subst', '', 's'),
96 ('/Process/Python expression', 'menu_show_pipe', '', '<Shift>exclam'),
97 ('/Process/XPath expression', 'menu_show_xpath', '', ''),
98 ('/Process/Normalise', 'do_normalise', '', ''),
99 ('/Process/Remove default namespaces', 'do_remove_ns', '', 'r'),
100 ('/Process/Comment to text', 'do_convert_to_text', '', ''),
102 ('/Program', None, '<Branch>', ''),
103 ('/Program/Input', 'menu_show_ask', '', 'question'),
104 ('/Program/Compare', 'do_compare', '', 'equal'),
105 ('/Program/Fail', 'do_fail', '', ''),
106 ('/Program/Pass', 'do_pass', '', ''),
107 ('/Program/Repeat last', 'do_again', '', 'dot'),
109 ('/View', None, '<Branch>', ''),
110 ('/View/Toggle hidden', 'do_toggle_hidden', '', '<Ctrl>H'),
111 ('/View/Show as HTML', 'do_show_html', '', ''),
112 ('/View/Show as canvas', 'do_show_canvas', '', ''),
113 ('/View/Close Window', 'menu_close_window', '', '<Ctrl>Q'),
115 ('/Options...', 'menu_options', '', '<Ctrl>O'),
118 def make_do(action):
119 return lambda(self): self.view.may_record([action])
121 class GUIView(Display, XDSLoader):
122 def __init__(self, window, view):
123 Display.__init__(self, window, view)
124 XDSLoader.__init__(self, ['application/x-dome', 'text/xml',
125 'application/xml'])
126 window.connect('key-press-event', self.key_press)
127 self.cursor_node = None
128 self.update_state()
130 menu.attach(window, self)
132 def update_state(self):
133 if self.view.rec_point:
134 state = "(recording)"
135 elif self.view.idle_cb or self.view.op_in_progress:
136 state = "(playing)"
137 else:
138 state = ""
139 self.parent_window.set_state(state)
140 self.do_update_now()
142 def xds_load_from_stream(self, path, type, stream):
143 if not path:
144 raise Exception('Can only load from files... sorry!')
145 if path.endswith('.html'):
146 self.view.load_html(path)
147 else:
148 self.view.load_xml(path)
149 if self.view.root == self.view.model.get_root():
150 self.parent_window.uri = path
151 self.parent_window.update_title()
153 def key_press(self, widget, kev):
154 focus = widget.focus_widget
155 if focus and focus is not widget and focus.get_toplevel() is widget:
156 if focus.event(kev):
157 return TRUE # Handled
159 if self.cursor_node:
160 return 0
161 if kev.keyval == keysyms.Up:
162 self.view.may_record(['move_prev_sib'])
163 elif kev.keyval == keysyms.Down:
164 self.view.may_record(['move_next_sib'])
165 elif kev.keyval == keysyms.Left:
166 self.view.may_record(['move_left'])
167 elif kev.keyval == keysyms.Right:
168 self.view.may_record(['move_right'])
169 elif kev.keyval == keysyms.KP_Add:
170 self.menu_show_add_attrib()
171 elif kev.keyval == keysyms.Tab:
172 self.toggle_edit()
173 else:
174 return 0
175 return 1
177 def node_clicked(self, node, bev):
178 print "Clicked", node.namespaceURI, node.localName
179 if node:
180 if bev.type == g.gdk.BUTTON_PRESS:
181 if len(self.view.current_nodes) == 0:
182 src = self.view.root
183 else:
184 src = self.view.current_nodes[-1]
185 shift = bev.state & g.gdk.SHIFT_MASK
186 add = bev.state & g.gdk.CONTROL_MASK
187 select_region = shift and node.nodeType == Node.ELEMENT_NODE
188 lit = shift and not select_region
190 ns = {}
191 path = make_relative_path(src, node, lit, ns)
192 if path == '.' and self.view.current_nodes and not self.view.current_attrib:
193 return
194 if select_region:
195 self.view.may_record(["select_region", path, ns])
196 else:
197 self.view.may_record(["do_search", path, ns, add])
198 else:
199 self.view.may_record(["toggle_hidden"])
201 def attrib_clicked(self, element, attrib, event):
202 if len(self.view.current_nodes) == 0:
203 src = self.view.root
204 else:
205 src = self.view.current_nodes[-1]
206 ns = {}
208 print "attrib_clicked", attrib, attrib.namespaceURI, attrib.localName
209 path = make_relative_path(src, element, FALSE, ns)
210 if path != '.':
211 self.view.may_record(["do_search", path, ns, FALSE])
212 self.view.may_record(["attribute", attrib.namespaceURI, attrib.localName])
214 def menu_save(self):
215 self.parent_window.save()
217 def show_menu(self, bev):
218 menu.popup(self, bev)
220 def playback(self, macro, map):
221 "Called when the user clicks on a macro button."
222 Exec.exec_state.clean()
223 if map:
224 self.view.may_record(['map', macro.uri])
225 else:
226 self.view.may_record(['play', macro.uri])
228 def menu_show_ask(self):
229 def do_ask(q, self = self):
230 action = ["ask", q]
231 self.view.may_record(action)
232 GetArg('Input:', do_ask, ('Prompt:',))
234 def menu_show_subst(self):
235 def do_subst(args, self = self):
236 action = ["subst", args[0], args[1]]
237 self.view.may_record(action)
238 GetArg('Substitute:', do_subst, ('Replace:', 'With:'))
240 def move_from(self, old = []):
241 self.hide_editbox()
242 Display.move_from(self, old)
244 def hide_editbox(self):
245 if self.cursor_node:
246 if self.cursor_attrib:
247 self.cursor_hidden_text.set(text = '%s=%s' %
248 (self.cursor_attrib.name, self.cursor_attrib.value))
249 self.cursor_hidden_text.show()
250 self.auto_highlight(self.cursor_node)
251 self.cursor_node = None
252 self.edit_box_item.destroy()
254 def show_editbox(self):
255 "Edit the current node/attribute"
256 self.do_update_now()
258 if self.cursor_node:
259 self.hide_editbox()
261 if not self.visible:
262 raise Exception("Can't edit while display is hidden!")
264 self.cursor_node = self.view.current_nodes[0]
265 group = self.node_to_group[self.cursor_node]
266 self.cursor_attrib = self.view.current_attrib
268 self.highlight(group, FALSE)
270 if self.cursor_attrib:
271 group = group.attrib_to_group[self.cursor_attrib]
273 self.cursor_hidden_text = group.text
274 if not self.cursor_attrib:
275 # Don't hide for attributes, so we can still see the name
276 group.text.hide()
277 else:
278 group.text.set(text = str(self.cursor_attrib.name) + '=')
280 self.update_now() # GnomeCanvas bug?
281 lx, ly, hx, hy = group.text.get_bounds()
282 x, y = group.i2w(lx, ly)
284 text = g.TextView()
285 text.show()
287 eb = g.Frame()
288 eb.add(text)
289 self.edit_box = eb
290 self.edit_box_text = text
291 m = 3
293 #s = eb.get_style().copy()
294 #s.font = load_font('fixed')
295 #eb.set_style(s)
296 #if self.cursor_attrib:
297 # name_width = s.font.measure(self.cursor_attrib.name + '=') + 1
298 #else:
299 # name_width = 0
300 name_width = 0
302 self.edit_box_item = self.root().add(canvas.CanvasWidget, widget = eb,
303 x = x - m + name_width, y = y - m,
304 anchor = g.ANCHOR_NW)
306 #text.set_editable(TRUE)
307 text.get_buffer().insert_at_cursor(self.get_edit_text(), -1)
308 text.set_wrap_mode(g.WRAP_NONE)
309 text.get_buffer().connect('changed', self.eb_changed)
310 text.connect('key-press-event', self.eb_key)
311 eb.show()
312 text.realize()
313 self.size_eb()
314 text.grab_focus()
315 #eb.select_region(0, -1)
316 eb.show()
318 def get_edit_text(self):
319 node = self.cursor_node
320 if node.nodeType == Node.ELEMENT_NODE:
321 if self.cursor_attrib:
322 return str(self.cursor_attrib.value)
323 return node.nodeName
324 else:
325 return node.nodeValue
327 def eb_key(self, eb, kev):
328 key = kev.keyval
329 if key == g.keysyms.KP_Enter:
330 key = g.keysyms.Return
331 if key == g.keysyms.Escape:
332 self.hide_editbox()
333 elif key == g.keysyms.Return and kev.state & g.gdk.CONTROL_MASK:
334 eb.insert_defaults('\n')
335 self.size_eb()
336 elif key == g.keysyms.Tab or key == g.keysyms.Return:
337 buffer = eb.get_buffer()
338 s = buffer.get_start_iter()
339 e = buffer.get_end_iter()
340 text = buffer.get_text(s, e, TRUE)
341 try:
342 if text != self.get_edit_text():
343 self.commit_edit(text)
344 finally:
345 self.hide_editbox()
346 return 0
348 def commit_edit(self, new):
349 if self.cursor_attrib:
350 self.view.may_record(['set_attrib', new])
351 else:
352 self.view.may_record(['change_node', new])
354 def eb_changed(self, eb):
355 self.size_eb()
357 def size_eb(self):
358 def cb():
359 req = self.edit_box_text.size_request()
360 print "Wants", req
361 width = max(req[0], 10)
362 height = max(req[1], 10)
363 self.edit_box_item.set(width = width + 12, height = height + 4)
364 g.idle_add(cb)
366 def toggle_edit(self):
367 if self.cursor_node:
368 self.hide_editbox()
369 else:
370 self.show_editbox()
372 def menu_select_attrib(self):
373 def do_attrib(name):
374 if ':' in name:
375 (prefix, localName) = name.split(':', 1)
376 else:
377 (prefix, localName) = (None, name)
378 namespaceURI = self.view.model.prefix_to_namespace(self.view.get_current(), prefix)
379 action = ["attribute", namespaceURI, localName]
380 self.view.may_record(action)
381 GetArg('Select attribute:', do_attrib, ['Name:'])
383 def menu_show_add_attrib(self):
384 def do_it(name):
385 action = ["add_attrib", "UNUSED", name]
386 self.view.may_record(action)
387 GetArg('Create attribute:', do_it, ['Name:'])
389 def menu_show_pipe(self):
390 def do_pipe(expr):
391 action = ["python", expr]
392 self.view.may_record(action)
393 GetArg('Python expression:', do_pipe, ['Eval:'], "'x' is the old text...")
395 def menu_show_xpath(self):
396 def go(expr):
397 action = ["xpath", expr]
398 self.view.may_record(action)
399 GetArg('XPath expression:', go, ['Eval:'], "Result goes on the clipboard")
401 def menu_show_global(self):
402 def do_global(pattern):
403 action = ["do_global", pattern]
404 self.view.may_record(action)
405 GetArg('Global:', do_global, ['Pattern:'],
406 '(@CURRENT@ is the current node\'s value)\n' +
407 'Perform next action on all nodes matching')
409 def menu_show_text_search(self):
410 def do_text_search(pattern):
411 action = ["do_text_search", pattern]
412 self.view.may_record(action)
413 GetArg('Search for:', do_text_search, ['Text pattern:'],
414 '(@CURRENT@ is the current node\'s value)\n')
416 def menu_show_search(self):
417 def do_search(pattern):
418 action = ["do_search", pattern]
419 self.view.may_record(action)
420 GetArg('Search for:',
421 do_search, ['XPath:'],
422 '(@CURRENT@ is the current node\'s value)')
424 def menu_rename_attr(self):
425 def do(name):
426 action = ["rename_attrib", name]
427 self.view.may_record(action)
428 GetArg('Rename to:', do, ['New name:'])
431 def show_add_box(self, action):
432 if action[0] == 'i':
433 text = 'Insert'
434 elif action[0] == 'a':
435 text = 'Append'
436 elif action[0] == 'o':
437 text = 'Open'
438 elif action[0] == 'e':
439 text = 'Open at end'
440 else:
441 assert 0
442 if action[1] == 'e':
443 text += ' element'
444 prompt = 'Node name'
445 elif action[1] == 't':
446 text += ' text'
447 prompt = 'Text'
448 else:
449 assert 0
451 def cb(value):
452 self.view.may_record(['add_node', action, value])
453 GetArg('Add node', cb, [prompt], text)
455 def new_name(self):
456 cur = self.view.get_current()
457 if cur.nodeType == Node.ELEMENT_NODE:
458 return cur.nodeName
459 return cur.parentNode.nodeName
461 def menu_insert_element(self):
462 "Insert element"
463 self.show_add_box('ie')
465 def menu_append_element(self):
466 "Append element"
467 self.show_add_box('ae')
469 def menu_open_element(self):
470 "Open element"
471 self.show_add_box('oe')
473 def menu_open_element_end(self):
474 "Open element at end"
475 self.show_add_box('ee')
477 def menu_insert_text(self):
478 "Insert text"
479 self.show_add_box('it')
481 def menu_append_text(self):
482 "Append text"
483 self.show_add_box('at')
485 def menu_open_text(self):
486 "Open text"
487 self.show_add_box('ot')
489 def menu_open_text_end(self):
490 "Open text at end"
491 self.show_add_box('et')
493 def menu_close_window(self):
494 self.parent_window.destroy()
496 def menu_options(self):
497 rox.edit_options()
499 def menu_clear_undo(self):
500 if rox.confirm('Really clear the undo buffer?',
501 g.STOCK_CLEAR):
502 self.view.model.clear_undo()
504 do_blank_all = make_do('blank_all')
505 do_enter = make_do('enter')
506 do_leave = make_do('leave')
507 do_suck = make_do('suck')
508 do_http_post = make_do('http_post')
509 do_soap_send = make_do('soap_send')
510 do_select_dups = make_do('select_dups')
511 do_paste_attribs = make_do('paste_attribs')
512 do_yank_value = make_do('yank_value')
513 do_yank_attributes = make_do('yank_attribs')
514 do_delete_node = make_do('delete_node')
515 do_delete_node_no_clipboard = make_do('delete_node_no_clipboard')
516 do_delete_shallow = make_do('delete_shallow')
517 do_yank = make_do('yank')
518 do_shallow_yank = make_do('shallow_yank')
519 do_put_replace = make_do('put_replace')
520 do_put_as_child = make_do('put_as_child')
521 do_put_before = make_do('put_before')
522 do_put_after = make_do('put_after')
523 do_undo = make_do('undo')
524 do_redo = make_do('redo')
525 do_fail = make_do('fail')
526 do_pass = make_do('do_pass')
527 do_toggle_hidden = make_do('toggle_hidden')
528 do_show_html = make_do('show_html')
529 do_show_canvas = make_do('show_canvas')
530 do_compare = make_do('compare')
531 do_again = make_do('again')
532 do_normalise = make_do('normalise')
533 do_convert_to_text = make_do('convert_to_text')
534 do_remove_ns = make_do('remove_ns')
536 do_clear_mark = make_do('clear_mark')
537 do_mark_switch = make_do('mark_switch')
538 do_mark_selection = make_do('mark_selection')
539 do_select_marked = make_do('select_marked_region')
540 do_select_children = make_do('select_children')
542 move_home = make_do('move_home')
543 move_end = make_do('move_end')
544 move_left = make_do('move_left')
545 move_right = make_do('move_right')
546 move_next_sib = make_do('move_next_sib')
547 move_prev_sib = make_do('move_prev_sib')