Properties view: every slot of a component, edited in the editor pane

Right-click a component on the wiresheet or in the tree and Properties takes
over the editor pane, listing every slot the manifest declares rather than the
handful a box paints. Config slots that are not @readonly are white and
editable; everything else is grey.

Rows are built from the slot's facets, read against SedonaDoc/niagara.html:
@trueText/@falseText, @range, @precision, @unit, and @min/@max, which bound
what Save accepts. Three facets it had wrong:

  - null is a value, not an absence. control::WriteFloat pairs 'in' with a
    setNull action and the sample app stores val="null" on four of them, so
    null now shows, parses back, and reads as @nullText where declared.
  - @min/@max on a Buf count characters. TcpDevice.ipAddr is a Buf(17) with
    @max 16; a longer string is refused rather than overrunning the device.
  - an int's implicit range is noise. [-2147483648 - 2147483647] said no more
    than "a whole number", so only byte and short print a range they did not
    declare.

meta gets a row of its own: the '>>' opens its user groups as checkboxes and
its position as x and y, since typing 822411269 into a field sets neither.

Save writes only the rows that moved, as one undoable SlotValueCommand, and
refuses the lot if any row fails its range check. An edited field paints the
view in the canvas's own dirty tint, and every way of leaving asks before
discarding.

The file name on the tree's root row goes red while anything is unwritten,
counting both a dirty document and unsaved fields. Two things this needed:
the nav tree runs on its own style, because a themed Treeview's selected
foreground beats a row tag's; and the tag is added to or removed from the row
rather than recoloured, because reconfiguring a tag does not repaint rows that
are already drawn.

Also fixes a latent hazard the panel exposed: Del and Backspace are bound
application-wide, so they now stand down while a text field has focus rather
than deleting the component behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 23:38:03 +03:00
co-authored by Claude Opus 5
parent 5fde6473a0
commit 2db230dec1
8 changed files with 1675 additions and 15 deletions
+57
View File
@@ -26,6 +26,34 @@ def restore_children(element, snapshot):
element.append(copy.deepcopy(child))
def set_prop_value(element, name, value):
"""Writes a component's <prop>, creating it in the block the other props sit in.
A .sax keeps every <prop> ahead of the child <comp> elements, and ET.SubElement
would append past them, so a new prop is inserted after the last existing one
instead. That is cosmetic to a parser and not to a human reading the diff.
"""
prop = element.find(f"prop[@name='{name}']")
if prop is not None:
prop.set("val", value)
return prop
prop = ET.Element("prop", name=name, val=value)
last_prop_index = -1
for index, child in enumerate(element):
if child.tag == "prop":
last_prop_index = index
element.insert(last_prop_index + 1, prop)
return prop
def remove_prop(element, name):
"""Drops a component's <prop>, for undoing a value that the file never carried."""
prop = element.find(f"prop[@name='{name}']")
if prop is not None:
element.remove(prop)
def read_meta_property(meta_str):
"""Parses 32-bit meta property integer fields via structured byte extraction rules."""
if meta_str is None or not meta_str:
@@ -77,6 +105,35 @@ def update_meta_property(element, meta_str, raw_x, raw_y):
meta_prop.set("val", str(new_val))
def update_meta_groups(element, meta_str, groups):
"""Rewrites only meta's four user-group bits, leaving the packed position alone.
The mirror of update_meta_property, which rewrites only x and y. Both exist because
meta is one integer carrying two unrelated things, and an editor that touches one of
them must not disturb the other.
"""
raw_x, raw_y, reserved, _, _, _, _, ug_res = read_meta_property(meta_str)
user_group_byte = 0
for bit, on in enumerate(groups[:4]):
if on:
user_group_byte |= (1 << bit)
user_group_byte |= (ug_res & 0x0F) << 4
new_val = (
((raw_x & 0xFF) << 24) |
((raw_y & 0xFF) << 16) |
((reserved & 0xFF) << 8) |
(user_group_byte & 0xFF)
)
meta_prop = element.find("prop[@name='meta']")
if meta_prop is None:
meta_prop = ET.SubElement(element, "prop", name="meta")
meta_prop.set("val", str(new_val))
def find_parent_element(root_elem, child_to_find):
"""Traverses the XML DOM tree recursively to find the parent element of a given element."""
for parent in root_elem.iter():