Live undo/redo: add/delete component and link, move, property edits

Undo and redo now work against a connected device; each step is sent
there through Sox. Deleting a component first reads its whole subtree
back (config, unloaded children, links, place among siblings) so undo
can recreate it; recreated components take new device ids, which the
DOM elements pick up so later steps still reach them. Binary Buf
config (a user's cred) is read with read_prop, since the cache decodes
every Buf as a Str. Moves, tidy layout, and Properties / page editor /
user edits record only what the device accepted.

A step that does not fully reach the device warns and clears the
history. History is also cleared on every view change in live mode,
as opening a sheet rebuilds the DOM from the device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 20:03:26 +03:00
co-authored by Claude Opus 5.5
parent fd22eb314d
commit b98fe828da
9 changed files with 534 additions and 67 deletions
+22 -13
View File
@@ -531,27 +531,27 @@ def _clear_restoring_flag():
def do_undo(event=None):
if app_state.live_mode:
app_state.set_status("Undo is not available in live mode", warn=True)
return "break"
label = undo.undo()
if label is None:
return "break"
app_state.window.title(f"{app_state.base_window_title()} — undid {label}")
app_state.window.title(f"{_base_title()} — undid {label}")
return "break"
def do_redo(event=None):
if app_state.live_mode:
app_state.set_status("Redo is not available in live mode", warn=True)
return "break"
label = undo.redo()
if label is None:
return "break"
app_state.window.title(f"{app_state.base_window_title()} — redid {label}")
app_state.window.title(f"{_base_title()} — redid {label}")
return "break"
def _base_title():
if app_state.live_mode:
return app_state.base_window_title_live()
return app_state.base_window_title()
def do_save(event=None):
if app_state.live_mode:
do_download_to_sax()
@@ -1771,11 +1771,12 @@ def on_canvas_release(event):
"Move", f"{comp_element.get('name', 'The component')} was not "
"moved on the device")
return
if not app_state.live_mode:
undo.push(undo.MetaMoveCommand(
[(comp_element, meta_before, raw_x, raw_y)],
label=f"move {comp_element.get('name', 'component')}"
))
command_class = (undo.LiveMetaMoveCommand if app_state.live_mode
else undo.MetaMoveCommand)
undo.push(command_class(
[(comp_element, meta_before, raw_x, raw_y)],
label=f"move {comp_element.get('name', 'component')}"
))
app_state.mark_dirty()
app_state.drag_data["tag"] = None
@@ -2029,6 +2030,12 @@ def record_visit(kind, element):
"""
global history_index, _history_file_path
# On a device, undo only reaches back as far as the view that is open. Opening a
# sheet reads the DOM afresh from the device, so steps taken before it name elements
# that are no longer there; Back and Forward are view changes too.
if app_state.live_mode:
undo.reset()
if _suppress_history:
return
@@ -2655,6 +2662,8 @@ def _live_tree_expand(comp_id, tree_item_id):
def _ui():
global _restoring_selection
app_state.xml_root_element = sax_root
# A fresh DOM: undo steps name elements of the one it replaces.
undo.reset()
sax_file.refresh_treeview_from_dom(sax_root)
# Re-open and re-select the node without triggering a wiresheet
# re-render — the sheet is already showing the right content and