Removed all support for multiple parent nodes.
[dom-editor.git] / Dome / Program.py
blobe050e8e2ee369a43fb7a112f8b46170f38fe8a49
1 from __future__ import nested_scopes
2 import string
3 from constants import DOME_NS
4 from xml.dom import Node
6 def el_named(node, name):
7 for n in node.childNodes:
8 if n.localName == name:
9 return n
10 return None
12 def bool_attr(node, attr):
13 return {"0": False, "1": True,
14 "False": False, "True": True}[node.getAttributeNS(None, attr)]
16 # Converts a DOM <block> node to a Block object.
17 def load(node, parent, ns):
18 #assert node.localName == 'block'
20 block = Block(parent)
21 prev = block.start
22 try:
23 if bool_attr(node, 'foreach'):
24 block.toggle_foreach()
25 if bool_attr(node, 'enter'):
26 block.toggle_enter()
27 if node.hasAttributeNS(None, 'restore'):
28 if bool_attr(node, 'restore'):
29 block.toggle_restore()
30 comment = node.getAttributeNS(None, 'comment')
31 block.set_comment(comment)
32 except:
33 pass
35 id_hash = {} # id from file -> Op
36 to_link = []
37 def _load(chain, prev, exit):
38 for op_node in chain.childNodes:
39 if str(op_node.localName) == 'node':
40 attr = op_node.getAttributeNS(None, 'action')
41 action = eval(str(attr))
42 if action[0] == 'Start':
43 print "Skipping Start"
44 continue
45 if action[0] == 'chroot':
46 action[0] = 'enter'
47 elif action[0] == 'unchroot':
48 action[0] = 'leave'
49 #elif action[0] == 'set_attrib':
50 # if action[3] == '':
51 # action = ('add_attrib', action[1], action[2])
52 # else:
53 # action = ('set_attrib', action[3])
54 elif action[0] == 'playback':
55 action[0] = 'map'
56 elif action[0] == 'add_attrib':
57 action[1] = "UNUSED"
58 elif action[0] == 'do_search' and type(action[-1]) is dict:
59 print "Converting search namespaces..."
60 for p, u in action[-1].iteritems():
61 print "Convert", p, u
62 old = p
63 if p.startswith('_'): p = None
64 p = ns.ensure_ns(p, u)
65 action[1] = action[1].replace(old + ':',
66 p + ':')
67 action[2] = 'unused'
68 op = Op(action)
69 elif op_node.localName == 'block':
70 op = load(op_node, block, ns)
71 else:
72 if op_node.nodeType == Node.ELEMENT_NODE and op_node.localName != 'fail':
73 print "** WARNING ** Unknown op:", op_node
74 continue
76 try:
77 dx = int(float(op_node.getAttributeNS(None, 'dx')))
78 if dx:
79 op.dx = dx
80 dy = int(float(op_node.getAttributeNS(None, 'dy')))
81 if dy:
82 op.dy = dy
83 except:
84 pass
86 node_id = op_node.getAttributeNS(None, 'id')
87 if node_id:
88 id_hash[node_id] = op
90 prev.link_to(op, exit)
91 exit = 'next'
92 prev = op
94 if op_node.localName == 'block':
95 # Block nodes have a special failure child
96 for x in op_node.childNodes:
97 if x.localName == 'fail':
98 _load(x, op, 'fail')
99 break
100 else:
101 # If the new node has children then they are the failure case
102 _load(op_node, op, 'fail')
104 link = op_node.getAttributeNS(None, 'target_fail')
105 if link:
106 to_link.append((op, 'fail', link))
107 link = op_node.getAttributeNS(None, 'target_next')
108 if link:
109 to_link.append((op, 'next', link))
110 _load(node, block.start, 'next')
111 for (op, exit, child) in to_link:
112 try:
113 to = id_hash[child]
114 except:
115 print "**** Not adding link to unknown ID ****"
116 else:
117 op.link_to(to, exit)
118 return block
120 def load_dome_program(prog, ns):
121 "prog should be a DOM 'dome-program' node. ns will be updated"
122 import Namespaces
123 assert isinstance(ns, Namespaces.Namespaces)
124 #print "Loading", prog
125 if prog.localName != 'dome-program':
126 raise Exception('Not a DOME program: %s!' % prog)
128 new = Program(str(prog.getAttributeNS(None, 'name')))
130 #print "Loading '%s'..." % new.name
131 done_update = 0
133 for node in prog.childNodes:
134 if node.localName == 'node' and not done_update:
135 print "*** Converting from old format ***"
136 new.code = load(prog, new, ns)
137 done_update = 1
138 if node.localName == 'block':
139 assert not done_update
140 new.code = load(node, new, ns)
141 if node.localName == 'dome-program':
142 new.add_sub(load_dome_program(node, ns))
144 new.modified = 0
145 return new
147 class Program:
148 "A program contains a code Block and any number of sub-programs."
149 def __init__(self, name):
150 assert '/' not in name
152 self.code = Block(self)
154 self.name = name
155 self.subprograms = {}
156 self.watchers = []
157 self.parent = None
158 self.modified = 0
160 def get_path(self):
161 path = ""
162 p = self
163 while p:
164 path = p.name + '/' + path
165 p = p.parent
166 return path[:-1]
168 def changed(self, op = None):
169 self.modified = 1
170 if self.parent:
171 self.parent.changed(op)
172 else:
173 for w in self.watchers:
174 w.program_changed(op)
176 def tree_changed(self):
177 self.modified = 1
178 if self.parent:
179 self.parent.tree_changed()
180 else:
181 for w in self.watchers:
182 w.prog_tree_changed()
184 def add_sub(self, prog):
185 if prog.parent:
186 raise Exception('%s already has a parent program!' % prog.name)
187 if self.subprograms.has_key(prog.name):
188 raise Exception('%s already has a child called %s!' %
189 (self.name, prog.name))
190 prog.parent = self
191 self.subprograms[prog.name] = prog
192 self.tree_changed()
194 def remove_sub(self, prog):
195 if prog.parent != self:
196 raise Exception('%s is no child of mime!' % prog)
197 prog.parent = None
198 del self.subprograms[prog.name]
199 self.tree_changed()
201 def rename(self, name):
202 p = self.parent
203 if p:
204 if p.subprograms.has_key(name):
205 raise Exception('%s already has a child called %s!' % (p.name, name))
206 p.remove_sub(self)
207 self.name = name
208 if p:
209 p.add_sub(self)
210 else:
211 self.tree_changed()
213 def to_xml(self, doc):
214 node = doc.createElementNS(DOME_NS, 'dome:dome-program')
215 node.setAttributeNS(None, 'name', self.name)
217 node.appendChild(self.code.to_xml(doc))
219 # Keep them in the same order to help with diffs...
220 progs = self.subprograms.keys()
221 progs.sort()
223 for name in progs:
224 p = self.subprograms[name]
225 node.appendChild(p.to_xml(doc))
227 return node
229 def __str__(self):
230 return "Program(%s)" % self.name
232 class Op:
233 "Each node in a chain is an Op. There is no graphical stuff in here."
235 def __init__(self, action = None):
236 "Creates a new node (can be linked into another node later)"
237 if not action:
238 action = ['Start']
239 else:
240 action = list(action)
241 self.parent = None
242 self.action = action
243 self.next = None
244 self.fail = None
245 self.prev = None
246 self.dx, self.dy = (0, 0)
248 def set_parent(self, parent):
249 if self.parent == parent:
250 return
251 if parent and self.parent:
252 raise Exception('Already got a parent!')
253 if self.next:
254 self.next.set_parent(parent)
255 if self.fail:
256 self.fail.set_parent(parent)
257 self.parent = parent
259 def changed(self, op = None):
260 if hasattr(self, 'cached_code'):
261 del self.cached_code
262 print "(remove cached code)"
263 self.parent.changed(op or self)
265 def swap_nf(self):
266 assert self.action[0] != 'Start'
267 self.next, self.fail = (self.fail, self.next)
268 self.changed()
270 def link_to(self, child, exit):
271 # Create a link from this exit to this child Op
272 # Can't link both exits to the same node (bad for tree-walking code in List)
273 assert self.action[0] != 'Start' or exit == 'next'
274 assert child.action[0] != 'Start'
275 assert child is not self
276 assert child.prev is None
278 if (exit == 'next' and self.fail == child) or \
279 (exit == 'fail' and self.next == child):
280 raise Exception("Can't link both exits (of %s) to the same node!" % self)
282 #print "Link %s:%s -> %s" % (self, exit, child)
284 if child.parent and child.parent is not self.parent:
285 raise Exception('%s is from a different parent (%s vs %s)!' %
286 (child, child.parent, self.parent))
287 # If we already have something on this exit, and the new node has a
288 # clear next exit, move the rest of the chain there.
289 child.set_parent(self.parent)
290 current = getattr(self, exit)
291 if current:
292 if child.next:
293 raise Exception('%s already has a next exit' % child)
294 self.unlink(exit, may_delete = 0)
295 child.link_to(current, 'next')
296 child.prev = self
297 setattr(self, exit, child)
298 self.changed()
300 def unlink(self, exit, may_delete = 1):
301 "Remove link from us to child"
302 assert exit in ['next', 'fail']
303 self._unlink(exit, may_delete)
304 self.changed()
306 def _unlink(self, exit, may_delete = 1):
307 child = getattr(self, exit)
308 if not child:
309 raise Exception('%s has no child on exit %s' % (self, exit))
310 if self is not child.prev:
311 raise Exception('Internal error: %s not my child!' % child)
313 child.prev = None
314 setattr(self, exit, None)
316 if may_delete:
317 # There is no way to reach this child now, so unlink its children.
318 child.parent = None
319 if child.next:
320 child._unlink('next')
321 if child.fail:
322 child._unlink('fail')
324 def del_node(self):
325 """Remove this node. It there is exactly one out-going arc and one incoming one,
326 join them together. Error if:"
327 - There are multiple out-going arcs
328 - There is a single out-going arc but multiple parents"""
329 if self.next and self.fail:
330 raise Exception("Can't delete a node with both fail and next exits in use.")
331 if not self.prev:
332 raise Exception("Can't delete a Start node!")
334 prog = self.parent
336 # Find the chain to preserve (can't have both set here)
337 if self.next:
338 exit = 'next'
339 elif self.fail:
340 exit = 'fail'
341 else:
342 exit = None
344 if exit:
345 prev = self.prev
346 preserve = getattr(self, exit)
347 self.unlink(exit, may_delete = 0)
348 if prev.next == self:
349 exit = 'next'
350 else:
351 exit = 'fail'
353 # Remove all links to us
354 if self.prev.next == self:
355 self.prev.unlink('next')
356 else:
357 self.prev.unlink('fail')
359 # Exit is now our parent's exit that leads to us...
360 if exit:
361 # Relink following nodes to our (single) parent
362 prev.link_to(preserve, exit)
364 assert not self.prev
365 assert not self.next
366 assert not self.fail
367 doc = self.to_doc()
368 self.action = None
369 self.parent = None
371 prog.changed()
372 return doc
374 def to_doc(self):
375 from Ft.Xml.cDomlette import implementation
376 doc = implementation.createDocument(DOME_NS, 'dome:dome-program', None)
377 self.to_xml_int(doc.documentElement)
378 return doc
380 def to_xml(self, doc):
381 node = doc.createElementNS(DOME_NS, 'dome:node')
382 node.setAttributeNS(None, 'action', `self.action`)
383 node.setAttributeNS(None, 'dx', str(self.dx))
384 node.setAttributeNS(None, 'dy', str(self.dy))
385 return node
387 def to_xml_int(self, parent):
388 """Adds a chain of <Node> elements to 'parent'. Links only followed when node is
389 first parent."""
390 node = self.to_xml(parent.ownerDocument)
391 parent.appendChild(node)
393 def add_link(op, parent):
394 node = parent.ownerDocument.createElementNS(DOME_NS, 'dome:link')
395 parent.appendChild(node)
396 node.setAttributeNS(None, 'target', str(id(op)))
398 if self.fail:
399 if isinstance(self, Block):
400 fail = parent.ownerDocument.createElementNS(DOME_NS, 'dome:fail')
401 self.fail.to_xml_int(fail)
402 node.appendChild(fail)
403 else:
404 self.fail.to_xml_int(node)
405 if self.next:
406 self.next.to_xml_int(parent)
408 def __str__(self):
409 return "{" + `self.action` + "}"
411 def __repr__(self):
412 return "{" + `self.action` + "}"
414 def get_program(self):
415 p = self.parent
416 while p and not isinstance(p, Program):
417 p = p.parent
418 return p
420 class Block(Op):
421 """A Block is an Op which contains a group of Ops."""
423 def __init__(self, parent):
424 Op.__init__(self, action = ['Block'])
425 self.parent = parent
426 self.start = Op()
427 self.start.parent = self
428 self.foreach = 0
429 self.enter = 0
430 self.restore = 0
431 self.comment = ''
433 def set_start(self, start):
434 assert not start.prev
436 start.set_parent(self)
437 self.start = start
438 self.changed()
440 def is_toplevel(self):
441 return not isinstance(self.parent, Block)
443 def link_to(self, child, exit):
444 assert not self.is_toplevel()
445 Op.link_to(self, child, exit)
447 def to_xml(self, doc):
448 node = doc.createElementNS(DOME_NS, 'dome:block')
449 node.setAttributeNS(None, 'foreach', str(self.foreach))
450 node.setAttributeNS(None, 'enter', str(self.enter))
451 node.setAttributeNS(None, 'restore', str(self.restore))
452 node.setAttributeNS(None, 'comment', str(self.comment))
453 assert not self.start.fail
454 if self.start.next:
455 self.start.next.to_xml_int(node)
456 return node
458 def toggle_restore(self):
459 self.restore = not self.restore
460 self.changed()
462 def toggle_enter(self):
463 self.enter = not self.enter
464 self.changed()
466 def toggle_foreach(self):
467 self.foreach = not self.foreach
468 self.changed()
470 def set_comment(self, comment):
471 self.comment = comment
472 self.changed()