App tree: keep expanded folders open; only the - sign closes one

Rebuilding the tree (live child loads, edits) now restores every row that
was expanded, matched by component id or name path. Double-click no longer
toggles a row through ttk's default binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 19:25:44 +03:00
co-authored by Claude Opus 5.5
parent 05ca7e9035
commit 96a703c5d5
3 changed files with 56 additions and 5 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.147" VERSION = "0.0.0.148"
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
+8 -3
View File
@@ -1232,18 +1232,23 @@ def _open_special_view(comp):
def on_tree_double_click(event): 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).
Returns "break" so ttk's own double-click does not also toggle the row: only the
+/- sign opens or closes a node.
"""
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: if not item_id:
return return "break"
comp = app_state.tree_element_map.get(item_id) comp = app_state.tree_element_map.get(item_id)
if comp is None: if comp is None:
return return "break"
if tree.parent(item_id) == "": if tree.parent(item_id) == "":
show_properties(comp) show_properties(comp)
else: else:
_open_special_view(comp) _open_special_view(comp)
return "break"
def on_canvas_double_click(event): def on_canvas_double_click(event):
+47 -1
View File
@@ -59,9 +59,53 @@ def parse_xml_to_tree(treeview, parent_node, xml_element):
parse_xml_to_tree(treeview, node_id, child) parse_xml_to_tree(treeview, node_id, child)
def _expansion_key(treeview, item, parent_key):
"""Names a tree row in a way that survives a rebuild of the tree.
A component's id is stable across edits and across a fresh DOM read from the
device; a row without one (the root, or an id-less file component) falls back to
its name under its parent's key.
"""
elem = app_state.tree_element_map.get(item)
comp_id = elem.get("id") if elem is not None else None
if comp_id is not None and treeview.parent(item) != "":
return ("id", comp_id)
name = elem.get("name", "") if elem is not None else treeview.item(item, "text")
return ("path", parent_key, name)
def _collect_open_keys(treeview, parent="", parent_key=None, found=None):
"""Returns the keys of every row that is expanded right now."""
if found is None:
found = set()
for item in treeview.get_children(parent):
key = _expansion_key(treeview, item, parent_key)
if treeview.item(item, "open"):
found.add(key)
_collect_open_keys(treeview, item, key, found)
return found
def _reopen_keys(treeview, open_keys, parent="", parent_key=None):
"""Expands again the rows that were expanded before the rebuild.
Setting the open flag in code does not raise <<TreeviewOpen>>, so this never
starts a live child load of its own.
"""
for item in treeview.get_children(parent):
key = _expansion_key(treeview, item, parent_key)
if key in open_keys:
treeview.item(item, open=True)
_reopen_keys(treeview, open_keys, item, key)
def refresh_treeview_from_dom(xml_root): def refresh_treeview_from_dom(xml_root):
"""Rebuilds navigation tree structures using current XML DOM trees.""" """Rebuilds navigation tree structures using current XML DOM trees.
Rows the user had expanded stay expanded; only the user's own "-" closes one.
"""
tree = app_state.tree tree = app_state.tree
open_keys = _collect_open_keys(tree)
app_state.tree_element_map.clear() app_state.tree_element_map.clear()
for item in tree.get_children(): for item in tree.get_children():
@@ -88,6 +132,8 @@ def refresh_treeview_from_dom(xml_root):
else: else:
parse_xml_to_tree(tree, root_node, xml_root) parse_xml_to_tree(tree, root_node, xml_root)
_reopen_keys(tree, open_keys)
if _tree_filled is not None: if _tree_filled is not None:
_tree_filled() _tree_filled()