From 2530bbd92c0f9ddf405fe0d59bd913e1a43d6a6e Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Sat, 19 Sep 2026 16:30:42 +0300 Subject: [PATCH] Implement live device add/delete component and app actions Fix add component from palette in live mode: resolve 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 --- app_state.py | 2 +- device_session.py | 32 ++++++++++++++++++++++++++++++-- editor.properties | 2 +- live_hooks.py | 23 ++++++++++++++--------- main.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- sax_file.py | 16 ++++++++++++---- sox.py | 1 + 7 files changed, 102 insertions(+), 19 deletions(-) diff --git a/app_state.py b/app_state.py index 1bf609e..bc08e23 100644 --- a/app_state.py +++ b/app_state.py @@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it. import configparser import os -VERSION = "0.0.0.103" +VERSION = "0.0.0.108" PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties") DEFAULT_GRID_SIZE = 10 DEFAULT_ADD_TIMESTAMP = True diff --git a/device_session.py b/device_session.py index 0fb1c53..375aaa2 100644 --- a/device_session.py +++ b/device_session.py @@ -96,13 +96,17 @@ class DeviceSession: if kit_name == "sys": base_slots = self._extract_component_base_slots(manifest) self._register_kit_types(registry, kit_id, kit_name, manifest) - # Prepend inherited Component base slots (meta, etc.) to every type + # Prepend inherited Component base slots (meta, etc.) to every type, + # then renumber IDs to match the wire slot index (positional in the + # flattened inherited + own list — see sedona Type.java resolveSlots). if base_slots: for key in registry: existing_names = {s["name"] for s in registry[key]} for bs in base_slots: if bs["name"] not in existing_names: - registry[key].insert(0, bs) + registry[key].insert(0, dict(bs)) + for i, slot in enumerate(registry[key]): + slot["id"] = i self.sox_client.slot_registry = registry def _extract_component_base_slots(self, manifest): @@ -238,6 +242,30 @@ class DeviceSession: pass self._current_subs.clear() + # ── Post-add / Post-delete bookkeeping ───────────────────── + + def after_add(self, comp_id): + """Subscribes a newly added component and reads its initial values.""" + comp = self.sox_client.cache_get(comp_id) + if comp is None or not self.is_connected: + return + try: + self.sox_client.update([comp], CONFIG | RUNTIME) + except Exception: + pass + try: + self.sox_client.subscribe([comp], ALL) + self._current_subs.add(comp_id) + except Exception: + pass + + def after_delete(self, comp_id, descendant_ids=None): + """Removes a deleted component (and its descendants) from the subscription set.""" + self._current_subs.discard(comp_id) + if descendant_ids: + for did in descendant_ids: + self._current_subs.discard(did) + # ── DOM Synthesis ─────────────────────────────────────────── def build_dom_tree(self, root_comp): diff --git a/editor.properties b/editor.properties index 410403c..1e923ea 100644 --- a/editor.properties +++ b/editor.properties @@ -7,7 +7,7 @@ left_pane_width = 230 trace_dasp = True [Device] -host = 192.168.0.26 +host = 192.168.1.251 port = 1876 user = admin diff --git a/live_hooks.py b/live_hooks.py index 5efbcd6..ede93d5 100644 --- a/live_hooks.py +++ b/live_hooks.py @@ -66,6 +66,7 @@ def live_add_component(parent_element, kit_name, type_name, comp_name, config_va try: new_comp = ds.sox_client.add(parent_id, kit_id, type_id_val, comp_name, sox_config) + ds.after_add(new_comp.id) return new_comp.id except Exception as e: app_state.set_status(f"Add failed: {e}", warn=True) @@ -84,8 +85,16 @@ def live_delete_component(element): if comp_id is None: return False + descendant_ids = [] + for desc in element.iter(): + if desc.tag == "comp" and desc is not element: + did = _comp_id(desc) + if did is not None: + descendant_ids.append(did) + try: ds.sox_client.delete(comp_id) + ds.after_delete(comp_id, descendant_ids) return True except Exception as e: app_state.set_status(f"Delete failed: {e}", warn=True) @@ -189,6 +198,8 @@ def live_invoke(element, slot_name, arg=None): # ── Helpers ───────────────────────────────────────────────────── def _comp_id(element): + if element.tag == "app": + return 0 cid = element.get("id") if cid is None: return None @@ -222,15 +233,9 @@ def _kit_id(ds, kit_name): def _type_id(ds, kit_id, type_name): if kit_id is None: return None - for (kid, tid), slots in ds.sox_client.slot_registry.items(): - if kid == kit_id: - comp = ds.sox_client.cache_get(0) - if comp and comp.type_name.endswith(f"::{type_name}"): - return tid - # fallback: search all cached components for a matching type_name - for c in ds.sox_client.cache: - if c is not None and c.kit_id == kit_id and c.type_name.endswith(f"::{type_name}"): - return c.type_id + for (kid, tid), full_name in ds.sox_client.type_name_map.items(): + if kid == kit_id and full_name.endswith(f"::{type_name}"): + return tid return None diff --git a/main.py b/main.py index 0a592b3..d792866 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/sax_file.py b/sax_file.py index c51ea2a..b7f1699 100644 --- a/sax_file.py +++ b/sax_file.py @@ -796,13 +796,21 @@ def add_component(kit_name, type_name, grid_x, grid_y): element.set("id", str(comp_id)) element.set("type", f"{kit_name}::{type_name}") - meta_prop = ET.SubElement(element, "prop") - meta_prop.set("name", "meta") - update_meta_property(element, None, grid_x, grid_y) - index = len(list(parent)) parent.append(element) + if live_hooks.is_live(): + ds = app_state.device_session + if ds is not None: + ds.update_element_from_cache(comp_id, app_state.xml_root_element) + + update_meta_property(element, None, grid_x, grid_y) + + if live_hooks.is_live(): + meta_prop = element.find("prop[@name='meta']") + if meta_prop is not None: + live_hooks.live_set_prop_value(element, "meta", meta_prop.get("val", "0")) + if not app_state.live_mode: undo.push(undo.AddComponentCommand( parent, index, element, diff --git a/sox.py b/sox.py index c1bcf56..8662a4a 100644 --- a/sox.py +++ b/sox.py @@ -652,6 +652,7 @@ class SoxClient: comp.name = name comp.parent = parent_id comp.children = [] + comp.type_name = self._resolve_type_name(kit_id, type_id_val) self.cache_add(comp) parent = self.cache_get(parent_id)