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)