"""Undo/redo as a stack of reversible commands. Commands own the DOM mutation in both directions. Nothing here touches Tk: the view is refreshed through a callback that main registers at startup, which also keeps the import graph free of cycles (sax_file and wiresheet both push commands here). """ import app_state from sax_dom import update_meta_property _undo_stack = [] _redo_stack = [] _saved_depth = 0 # len(_undo_stack) as of the last save _refresh_view = None def set_refresh_callback(callback): """Registers how the view should be rebuilt after an undo or redo.""" global _refresh_view _refresh_view = callback def _refresh(): if _refresh_view is not None: _refresh_view() # Undoing back to the last saved state should clear the dirty marker, not keep it lit. if len(_undo_stack) == _saved_depth: app_state.mark_clean() else: app_state.mark_dirty() def push(command): """Records an already-applied command. Any redo history becomes unreachable.""" _undo_stack.append(command) _redo_stack.clear() def can_undo(): return bool(_undo_stack) def can_redo(): return bool(_redo_stack) def undo(): if not _undo_stack: return None command = _undo_stack.pop() command.undo() _redo_stack.append(command) _refresh() return command.label def redo(): if not _redo_stack: return None command = _redo_stack.pop() command.redo() _undo_stack.append(command) _refresh() return command.label def mark_saved(): """Pins the current stack depth as the on-disk state.""" global _saved_depth _saved_depth = len(_undo_stack) def reset(): """Drops all history, for when a different file is loaded.""" global _saved_depth _undo_stack.clear() _redo_stack.clear() _saved_depth = 0 class MetaMoveCommand: """One or many components changing position, i.e. their packed meta x/y.""" def __init__(self, changes, label="move"): # changes: list of (element, old_meta_val, new_x, new_y) self.changes = changes self.label = label def undo(self): for element, old_val, _, _ in self.changes: meta_prop = element.find("prop[@name='meta']") if meta_prop is not None and old_val is not None: meta_prop.set("val", old_val) def redo(self): for element, old_val, new_x, new_y in self.changes: update_meta_property(element, old_val, new_x, new_y) class AddComponentCommand: """Placing a component, plus the entry that placing it may have required.""" def __init__(self, parent, index, element, schema_parent=None, kit_element=None, label="add"): self.parent = parent self.index = index self.element = element self.schema_parent = schema_parent self.kit_element = kit_element self.label = label def undo(self): if self.element in list(self.parent): self.parent.remove(self.element) if self.kit_element is not None and self.schema_parent is not None: if self.kit_element in list(self.schema_parent): self.schema_parent.remove(self.kit_element) def redo(self): if self.kit_element is not None and self.schema_parent is not None: if self.kit_element not in list(self.schema_parent): self.schema_parent.append(self.kit_element) if self.element not in list(self.parent): self.parent.insert(self.index, self.element) class DeleteComponentCommand: """A cascading delete: the component subtree plus every link that referenced it.""" def __init__(self, parent, index, element, link_records, label="delete"): # link_records: list of (links_parent, index, link_element) self.parent = parent self.index = index self.element = element self.link_records = link_records self.label = label def undo(self): self.parent.insert(self.index, self.element) # Ascending order so each link lands back on its original index. for links_parent, index, link in sorted(self.link_records, key=lambda r: r[1]): links_parent.insert(index, link) def redo(self): for links_parent, _, link in self.link_records: if link in list(links_parent): links_parent.remove(link) if self.element in list(self.parent): self.parent.remove(self.element)