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>
This commit is contained in:
2026-09-20 00:14:52 +03:00
co-authored by Claude Opus 4.6
parent 2530bbd92c
commit abdafef7cb
6 changed files with 122 additions and 17 deletions
+1 -1
View File
@@ -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.108" VERSION = "0.0.0.113"
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
+4 -4
View File
@@ -53,9 +53,10 @@ class DeviceSession:
self._load_slot_registry(parts) self._load_slot_registry(parts)
root_comp = self.sox_client.load(0) root_comp = self.sox_client.load(0)
# Pre-load immediate children so the tree shows the first level
if root_comp and root_comp.children: if root_comp and root_comp.children:
self.load_children(root_comp.id) children = self.load_children(root_comp.id)
if children:
self.sox_client.update(children, CONFIG | RUNTIME)
return root_comp return root_comp
def disconnect(self): def disconnect(self):
@@ -283,8 +284,7 @@ class DeviceSession:
app_elem = ET.SubElement(sax_root, "app") app_elem = ET.SubElement(sax_root, "app")
if root_comp is not None: if root_comp is not None:
# Build children of root directly under <app>, matching .sax format app_elem.set("id", str(root_comp.id))
# where <app> IS the root container (not a <comp>)
for child_id in root_comp.children: for child_id in root_comp.children:
child_comp = self.sox_client.cache_get(child_id) child_comp = self.sox_client.cache_get(child_id)
if child_comp is not None: if child_comp is not None:
+6
View File
@@ -213,6 +213,12 @@ def _find_slot(ds, comp_id, slot_name):
comp = ds.sox_client.cache_get(comp_id) comp = ds.sox_client.cache_get(comp_id)
if comp is None: if comp is None:
return 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)) slots = ds.sox_client.slot_registry.get((comp.kit_id, comp.type_id))
if slots is None: if slots is None:
return None return None
+16 -1
View File
@@ -11,6 +11,7 @@ import app_state
import connect_dialog import connect_dialog
import device_session import device_session
import link_view import link_view
import live_hooks
import manage_kits import manage_kits
import palette import palette
import properties import properties
@@ -191,7 +192,15 @@ def refresh_all_views():
def _component_in_document(comp): def _component_in_document(comp):
root = app_state.xml_root_element root = app_state.xml_root_element
return root is not None and any(element is comp for element in root.iter()) if root is None:
return False
if any(element is comp for element in root.iter()):
return True
if app_state.live_mode:
comp_id = comp.get("id")
if comp_id is not None:
return any(e.get("id") == comp_id for e in root.iter())
return False
def delete_component(comp): def delete_component(comp):
@@ -1329,6 +1338,12 @@ def on_canvas_release(event):
if orig_x != raw_x or orig_y != raw_y: if orig_x != raw_x or orig_y != raw_y:
meta_before = app_state.drag_data.get("meta_before", meta_str) meta_before = app_state.drag_data.get("meta_before", meta_str)
update_meta_property(comp_element, meta_str, raw_x, raw_y) update_meta_property(comp_element, meta_str, raw_x, raw_y)
if live_hooks.is_live():
new_meta = comp_element.find("prop[@name='meta']")
if new_meta is not None:
live_hooks.live_set_prop_value(
comp_element, "meta", new_meta.get("val", "0"))
if not app_state.live_mode:
undo.push(undo.MetaMoveCommand( undo.push(undo.MetaMoveCommand(
[(comp_element, meta_before, raw_x, raw_y)], [(comp_element, meta_before, raw_x, raw_y)],
label=f"move {comp_element.get('name', 'component')}" label=f"move {comp_element.get('name', 'component')}"
+42 -2
View File
@@ -518,6 +518,8 @@ class PropertiesPanel:
# sibling nothing at all. # sibling nothing at all.
self._build_header(comp_type) self._build_header(comp_type)
self._build_footer() self._build_footer()
if app_state.live_mode and app_state.device_session is not None:
self._refresh_from_device()
self._loading = True self._loading = True
self._build_rows_area() self._build_rows_area()
self._loading = False self._loading = False
@@ -1056,7 +1058,13 @@ class PropertiesPanel:
# -- the two buttons -- # -- the two buttons --
def refresh(self): def refresh(self):
"""Re-reads every value from the DOM, discarding edits that were not saved.""" """Re-reads every value from the DOM, discarding edits that were not saved.
In live mode, fetches fresh values from the device first.
"""
if app_state.live_mode and app_state.device_session is not None:
self._refresh_from_device()
self._loading = True self._loading = True
for row in self.rows: for row in self.rows:
self._load_row(row) self._load_row(row)
@@ -1066,8 +1074,31 @@ class PropertiesPanel:
self._load_meta_row() self._load_meta_row()
self._load_path() self._load_path()
self._update_tint() self._update_tint()
if app_state.live_mode:
self.status_var.set("Refreshed from device.")
else:
self.status_var.set("Refreshed from the document.") self.status_var.set("Refreshed from the document.")
def _refresh_from_device(self):
"""Reads fresh config+runtime values from the device into the DOM element."""
from sox import CONFIG, RUNTIME
ds = app_state.device_session
comp_id = self.comp.get("id")
if comp_id is None:
return
try:
comp_id = int(comp_id)
except ValueError:
return
sox_comp = ds.sox_client.cache_get(comp_id)
if sox_comp is None:
return
try:
ds.sox_client.update([sox_comp], CONFIG | RUNTIME)
except Exception:
pass
ds.update_element_from_cache(comp_id, app_state.xml_root_element)
def save(self): def save(self):
"""Writes the edited rows back, as one undoable step. """Writes the edited rows back, as one undoable step.
@@ -1174,7 +1205,16 @@ class PropertiesPanel:
root = app_state.xml_root_element root = app_state.xml_root_element
if root is None: if root is None:
return False return False
return any(element is self.comp for element in root.iter()) if any(element is self.comp for element in root.iter()):
return True
if app_state.live_mode:
comp_id = self.comp.get("id")
if comp_id is not None:
for element in root.iter():
if element.get("id") == comp_id:
self.comp = element
return True
return False
def _sync_bar(self, bar, first, last): def _sync_bar(self, bar, first, last):
"""Moves a scrollbar's thumb, and hides the bar entirely when it spans the whole.""" """Moves a scrollbar's thumb, and hides the bar entirely when it spans the whole."""
+45 -1
View File
@@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET
import app_state import app_state
import live_hooks import live_hooks
import palette import palette
import properties
import schema import schema
import undo import undo
import wiresheet import wiresheet
@@ -69,6 +70,7 @@ def refresh_treeview_from_dom(xml_root):
app_element = xml_root.find("app") app_element = xml_root.find("app")
if app_element is not None: if app_element is not None:
app_state.tree_element_map[root_node] = app_element
parse_xml_to_tree(tree, root_node, app_element) parse_xml_to_tree(tree, root_node, app_element)
else: else:
parse_xml_to_tree(tree, root_node, xml_root) parse_xml_to_tree(tree, root_node, xml_root)
@@ -754,6 +756,28 @@ def reconcile_schema_kits_before_save():
return True return True
def _build_config_values(full_type):
"""Builds SOX config_values list from manifest defaults for an add command."""
from sox import type_id_for_slot
from live_hooks import _coerce_value
slots = app_state.final_components_registry.get(full_type, [])
config_values = []
for slot in slots:
if "c" not in slot["flags"]:
continue
if slot["type"] == "void":
continue
val = slot["default"]
if val is None:
val = properties.type_zero(slot["type"])
if val is None:
continue
tid = type_id_for_slot(slot["type"])
config_values.append((tid, _coerce_value(tid, val)))
return config_values
def add_component(kit_name, type_name, grid_x, grid_y): def add_component(kit_name, type_name, grid_x, grid_y):
"""Places a new component of kit::type at a grid position. Returns the element.""" """Places a new component of kit::type at a grid position. Returns the element."""
parent = current_sheet_container() parent = current_sheet_container()
@@ -784,7 +808,10 @@ def add_component(kit_name, type_name, grid_x, grid_y):
comp_name = unique_child_name(parent, type_name) comp_name = unique_child_name(parent, type_name)
if live_hooks.is_live(): if live_hooks.is_live():
new_id = live_hooks.live_add_component(parent, kit_name, type_name, comp_name) full_type = f"{kit_name}::{type_name}"
config_values = _build_config_values(full_type)
new_id = live_hooks.live_add_component(
parent, kit_name, type_name, comp_name, config_values or None)
if new_id is None: if new_id is None:
return None return None
comp_id = new_id comp_id = new_id
@@ -796,6 +823,23 @@ 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}")
full_type = f"{kit_name}::{type_name}"
slots = app_state.final_components_registry.get(full_type, [])
for slot in slots:
flags = slot["flags"]
if "a" in flags or "r" in flags:
continue
if slot["type"] == "void":
continue
val = slot["default"]
if val is None:
val = properties.type_zero(slot["type"])
if val is None:
continue
prop = ET.SubElement(element, "prop")
prop.set("name", slot["name"])
prop.set("val", val)
index = len(list(parent)) index = len(list(parent))
parent.append(element) parent.append(element)