From 73d168fd870b964dab575068b2b7f84c47221b70 Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Tue, 29 Sep 2026 21:04:03 +0300 Subject: [PATCH] Add a Properties view for the app root The root tree row now names the app component, "app ()" or "app ()", and offers Properties from its context menu and on double-click. The live DOM's 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 --- app_state.py | 2 +- device_session.py | 42 ++++++++++++++++++++++++++---------------- main.py | 17 ++++++++++++++--- properties.py | 7 +++++-- sax_file.py | 7 ++++--- 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/app_state.py b/app_state.py index ebc1325..356ed14 100644 --- a/app_state.py +++ b/app_state.py @@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it. import configparser 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") DEFAULT_GRID_SIZE = 10 DEFAULT_ADD_TIMESTAMP = True diff --git a/device_session.py b/device_session.py index 2f24fbe..adcd48b 100644 --- a/device_session.py +++ b/device_session.py @@ -300,6 +300,10 @@ class DeviceSession: app_elem = ET.SubElement(sax_root, "app") if root_comp is not None: 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: child_comp = self.sox_client.cache_get(child_id) if child_comp is not None: @@ -318,22 +322,7 @@ class DeviceSession: "type": type_qname, } comp_elem = ET.SubElement(parent_elem, "comp", **attribs) - - 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) + self._add_prop_elements(comp_elem, sox_comp) for child_id in sox_comp.children: child_comp = self.sox_client.cache_get(child_id) @@ -346,6 +335,24 @@ class DeviceSession: return comp_elem + def _add_prop_elements(self, comp_elem, sox_comp): + """Writes a 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): """Adds elements under a container, matching .sax structure.""" links_elem = sax_root.find("links") @@ -449,6 +456,9 @@ class DeviceSession: @staticmethod def _find_element_by_id(root, 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"): if elem.get("id") == target: return elem diff --git a/main.py b/main.py index 6692f0b..ff7e07b 100644 --- a/main.py +++ b/main.py @@ -293,7 +293,8 @@ def show_properties(comp): ) properties_panel.frame.pack(fill=tk.BOTH, expand=True) 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): @@ -1064,6 +1065,12 @@ def on_tree_right_click(event): if parent_id == "": tree_root_context_menu.delete(0, tk.END) + # The root row names the 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: for action_name in _APP_ACTIONS: 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).""" tree = app_state.tree item_id = tree.identify_row(event.y) - if not item_id or tree.parent(item_id) == "": + if not item_id: return 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) diff --git a/properties.py b/properties.py index 9e672fd..81ea09d 100644 --- a/properties.py +++ b/properties.py @@ -508,7 +508,9 @@ class PropertiesPanel: # the view would open already claiming unsaved edits. self._loading = False - comp_type = comp.get("type", "sys::Component") + # A .sax file's 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.frame = tk.Frame(parent) @@ -624,7 +626,8 @@ class PropertiesPanel: name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold") 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) self._tinted(tk.Label(header, text=f"({comp_type})", font=type_font, fg="#555555", bg=HEADER_BG), diff --git a/sax_file.py b/sax_file.py index cf18e38..04766b7 100644 --- a/sax_file.py +++ b/sax_file.py @@ -64,12 +64,13 @@ def refresh_treeview_from_dom(xml_root): return 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: - file_name = os.path.basename(app_state.current_file_path) + source = os.path.basename(app_state.current_file_path) + # The row is the component itself; where it came from goes in brackets. # 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. - 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") if app_element is not None: