Send rox-changed instead of row-deleted/row-inserted.
[dom-editor.git] / Dome / Model.py
blobfdfddff7f2f79d153393405500afced2c80c9c57
1 from __future__ import nested_scopes
3 # An model contains:
4 # - A DOM document
5 # - The undo history
6 # - The root program
7 # All changes to the DOM must go through here.
8 # Notification to views of changes is done.
10 from Ft.Xml.cDomlette import implementation, nonvalParse
11 from Ft.Xml.Domlette import GetAllNs
12 from Ft.Xml import XMLNS_NAMESPACE
14 from xml.dom import Node
15 import support
16 from Beep import Beep
18 def get_xslt_source(doc, dome_data):
19 print "get_xslt_source", dome_data
20 src = doc.createElementNS(None, 'Source')
21 if file:
22 src.appendChild(support.import_with_ns(doc, dome_data.documentElement))
23 return src
25 class Model:
26 def __init__(self, path, root_program = None, dome_data = None, do_load = 1):
27 "If root_program is given, then no data is loaded (used for lock_and_copy)."
28 self.uri = 'Prog.dome'
29 import Namespaces
30 self.namespaces = Namespaces.Namespaces()
32 if dome_data:
33 from Ft.Xml.InputSource import InputSourceFactory
34 isrc = InputSourceFactory()
35 dome_data = nonvalParse(isrc.fromUri(dome_data))
37 self.clear_undo()
39 doc = None
40 if path:
41 if path != '-':
42 self.uri = path
43 if do_load and (path.endswith('.html') or path.endswith('.htm')):
44 doc = self.load_html(path)
45 if not doc and not root_program:
46 from Ft.Xml.InputSource import InputSourceFactory
47 isrc = InputSourceFactory()
48 doc = nonvalParse(isrc.fromUri(path))
49 if not doc:
50 doc = implementation.createDocument(None, 'root', None)
51 root = doc.documentElement
53 self.root_program = None
54 data_to_load = None
56 from Program import Program, load_dome_program
57 import constants
58 if root.namespaceURI == constants.DOME_NS and root.localName == 'dome':
59 for x in root.childNodes:
60 if x.namespaceURI == constants.DOME_NS:
61 if x.localName == 'dome-program':
62 self.root_program = load_dome_program(x)
63 elif x.localName == 'dome-data':
64 for y in x.childNodes:
65 if y.nodeType == Node.ELEMENT_NODE:
66 data_to_load = y
67 if dome_data:
68 data_to_load = dome_data.documentElement
69 elif (root.namespaceURI == constants.XSLT_NS and
70 root.localName in ['stylesheet', 'transform']) or \
71 root.hasAttributeNS(constants.XSLT_NS, 'version'):
72 import xslt
73 self.root_program = xslt.import_sheet(doc)
74 x = implementation.createDocument(None, 'xslt', None)
75 data_to_load = x.documentElement
76 src = get_xslt_source(x, dome_data)
77 data_to_load.appendChild(x.createElementNS(None, 'Result'))
78 data_to_load.appendChild(src)
79 dome_data = None
80 else:
81 data_to_load = root
83 if root_program:
84 self.root_program = root_program
85 else:
86 if not self.root_program:
87 self.root_program = Program('Root')
89 if data_to_load:
90 self.doc = implementation.createDocument(None, 'root', None)
91 if not root_program:
92 node = support.import_with_ns(self.doc, data_to_load)
93 self.doc.replaceChild(node, self.doc.documentElement)
94 self.strip_space()
96 self.views = [] # Notified when something changes
97 self.locks = {} # Node -> number of locks
99 def clear_undo(self):
100 # Pop an (op_number, function) off one of these and call the function to
101 # move forwards or backwards in the undo history.
102 self.undo_stack = []
103 self.redo_stack = []
104 self.doing_undo = 0
106 # Each series of suboperations in an undo stack which are part of a single
107 # user op will have the same number...
108 self.user_op = 1
110 #import gc
111 #print "GC freed:", gc.collect()
112 #print "Garbage", gc.garbage
114 def lock(self, node):
115 """Prevent removal of this node (or any ancestor)."""
116 #print "Locking", node.nodeName
117 self.locks[node] = self.get_locks(node) + 1
118 if node.parentNode:
119 self.lock(node.parentNode)
121 def unlock(self, node):
122 """Reverse the effect of lock(). Must call unlock the same number
123 of times as lock to fully unlock the node."""
124 l = self.get_locks(node)
125 if l > 1:
126 self.locks[node] = l - 1
127 elif l == 1:
128 del self.locks[node] # Or get a memory leak...
129 if node == self.doc.documentElement:
130 if node.parentNode:
131 self.unlock(node.parentNode)
132 return
133 else:
134 raise Exception('unlock(%s): Node not locked!' % node)
135 if node.parentNode:
136 self.unlock(node.parentNode)
138 def get_locks(self, node):
139 try:
140 return self.locks[node]
141 except KeyError:
142 return 0
144 def lock_and_copy(self, node):
145 """Locks 'node' in the current model and returns a new model
146 with a copy of the subtree."""
147 if self.get_locks(node):
148 raise Exception("Can't enter locked node!")
149 m = Model(self.get_base_uri(node), root_program = self.root_program, do_load = 0)
150 copy = support.import_with_ns(m.doc, node)
151 root = m.get_root()
152 m.replace_node(root, copy)
153 self.lock(node)
154 return m
156 def mark(self):
157 "Increment the user_op counter. Undo will undo every operation between"
158 "two marks."
159 self.user_op += 1
161 def get_root(self):
162 "Return the true root node (not a view root)"
163 return self.doc.documentElement
165 def add_view(self, view):
166 "'view' provides:"
167 "'update_all(subtree) - called when a major change occurs."
168 #print "New view:", view
169 self.views.append(view)
171 def remove_view(self, view):
172 #print "Removing view", view
173 self.views.remove(view)
174 #print "Now:", self.views
175 if not self.views:
176 self.clear_undo()
178 def update_all(self, node):
179 "Called when 'node' has been updated."
180 "'node' is still in the document, so deleting or replacing"
181 "a node calls this on the parent."
182 for v in self.views:
183 v.update_all(node)
185 def update_replace(self, old, new):
186 "Called when 'old' is replaced by 'new'."
187 for v in self.views:
188 v.update_replace(old, new)
190 def strip_space(self, node = None):
191 if not node:
192 node = self.doc.documentElement
193 def ss(node):
194 if node.nodeType == Node.TEXT_NODE:
195 #node.data = node.data.strip()
196 #if node.data == '':
197 # node.parentNode.removeChild(node)
198 if not node.data.strip():
199 node.parentNode.removeChild(node)
200 else:
201 for k in node.childNodes[:]:
202 ss(k)
203 ss(node)
205 # Changes
207 def normalise(self, node):
208 old = node.cloneNode(1)
209 node.normalize()
210 self.add_undo(lambda: self.replace_node(node, old))
211 self.update_all(node)
213 def convert_to_element(self, node):
214 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
215 Node.TEXT_NODE)
216 new = self.doc.createElementNS(None, node.data.strip())
217 self.replace_node(node, new)
218 return new
220 def convert_to_text(self, node):
221 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
222 Node.TEXT_NODE, Node.ELEMENT_NODE)
223 if node.nodeType == Node.ELEMENT_NODE:
224 new = self.doc.createTextNode(node.localName)
225 else:
226 new = self.doc.createTextNode(node.data)
227 self.replace_node(node, new)
228 return new
230 def convert_to_comment(self, node):
231 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
232 Node.TEXT_NODE)
233 new = self.doc.createComment(node.data)
234 self.replace_node(node, new)
235 return new
237 def remove_ns(self, node):
238 nss = GetAllNs(node.parentNode)
239 dns = nss.get(None, None)
240 create = node.ownerDocument.createElementNS
241 # clone is an argument to fix a wierd gc bug in python2.2
242 def ns_clone(node, clone):
243 if node.nodeType != Node.ELEMENT_NODE:
244 return node.cloneNode(1)
245 new = create(dns, node.nodeName)
246 for a in node.attributes.values():
247 if a.localName == 'xmlns' and a.prefix is None:
248 print "Removing xmlns attrib on", node
249 continue
250 new.setAttributeNS(a.namespaceURI, a.name, a.value)
251 for k in node.childNodes:
252 new.appendChild(clone(k, clone))
253 return new
254 new = ns_clone(node, ns_clone)
255 self.replace_node(node, new)
256 return new
258 def set_name(self, node, namespace, name):
259 if self.get_locks(node):
260 raise Exception('Attempt to set name on locked node %s' % node)
262 new = node.ownerDocument.createElementNS(namespace, name)
263 self.replace_shallow(node, new)
264 return new
266 def replace_shallow(self, old, new):
267 """Replace old with new, keeping the old children."""
268 assert not new.childNodes
269 assert not new.parentNode
271 old_name = old.nodeName
272 old_ns = old.namespaceURI
274 kids = old.childNodes[:]
275 attrs = old.attributes.values()
276 parent = old.parentNode
277 [ old.removeChild(k) for k in kids ]
278 parent.replaceChild(new, old)
279 [ new.appendChild(k) for k in kids ]
280 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
282 self.add_undo(lambda: self.replace_shallow(new, old))
284 self.update_replace(old, new)
286 import __main__
287 if __main__.no_gui_mode:
288 def add_undo(self, fn):
289 pass
290 else:
291 def add_undo(self, fn):
292 self.undo_stack.append((self.user_op, fn))
293 if not self.doing_undo:
294 self.redo_stack = []
296 def set_data(self, node, data):
297 old_data = node.data
298 node.data = data
299 self.add_undo(lambda: self.set_data(node, old_data))
300 self.update_all(node)
302 def replace_node(self, old, new):
303 if self.get_locks(old):
304 raise Exception('Attempt to replace locked node %s' % old)
305 old.parentNode.replaceChild(new, old)
306 self.add_undo(lambda: self.replace_node(new, old))
308 self.update_replace(old, new)
310 def delete_shallow(self, node):
311 """Replace node with its contents"""
312 kids = node.childNodes[:]
313 next = node.nextSibling
314 parent = node.parentNode
315 for n in kids + [node]:
316 if self.get_locks(n):
317 raise Exception('Attempt to move/delete locked node %s' % n)
318 for k in kids:
319 self.delete_internal(k)
320 self.delete_internal(node)
321 for k in kids:
322 self.insert_before_interal(next, k, parent)
323 self.update_all(parent)
325 def delete_nodes(self, nodes):
326 #print "Deleting", nodes
327 for n in nodes:
328 if self.get_locks(n):
329 raise Exception('Attempt to delete locked node %s' % n)
330 for n in nodes:
331 p = n.parentNode
332 self.delete_internal(n)
333 self.update_all(p)
335 def delete_internal(self, node):
336 "Doesn't update display."
337 next = node.nextSibling
338 parent = node.parentNode
339 parent.removeChild(node)
340 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
342 def insert_before_interal(self, node, new, parent):
343 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
344 "of parent's children."
345 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
346 assert parent.nodeType == Node.ELEMENT_NODE
347 parent.insertBefore(new, node)
348 self.add_undo(lambda: self.delete_nodes([new]))
350 def undo(self):
351 if not self.undo_stack:
352 raise Exception('Nothing to undo')
354 assert not self.doing_undo
356 uop = self.undo_stack[-1][0]
358 # Swap stacks so that the undo actions will populate the redo stack...
359 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
360 self.doing_undo = 1
361 try:
362 while self.redo_stack and self.redo_stack[-1][0] == uop:
363 self.redo_stack[-1][1]()
364 self.redo_stack.pop()
365 finally:
366 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
367 self.doing_undo = 0
369 def redo(self):
370 if not self.redo_stack:
371 raise Exception('Nothing to redo')
373 uop = self.redo_stack[-1][0]
374 self.doing_undo = 1
375 try:
376 while self.redo_stack and self.redo_stack[-1][0] == uop:
377 self.redo_stack[-1][1]()
378 self.redo_stack.pop()
379 finally:
380 self.doing_undo = 0
382 def insert(self, node, new, index = 0):
383 if len(node.childNodes) > index:
384 self.insert_before(node.childNodes[index], new)
385 else:
386 self.insert_before(None, new, parent = node)
388 def insert_after(self, node, new):
389 self.insert_before(node.nextSibling, new, parent = node.parentNode)
391 def insert_before(self, node, new, parent = None):
392 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
393 "of parent's children."
394 if not parent:
395 parent = node.parentNode
396 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
397 for n in new.childNodes[:]:
398 self.insert_before_interal(node, n, parent)
399 else:
400 self.insert_before_interal(node, new, parent)
401 self.update_all(parent)
403 def split_qname(self, node, name):
404 if name == 'xmlns':
405 namespaceURI = XMLNS_NAMESPACE
406 localName = name
407 elif ':' in name:
408 prefix, localName = name.split(':')
409 namespaceURI = self.prefix_to_namespace(node, prefix)
410 else:
411 namespaceURI = None
412 localName = name
413 return namespaceURI, localName
415 def set_attrib(self, node, name, value, with_update = 1):
416 """Set an attribute's value. If value is None, remove the attribute.
417 Returns the new attribute node, or None if removing."""
418 namespaceURI, localName = self.split_qname(node, name)
420 if node.hasAttributeNS(namespaceURI, localName):
421 old = node.getAttributeNS(namespaceURI, localName)
422 else:
423 old = None
424 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
425 if value != None:
426 node.setAttributeNS(namespaceURI, name, value)
427 else:
428 node.removeAttributeNS(namespaceURI, localName)
430 self.add_undo(lambda: self.set_attrib(node, name, old))
432 if with_update:
433 self.update_all(node)
434 if value != None:
435 if localName == 'xmlns':
436 localName = None
437 return node.attributes[(namespaceURI, localName)]
439 def prefix_to_namespace(self, node, prefix):
440 "Use the xmlns attributes to workout the namespace."
441 nss = GetAllNs(node)
442 if nss.has_key(prefix):
443 return nss[prefix] or None
444 if prefix:
445 if prefix == 'xmlns':
446 return XMLNS_NAMESPACE
447 raise Exception("No such namespace prefix '%s'" % prefix)
448 else:
449 return None
451 def get_base_uri(self, node):
452 """Go up through the parents looking for a uri attribute.
453 If there isn't one, use the document's URI."""
454 while node:
455 if node.nodeType == Node.DOCUMENT_NODE:
456 return self.uri
457 if node.hasAttributeNS(None, 'uri'):
458 return node.getAttributeNS(None, 'uri')
459 node = node.parentNode
460 return None
462 def load_html(self, path):
463 """Load this HTML file and return the new document."""
464 data = file(path).read()
465 data = support.to_html_doc(data)
466 doc = support.parse_data(data, path)
467 self.strip_space(doc)
468 return doc