Add a Properties view for the app root

The root tree row now names the app component, "app (<host:port>)" or
"app (<file>)", and offers Properties from its context menu and on
double-click. The live DOM's <app> element now carries its type and slot
values, and the refresh-by-id lookup finds it, so the view shows real
sys::App values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:04:03 +03:00
co-authored by Claude Opus 5.5
parent a9593de065
commit 73d168fd87
5 changed files with 50 additions and 25 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.134" VERSION = "0.0.0.135"
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
+26 -16
View File
@@ -300,6 +300,10 @@ 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:
app_elem.set("id", str(root_comp.id)) app_elem.set("id", str(root_comp.id))
# The app is a component like any other, so the Properties view needs its
# type and slot values too. A .sax file leaves both implied; here they are not.
app_elem.set("type", root_comp.type_name)
self._add_prop_elements(app_elem, root_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:
@@ -318,22 +322,7 @@ class DeviceSession:
"type": type_qname, "type": type_qname,
} }
comp_elem = ET.SubElement(parent_elem, "comp", **attribs) comp_elem = ET.SubElement(parent_elem, "comp", **attribs)
self._add_prop_elements(comp_elem, sox_comp)
slots = self.sox_client.slot_registry.get(
(sox_comp.kit_id, sox_comp.type_id))
if slots:
for slot in slots:
if slot.get("is_action"):
continue
name = slot["name"]
if name in sox_comp.values:
val = sox_comp.values[name]
if isinstance(val, float):
val_str = _fmt_float(val, _slot_precision(type_qname, name))
else:
val_str = str(val)
ET.SubElement(comp_elem, "prop",
name=name, val=val_str)
for child_id in sox_comp.children: for child_id in sox_comp.children:
child_comp = self.sox_client.cache_get(child_id) child_comp = self.sox_client.cache_get(child_id)
@@ -346,6 +335,24 @@ class DeviceSession:
return comp_elem return comp_elem
def _add_prop_elements(self, comp_elem, sox_comp):
"""Writes a <prop> for every cached non-action slot value of `sox_comp`."""
slots = self.sox_client.slot_registry.get(
(sox_comp.kit_id, sox_comp.type_id))
if not slots:
return
for slot in slots:
if slot.get("is_action"):
continue
name = slot["name"]
if name in sox_comp.values:
val = sox_comp.values[name]
if isinstance(val, float):
val_str = _fmt_float(val, _slot_precision(sox_comp.type_name, name))
else:
val_str = str(val)
ET.SubElement(comp_elem, "prop", name=name, val=val_str)
def build_links_elements(self, sax_root): def build_links_elements(self, sax_root):
"""Adds <link> elements under a <links> container, matching .sax structure.""" """Adds <link> elements under a <links> container, matching .sax structure."""
links_elem = sax_root.find("links") links_elem = sax_root.find("links")
@@ -449,6 +456,9 @@ class DeviceSession:
@staticmethod @staticmethod
def _find_element_by_id(root, comp_id): def _find_element_by_id(root, comp_id):
target = str(comp_id) target = str(comp_id)
app_elem = root.find("app")
if app_elem is not None and app_elem.get("id") == target:
return app_elem
for elem in root.iter("comp"): for elem in root.iter("comp"):
if elem.get("id") == target: if elem.get("id") == target:
return elem return elem
+14 -3
View File
@@ -293,7 +293,8 @@ def show_properties(comp):
) )
properties_panel.frame.pack(fill=tk.BOTH, expand=True) properties_panel.frame.pack(fill=tk.BOTH, expand=True)
record_visit(VIEW_PROPERTIES, comp) record_visit(VIEW_PROPERTIES, comp)
app_state.set_status(f"Properties: {comp.get('name', '?')}") default_name = "App" if comp.tag == "app" else "?"
app_state.set_status(f"Properties: {comp.get('name', default_name)}")
def dismiss_properties(restore_canvas=True): def dismiss_properties(restore_canvas=True):
@@ -1064,6 +1065,12 @@ def on_tree_right_click(event):
if parent_id == "": if parent_id == "":
tree_root_context_menu.delete(0, tk.END) tree_root_context_menu.delete(0, tk.END)
# The root row names the <app> itself, which has slots of its own to show.
app_element = app_state.tree_element_map.get(item_id)
if app_element is not None:
tree_root_context_menu.add_command(
label="Properties", command=lambda: show_properties(app_element))
tree_root_context_menu.add_separator()
if app_state.live_mode and app_state.device_session and app_state.device_session.is_connected: if app_state.live_mode and app_state.device_session and app_state.device_session.is_connected:
for action_name in _APP_ACTIONS: for action_name in _APP_ACTIONS:
tree_root_context_menu.add_command( tree_root_context_menu.add_command(
@@ -1100,10 +1107,14 @@ def on_tree_double_click(event):
"""Double-click on a tree node opens its special view (or Properties).""" """Double-click on a tree node opens its special view (or Properties)."""
tree = app_state.tree tree = app_state.tree
item_id = tree.identify_row(event.y) item_id = tree.identify_row(event.y)
if not item_id or tree.parent(item_id) == "": if not item_id:
return return
comp = app_state.tree_element_map.get(item_id) comp = app_state.tree_element_map.get(item_id)
if comp is not None: if comp is None:
return
if tree.parent(item_id) == "":
show_properties(comp)
else:
_open_special_view(comp) _open_special_view(comp)
+5 -2
View File
@@ -508,7 +508,9 @@ class PropertiesPanel:
# the view would open already claiming unsaved edits. # the view would open already claiming unsaved edits.
self._loading = False self._loading = False
comp_type = comp.get("type", "sys::Component") # A .sax file's <app> carries no type attribute: it is always a sys::App.
default_type = "sys::App" if comp.tag == "app" else "sys::Component"
comp_type = comp.get("type") or default_type
self.slots = schema.get_full_slots_for_type(comp_type) self.slots = schema.get_full_slots_for_type(comp_type)
self.frame = tk.Frame(parent) self.frame = tk.Frame(parent)
@@ -624,7 +626,8 @@ class PropertiesPanel:
name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold") name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold")
type_font = tkfont.Font(family="Segoe UI", size=9) type_font = tkfont.Font(family="Segoe UI", size=9)
self._tinted(tk.Label(header, text=self.comp.get("name", "?"), font=name_font, default_name = "App" if self.comp.tag == "app" else "?"
self._tinted(tk.Label(header, text=self.comp.get("name", default_name), font=name_font,
bg=HEADER_BG), HEADER_BG).pack(side=tk.LEFT) bg=HEADER_BG), HEADER_BG).pack(side=tk.LEFT)
self._tinted(tk.Label(header, text=f"({comp_type})", font=type_font, self._tinted(tk.Label(header, text=f"({comp_type})", font=type_font,
fg="#555555", bg=HEADER_BG), fg="#555555", bg=HEADER_BG),
+4 -3
View File
@@ -64,12 +64,13 @@ def refresh_treeview_from_dom(xml_root):
return return
if app_state.live_mode: if app_state.live_mode:
file_name = f"{app_state.device_host}:{app_state.device_port}" source = f"{app_state.device_host}:{app_state.device_port}"
else: else:
file_name = os.path.basename(app_state.current_file_path) source = os.path.basename(app_state.current_file_path)
# The row is the <app> component itself; where it came from goes in brackets.
# Inserted plain. refresh_dirty_display puts the unsaved tag on or off, and runs # Inserted plain. refresh_dirty_display puts the unsaved tag on or off, and runs
# after every rebuild, so a fresh row picks up the right state there. # after every rebuild, so a fresh row picks up the right state there.
root_node = tree.insert("", "end", text=file_name, open=True) root_node = tree.insert("", "end", text=f"app ({source})", open=True)
app_element = xml_root.find("app") app_element = xml_root.find("app")
if app_element is not None: if app_element is not None: