Remove redo; undo rename on a device

Redo is gone in both file and device mode: the Edit menu entry, Ctrl/Cmd+Y
and Ctrl/Cmd+Shift+Z, and the redo stack. An undone step is dropped.
Undoing past the last save now leaves the saved depth unreachable, so a
new edit cannot be mistaken for the saved state.

Renames on a device are undoable: LiveRenameCommand renames back on the
device first, and warns and clears history if the device refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 20:51:02 +03:00
co-authored by Claude Opus 5.5
parent 8b21c5f389
commit b63655462f
5 changed files with 50 additions and 56 deletions
+30 -31
View File
@@ -1,7 +1,8 @@
"""Undo/redo as a stack of reversible commands.
"""Undo as a stack of reversible commands. There is no redo: an undone step is gone.
Commands own the DOM mutation in both directions; in live mode they also carry it to the
device, and raise LiveUndoError when it does not get there. Nothing here touches Tk: the view is
Commands own the DOM mutation in both directions -- redo() is how a command is first
applied, undo() takes it back. In live mode they also carry it to the device, and raise
LiveUndoError when it does not get there. 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).
"""
@@ -13,13 +14,12 @@ import schema
from sax_dom import remove_prop, restore_children, set_prop_value, 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."""
"""Registers how the view should be rebuilt after an undo."""
global _refresh_view
_refresh_view = callback
@@ -36,21 +36,20 @@ def _refresh():
def push(command):
"""Records an already-applied command. Any redo history becomes unreachable."""
"""Records an already-applied command."""
global _saved_depth
# Undone past the save, the saved state can no longer be reached by undoing.
if len(_undo_stack) < _saved_depth:
_saved_depth = -1
_undo_stack.append(command)
_redo_stack.clear()
def can_undo():
return bool(_undo_stack)
def can_redo():
return bool(_redo_stack)
class LiveUndoError(Exception):
"""A live command's undo or redo did not fully reach the device.
"""A live command's undo did not fully reach the device.
Raised with what did not happen; live_hooks.last_error holds why.
"""
@@ -65,21 +64,6 @@ def undo():
except LiveUndoError as failure:
_abandon_history("Undo", failure)
return None
_redo_stack.append(command)
_refresh()
return command.label
def redo():
if not _redo_stack:
return None
command = _redo_stack.pop()
try:
command.redo()
except LiveUndoError as failure:
_abandon_history("Redo", failure)
return None
_undo_stack.append(command)
_refresh()
return command.label
@@ -87,8 +71,8 @@ def redo():
def _abandon_history(title, failure):
"""After a step that did not reach the device, the history no longer matches it.
The device may hold part of the step, so neither direction can be trusted to land
where it says. History is dropped rather than replayed against a state it never saw.
The device may hold part of the step, so the steps below it cannot be trusted to land
where they say. History is dropped rather than replayed against a state it never saw.
"""
reset()
_refresh()
@@ -105,14 +89,13 @@ def reset():
"""Drops all history, for when a different file is loaded."""
global _saved_depth
_undo_stack.clear()
_redo_stack.clear()
_saved_depth = 0
def reload_schema_registry():
"""Recompiles the slot registry after the <schema> block changed.
A failure is swallowed: undo and redo only ever land on a state that resolved once
A failure is swallowed: undo only ever lands on a state that resolved once
already, so a missing manifest here means Sedona home moved underneath the session,
which the next action reports in its own words.
"""
@@ -360,3 +343,19 @@ class RenameComponentCommand:
self.element.set("name", self.new_name)
for link, attr, _, new_value in self.link_changes:
link.set(attr, new_value)
class LiveRenameCommand(RenameComponentCommand):
"""A rename on the device: the old name goes back there before the DOM takes it."""
def undo(self):
if not live_hooks.live_rename_component(self.element, self.old_name):
raise LiveUndoError(f"{self.new_name} was not renamed back to {self.old_name} "
"on the device")
super().undo()
def redo(self):
if not live_hooks.live_rename_component(self.element, self.new_name):
raise LiveUndoError(f"{self.old_name} was not renamed to {self.new_name} "
"on the device")
super().redo()