Right-clicking a wire or a knob now offers Go to Link Wire Sheet "<name>", naming the sheet the far end is on -- the end a grey knob stands in for. A link with both ends on the open sheet has nowhere to go, so the entry is left off rather than greyed out. find_component_by_path turns a link's from/to reference back into the component it points at, the inverse of get_component_path, and the sheet is that component's parent; a parent of <app> is the file's root node, which the popup names after the file. Arriving selects the same link and scrolls it to the middle of the view. It is found by its element rather than by the tag it was picked on, since a repaint hands out fresh wire tags, and it waits for that repaint from an after_idle callback: ttk queues its selection event rather than dispatching it, so the sheet is not painted when the tree node is selected -- and painting a sheet clears the selected link, which was wiping the selection this feature had just made. The jump is an ordinary sheet opening, so Back returns to where the link was picked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
"""Document-level helpers for a Sedona app: the packed 'meta' property and component paths.
|
|
|
|
Pure XML and bit work, deliberately free of Tk so the rules stay testable headless.
|
|
"""
|
|
|
|
import copy
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
def snapshot_children(element):
|
|
"""Copies an element's children and leading text, so it can be put back wholesale.
|
|
|
|
Used for the <schema> block, which is short enough that copying it whole is easier to
|
|
trust than tracking which attribute moved and which entry was inserted where.
|
|
"""
|
|
return [copy.deepcopy(child) for child in element], element.text
|
|
|
|
|
|
def restore_children(element, snapshot):
|
|
"""Puts back what snapshot_children captured, whitespace and all."""
|
|
children, text = snapshot
|
|
for child in list(element):
|
|
element.remove(child)
|
|
element.text = text
|
|
for child in children:
|
|
element.append(copy.deepcopy(child))
|
|
|
|
|
|
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:
|
|
return 0, 0, 0, True, False, False, False, 0
|
|
try:
|
|
if meta_str.startswith("0x") or meta_str.startswith("0X"):
|
|
meta_val = int(meta_str, 16)
|
|
else:
|
|
meta_val = int(meta_str)
|
|
|
|
raw_x = (meta_val >> 24) & 0xFF
|
|
raw_y = (meta_val >> 16) & 0xFF
|
|
reserved = (meta_val >> 8) & 0xFF
|
|
user_group_byte = meta_val & 0xFF
|
|
|
|
group1 = bool(user_group_byte & 0x01)
|
|
group2 = bool(user_group_byte & 0x02)
|
|
group3 = bool(user_group_byte & 0x04)
|
|
group4 = bool(user_group_byte & 0x08)
|
|
user_group_reserved = (user_group_byte >> 4) & 0x0F
|
|
|
|
return raw_x, raw_y, reserved, group1, group2, group3, group4, user_group_reserved
|
|
except Exception:
|
|
return 0, 0, 0, True, False, False, False, 0
|
|
|
|
|
|
def update_meta_property(element, meta_str, raw_x, raw_y):
|
|
"""Re-encodes coordinates back into meta, completely preserving existing lower and consecutive bytes."""
|
|
_, _, reserved, g1, g2, g3, g4, ug_res = read_meta_property(meta_str)
|
|
|
|
user_group_byte = 0
|
|
if g1: user_group_byte |= 0x01
|
|
if g2: user_group_byte |= 0x02
|
|
if g3: user_group_byte |= 0x04
|
|
if g4: user_group_byte |= 0x08
|
|
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():
|
|
if child_to_find in list(parent):
|
|
return parent
|
|
return None
|
|
|
|
|
|
def find_component_by_path(root_elem, path):
|
|
"""The component an absolute path names, or None if the document has no such node.
|
|
|
|
The inverse of get_component_path, and the way a <link>'s from/to reference is
|
|
turned back into the component it points at. Paths carry no entry for <app>, so
|
|
resolution starts among its children.
|
|
"""
|
|
if root_elem is None or not path:
|
|
return None
|
|
|
|
names = [part for part in path.split("/") if part]
|
|
if not names:
|
|
return None
|
|
|
|
node = root_elem.find("app")
|
|
if node is None:
|
|
node = root_elem
|
|
|
|
for name in names:
|
|
node = next(
|
|
(child for child in node if child.tag == "comp" and child.get("name") == name),
|
|
None,
|
|
)
|
|
if node is None:
|
|
return None
|
|
return node
|
|
|
|
|
|
def get_component_path(element, root_elem):
|
|
"""Computes the absolute path of a component within the application tree hierarchy."""
|
|
path_parts = []
|
|
curr = element
|
|
while curr is not None and curr.tag == "comp":
|
|
path_parts.append(curr.get("name", ""))
|
|
curr = find_parent_element(root_elem, curr)
|
|
path_parts.reverse()
|
|
return "/" + "/".join(path_parts)
|