Files
sedona_editor/sax_dom.py
T
ArdaandClaude Opus 5 01dadea610 Manage kits: reconcile the schema on save, and a dialog to repin builds
Placing a palette component no longer edits <schema> straight away. The kit is
noted as pending, its manifest is compiled so the box can be painted, and the
status bar says the kit will be declared on save. The <schema> block is then
reconciled when the file is written, in the one place that can report the whole
kit list the file will carry, with each kit's version and checksum.

What a kit's manifest depends on counts as used too, transitively, so placing a
component can bring in more than one entry: hvac pulls in types, ontrolPion
pulls in ontrolPlatform.

New Kits > Manage Kits... dialog lists every kit in three groups -- needed by
the app, declared but unused, and merely installed -- with the pinned build, the
newest installed build, and an Action dropdown of every build a kit ships.
The tick means the schema declares the kit: grey and fixed for kits the app
needs, unticked to remove, ticked to add at the newest build. "Upgrade all kits
to latest version" moves every declared kit; "Update kits" applies the ticks and
the Action column, additions and removals included.

Every schema change is applied as a batch, then verified: a missing manifest or
a component type that stops resolving reverts the whole block and reports why,
and a kit the app needs cannot be removed. Changes are one undo step, which
recompiles the slot registry in both directions since a repin can change a
component's slots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:06:25 +03:00

97 lines
3.2 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 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)