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:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
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")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
+30
-2
@@ -96,13 +96,17 @@ class DeviceSession:
|
|||||||
if kit_name == "sys":
|
if kit_name == "sys":
|
||||||
base_slots = self._extract_component_base_slots(manifest)
|
base_slots = self._extract_component_base_slots(manifest)
|
||||||
self._register_kit_types(registry, kit_id, kit_name, 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:
|
if base_slots:
|
||||||
for key in registry:
|
for key in registry:
|
||||||
existing_names = {s["name"] for s in registry[key]}
|
existing_names = {s["name"] for s in registry[key]}
|
||||||
for bs in base_slots:
|
for bs in base_slots:
|
||||||
if bs["name"] not in existing_names:
|
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
|
self.sox_client.slot_registry = registry
|
||||||
|
|
||||||
def _extract_component_base_slots(self, manifest):
|
def _extract_component_base_slots(self, manifest):
|
||||||
@@ -238,6 +242,30 @@ class DeviceSession:
|
|||||||
pass
|
pass
|
||||||
self._current_subs.clear()
|
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 ───────────────────────────────────────────
|
# ── DOM Synthesis ───────────────────────────────────────────
|
||||||
|
|
||||||
def build_dom_tree(self, root_comp):
|
def build_dom_tree(self, root_comp):
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ left_pane_width = 230
|
|||||||
trace_dasp = True
|
trace_dasp = True
|
||||||
|
|
||||||
[Device]
|
[Device]
|
||||||
host = 192.168.0.26
|
host = 192.168.1.251
|
||||||
port = 1876
|
port = 1876
|
||||||
user = admin
|
user = admin
|
||||||
|
|
||||||
|
|||||||
+13
-8
@@ -66,6 +66,7 @@ def live_add_component(parent_element, kit_name, type_name, comp_name, config_va
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
new_comp = ds.sox_client.add(parent_id, kit_id, type_id_val, comp_name, sox_config)
|
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
|
return new_comp.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_state.set_status(f"Add failed: {e}", warn=True)
|
app_state.set_status(f"Add failed: {e}", warn=True)
|
||||||
@@ -84,8 +85,16 @@ def live_delete_component(element):
|
|||||||
if comp_id is None:
|
if comp_id is None:
|
||||||
return False
|
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:
|
try:
|
||||||
ds.sox_client.delete(comp_id)
|
ds.sox_client.delete(comp_id)
|
||||||
|
ds.after_delete(comp_id, descendant_ids)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_state.set_status(f"Delete failed: {e}", warn=True)
|
app_state.set_status(f"Delete failed: {e}", warn=True)
|
||||||
@@ -189,6 +198,8 @@ def live_invoke(element, slot_name, arg=None):
|
|||||||
# ── Helpers ─────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _comp_id(element):
|
def _comp_id(element):
|
||||||
|
if element.tag == "app":
|
||||||
|
return 0
|
||||||
cid = element.get("id")
|
cid = element.get("id")
|
||||||
if cid is None:
|
if cid is None:
|
||||||
return None
|
return None
|
||||||
@@ -222,15 +233,9 @@ def _kit_id(ds, kit_name):
|
|||||||
def _type_id(ds, kit_id, type_name):
|
def _type_id(ds, kit_id, type_name):
|
||||||
if kit_id is None:
|
if kit_id is None:
|
||||||
return None
|
return None
|
||||||
for (kid, tid), slots in ds.sox_client.slot_registry.items():
|
for (kid, tid), full_name in ds.sox_client.type_name_map.items():
|
||||||
if kid == kit_id:
|
if kid == kit_id and full_name.endswith(f"::{type_name}"):
|
||||||
comp = ds.sox_client.cache_get(0)
|
|
||||||
if comp and comp.type_name.endswith(f"::{type_name}"):
|
|
||||||
return tid
|
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
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -445,6 +445,39 @@ def do_download_to_sax():
|
|||||||
messagebox.showerror("Download Failed", str(e))
|
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
|
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)
|
parent_id = tree.parent(item_id)
|
||||||
|
|
||||||
if parent_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)
|
tree_root_context_menu.post(event.x_root, event.y_root)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2086,7 +2128,7 @@ def _start_cov_polling():
|
|||||||
if ds is None or not ds.is_connected:
|
if ds is None or not ds.is_connected:
|
||||||
_cov_poll_id = None
|
_cov_poll_id = None
|
||||||
if app_state.live_mode:
|
if app_state.live_mode:
|
||||||
app_state.connection_status = "error"
|
do_device_disconnect()
|
||||||
app_state.set_status("Connection lost", warn=True)
|
app_state.set_status("Connection lost", warn=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2192,7 +2234,6 @@ def _rebuild_link_context_menu():
|
|||||||
tree_comp_context_menu = tk.Menu(window, tearoff=0)
|
tree_comp_context_menu = tk.Menu(window, tearoff=0)
|
||||||
|
|
||||||
tree_root_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 ---
|
# --- Keyboard shortcuts ---
|
||||||
# Command and Control are both bound on every platform, so muscle memory from either
|
# Command and Control are both bound on every platform, so muscle memory from either
|
||||||
|
|||||||
+12
-4
@@ -796,13 +796,21 @@ def add_component(kit_name, type_name, grid_x, grid_y):
|
|||||||
element.set("id", str(comp_id))
|
element.set("id", str(comp_id))
|
||||||
element.set("type", f"{kit_name}::{type_name}")
|
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))
|
index = len(list(parent))
|
||||||
parent.append(element)
|
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:
|
if not app_state.live_mode:
|
||||||
undo.push(undo.AddComponentCommand(
|
undo.push(undo.AddComponentCommand(
|
||||||
parent, index, element,
|
parent, index, element,
|
||||||
|
|||||||
@@ -652,6 +652,7 @@ class SoxClient:
|
|||||||
comp.name = name
|
comp.name = name
|
||||||
comp.parent = parent_id
|
comp.parent = parent_id
|
||||||
comp.children = []
|
comp.children = []
|
||||||
|
comp.type_name = self._resolve_type_name(kit_id, type_id_val)
|
||||||
self.cache_add(comp)
|
self.cache_add(comp)
|
||||||
|
|
||||||
parent = self.cache_get(parent_id)
|
parent = self.cache_get(parent_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user