Implement live device add/delete component and app actions

Fix add component from palette in live mode: resolve <app> element
as comp_id 0, fix type_id lookup to use type_name_map, set type_name
on newly added SoxComponent, sync device values into DOM after add,
write meta position back to device, and subscribe new component to COV.

Fix delete component in live mode: clean up subscriptions for deleted
component and all its descendants.

Add tree root context menu with device app actions (save, hibernate,
quit, restart, reboot) in live mode. Device save clears dirty state.

Clean up connection lost handling to do a full disconnect.
Fix slot registry to copy base slots and renumber IDs correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 16:30:42 +03:00
co-authored by Claude Opus 4.6
parent 9a483a0b0e
commit 2530bbd92c
7 changed files with 102 additions and 19 deletions
+43 -2
View File
@@ -445,6 +445,39 @@ def do_download_to_sax():
messagebox.showerror("Download Failed", str(e))
def _invoke_app_action(slot_name):
"""Invokes an action slot on the app component (comp_id 0) of the connected device."""
ds = app_state.device_session
if ds is None or not ds.is_connected:
app_state.set_status("Not connected to a device", warn=True)
return
comp = ds.sox_client.cache_get(0)
if comp is None:
app_state.set_status("App component not loaded", warn=True)
return
slots = ds.sox_client.slot_registry.get((comp.kit_id, comp.type_id))
if slots is None:
app_state.set_status("No slot registry for the app component", warn=True)
return
slot = next((s for s in slots if s["name"] == slot_name and s.get("is_action")), None)
if slot is None:
app_state.set_status(f"Action slot '{slot_name}' not found on app", warn=True)
return
try:
ds.sox_client.invoke(0, slot["id"])
app_state.set_status(f"Invoked '{slot_name}' on device")
except Exception as e:
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
return
if slot_name == "save":
app_state.mark_clean()
elif slot_name in ("hibernate", "quit", "restart", "reboot"):
do_device_disconnect()
_APP_ACTIONS = ["save", "hibernate", "quit", "restart", "reboot"]
NL = chr(10) # newline, for the multi-line message boxes
@@ -876,6 +909,15 @@ def on_tree_right_click(event):
parent_id = tree.parent(item_id)
if parent_id == "":
tree_root_context_menu.delete(0, tk.END)
if app_state.live_mode and app_state.device_session and app_state.device_session.is_connected:
for action_name in _APP_ACTIONS:
tree_root_context_menu.add_command(
label=action_name.capitalize(),
command=lambda a=action_name: _invoke_app_action(a))
else:
tree_root_context_menu.add_command(
label="Save", command=sax_file.execute_direct_overwrite_save)
tree_root_context_menu.post(event.x_root, event.y_root)
return
@@ -2086,7 +2128,7 @@ def _start_cov_polling():
if ds is None or not ds.is_connected:
_cov_poll_id = None
if app_state.live_mode:
app_state.connection_status = "error"
do_device_disconnect()
app_state.set_status("Connection lost", warn=True)
return
@@ -2192,7 +2234,6 @@ def _rebuild_link_context_menu():
tree_comp_context_menu = tk.Menu(window, tearoff=0)
tree_root_context_menu = tk.Menu(window, tearoff=0)
tree_root_context_menu.add_command(label="Save", command=sax_file.execute_direct_overwrite_save)
# --- Keyboard shortcuts ---
# Command and Control are both bound on every platform, so muscle memory from either