Files
sedona_editor/live_hooks.py
T
ArdaandClaude Opus 4.6 abdafef7cb Use manifest defaults when adding components and refresh properties from device
- Populate prop elements with manifest default values when adding
  components in both offline and live mode
- Send config values from manifest in SOX add command for live devices
- Refresh button in properties panel now fetches fresh values from
  the connected device before re-reading the DOM
- Properties panel fetches device values on open in live mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 00:14:52 +03:00

263 lines
8.2 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
type_name = ds.sox_client.type_name_map.get((comp.kit_id, comp.type_id))
if type_name and type_name in app_state.final_components_registry:
for s in app_state.final_components_registry[type_name]:
if s["name"] == slot_name:
return {"name": s["name"], "id": s["slot_id"],
"type_name": s.get("type", "int")}
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