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>
257 lines
7.8 KiB
Python
257 lines
7.8 KiB
Python
"""
|
|
Write-through interception layer for live device mode.
|
|
|
|
Checks app_state.live_mode and routes mutations through DeviceSession/SoxClient.
|
|
In offline mode, falls through to the original DOM-only code paths.
|
|
"""
|
|
|
|
import app_state
|
|
from sox import type_id_for_slot
|
|
|
|
|
|
def is_live():
|
|
return (app_state.live_mode
|
|
and app_state.device_session is not None
|
|
and app_state.device_session.is_connected)
|
|
|
|
|
|
# ── Property Write ──────────────────────────────────────────────
|
|
|
|
def live_set_prop_value(element, slot_name, value_str):
|
|
"""In live mode, sends a Sox write to the device. Returns True on success."""
|
|
if not is_live():
|
|
return False
|
|
|
|
ds = app_state.device_session
|
|
comp_id = _comp_id(element)
|
|
if comp_id is None:
|
|
return False
|
|
|
|
slot_info = _find_slot(ds, comp_id, slot_name)
|
|
if slot_info is None:
|
|
return False
|
|
|
|
type_id = type_id_for_slot(slot_info["type_name"])
|
|
coerced = _coerce_value(type_id, value_str)
|
|
|
|
try:
|
|
ds.sox_client.write_prop(comp_id, slot_info["id"], type_id, coerced)
|
|
return True
|
|
except Exception as e:
|
|
app_state.set_status(f"Write failed: {e}", warn=True)
|
|
return False
|
|
|
|
|
|
# ── Add Component ───────────────────────────────────────────────
|
|
|
|
def live_add_component(parent_element, kit_name, type_name, comp_name, config_values=None):
|
|
"""In live mode, sends a Sox add to the device. Returns new comp_id or None."""
|
|
if not is_live():
|
|
return None
|
|
|
|
ds = app_state.device_session
|
|
parent_id = _comp_id(parent_element)
|
|
if parent_id is None:
|
|
return None
|
|
|
|
kit_id = _kit_id(ds, kit_name)
|
|
type_id_val = _type_id(ds, kit_id, type_name)
|
|
if kit_id is None or type_id_val is None:
|
|
return None
|
|
|
|
sox_config = []
|
|
if config_values:
|
|
for tid, val in config_values:
|
|
sox_config.append((tid, val))
|
|
|
|
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)
|
|
return None
|
|
|
|
|
|
# ── Delete Component ────────────────────────────────────────────
|
|
|
|
def live_delete_component(element):
|
|
"""In live mode, sends a Sox delete to the device. Returns True on success."""
|
|
if not is_live():
|
|
return False
|
|
|
|
ds = app_state.device_session
|
|
comp_id = _comp_id(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)
|
|
return False
|
|
|
|
|
|
# ── Rename Component ────────────────────────────────────────────
|
|
|
|
def live_rename_component(element, new_name):
|
|
"""In live mode, sends a Sox rename to the device. Returns True on success."""
|
|
if not is_live():
|
|
return False
|
|
|
|
ds = app_state.device_session
|
|
comp_id = _comp_id(element)
|
|
if comp_id is None:
|
|
return False
|
|
|
|
try:
|
|
ds.sox_client.rename(comp_id, new_name)
|
|
return True
|
|
except Exception as e:
|
|
app_state.set_status(f"Rename failed: {e}", warn=True)
|
|
return False
|
|
|
|
|
|
# ── Link / Unlink ───────────────────────────────────────────────
|
|
|
|
def live_add_link(from_element, from_slot_name, to_element, to_slot_name):
|
|
"""In live mode, sends a Sox link command. Returns True on success."""
|
|
if not is_live():
|
|
return False
|
|
|
|
ds = app_state.device_session
|
|
from_id = _comp_id(from_element)
|
|
to_id = _comp_id(to_element)
|
|
if from_id is None or to_id is None:
|
|
return False
|
|
|
|
from_slot = _find_slot(ds, from_id, from_slot_name)
|
|
to_slot = _find_slot(ds, to_id, to_slot_name)
|
|
if from_slot is None or to_slot is None:
|
|
return False
|
|
|
|
try:
|
|
ds.sox_client.link(from_id, from_slot["id"], to_id, to_slot["id"])
|
|
return True
|
|
except Exception as e:
|
|
app_state.set_status(f"Link failed: {e}", warn=True)
|
|
return False
|
|
|
|
|
|
def live_delete_link(from_element, from_slot_name, to_element, to_slot_name):
|
|
"""In live mode, sends a Sox unlink command. Returns True on success."""
|
|
if not is_live():
|
|
return False
|
|
|
|
ds = app_state.device_session
|
|
from_id = _comp_id(from_element)
|
|
to_id = _comp_id(to_element)
|
|
if from_id is None or to_id is None:
|
|
return False
|
|
|
|
from_slot = _find_slot(ds, from_id, from_slot_name)
|
|
to_slot = _find_slot(ds, to_id, to_slot_name)
|
|
if from_slot is None or to_slot is None:
|
|
return False
|
|
|
|
try:
|
|
ds.sox_client.unlink(from_id, from_slot["id"], to_id, to_slot["id"])
|
|
return True
|
|
except Exception as e:
|
|
app_state.set_status(f"Unlink failed: {e}", warn=True)
|
|
return False
|
|
|
|
|
|
# ── Invoke Action ───────────────────────────────────────────────
|
|
|
|
def live_invoke(element, slot_name, arg=None):
|
|
"""In live mode, sends a Sox invoke. Returns True on success."""
|
|
if not is_live():
|
|
return False
|
|
|
|
ds = app_state.device_session
|
|
comp_id = _comp_id(element)
|
|
if comp_id is None:
|
|
return False
|
|
|
|
slot_info = _find_slot(ds, comp_id, slot_name)
|
|
if slot_info is None:
|
|
return False
|
|
|
|
try:
|
|
ds.sox_client.invoke(comp_id, slot_info["id"])
|
|
return True
|
|
except Exception as e:
|
|
app_state.set_status(f"Invoke failed: {e}", warn=True)
|
|
return False
|
|
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────
|
|
|
|
def _comp_id(element):
|
|
if element.tag == "app":
|
|
return 0
|
|
cid = element.get("id")
|
|
if cid is None:
|
|
return None
|
|
try:
|
|
return int(cid)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _find_slot(ds, comp_id, slot_name):
|
|
comp = ds.sox_client.cache_get(comp_id)
|
|
if comp is None:
|
|
return None
|
|
slots = ds.sox_client.slot_registry.get((comp.kit_id, comp.type_id))
|
|
if slots is None:
|
|
return None
|
|
for s in slots:
|
|
if s["name"] == slot_name:
|
|
return s
|
|
return None
|
|
|
|
|
|
def _kit_id(ds, kit_name):
|
|
if ds.sox_client.schema_parts:
|
|
for i, part in enumerate(ds.sox_client.schema_parts):
|
|
if part.name == kit_name:
|
|
return i
|
|
return None
|
|
|
|
|
|
def _type_id(ds, kit_id, type_name):
|
|
if kit_id is None:
|
|
return None
|
|
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
|
|
|
|
|
|
def _coerce_value(type_id, value_str):
|
|
from sox import BOOL_ID, BYTE_ID, SHORT_ID, INT_ID, LONG_ID, FLOAT_ID, DOUBLE_ID, STR_ID
|
|
if type_id == BOOL_ID:
|
|
return value_str.lower() in ("true", "1", "yes")
|
|
elif type_id in (BYTE_ID, SHORT_ID, INT_ID):
|
|
return int(value_str)
|
|
elif type_id == LONG_ID:
|
|
return int(value_str)
|
|
elif type_id == FLOAT_ID:
|
|
return float(value_str)
|
|
elif type_id == DOUBLE_ID:
|
|
return float(value_str)
|
|
elif type_id == STR_ID:
|
|
return str(value_str)
|
|
return value_str
|