Parse the readonly facet from kit manifests and use it to disable slots that cannot participate in a link: read-only slots are greyed on the target side only (they remain selectable as sources), action and void slots are greyed on both sides, and type mismatches are greyed when the other side has a selection. A status label previews the link path at the bottom of the dialog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
8.0 KiB
Python
226 lines
8.0 KiB
Python
"""Kit manifest loading, slot inheritance, and which slots are worth showing."""
|
|
|
|
import os
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import app_state
|
|
|
|
|
|
class MissingManifestError(Exception):
|
|
"""A kit the file needs has no manifest under the Sedona home folder.
|
|
|
|
Raised rather than handled here, so the caller decides what to abandon: opening a
|
|
file abandons the whole load, placing a palette component only abandons that
|
|
component. Carrying the kit names lets either one name them in its own wording.
|
|
"""
|
|
|
|
def __init__(self, kit_names, unresolved_type=None):
|
|
self.kit_names = sorted(set(kit_names))
|
|
self.unresolved_type = unresolved_type
|
|
super().__init__(", ".join(self.kit_names))
|
|
|
|
|
|
def describe_missing_manifests(error):
|
|
"""The body of the dialog both callers show, pointing at the folder to check."""
|
|
kit_list = "\n".join(f" • {name}" for name in error.kit_names)
|
|
home = app_state.SEDONA_HOME or "(not set)"
|
|
|
|
lines = ["No manifest was found for:", "", kit_list, "", f"Sedona home folder:\n {home}", ""]
|
|
|
|
if error.unresolved_type:
|
|
lines.append(
|
|
f"Type '{error.unresolved_type}' inherits from a kit that is not in the "
|
|
"file's schema block, so its slots cannot be resolved."
|
|
)
|
|
lines.append("")
|
|
|
|
lines.append(
|
|
"Check that the Sedona home folder is the right one and that it holds "
|
|
"manifests/<kit>/<kit>-<checksum>.xml for each kit listed above. "
|
|
"Preferences → Sedona home folder points it somewhere else."
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def read_summary_facet(slot_element):
|
|
"""Reads a slot's '@summary' facet. Absent means summary, matching manifest convention."""
|
|
facets = slot_element.find("facets")
|
|
if facets is None:
|
|
return True
|
|
for bool_facet in facets.findall("bool"):
|
|
if bool_facet.get("name") == "summary":
|
|
return (bool_facet.get("val", "true").strip().lower() != "false")
|
|
return True
|
|
|
|
|
|
def read_readonly_facet(slot_element):
|
|
"""Reads a slot's '@readonly' facet. Absent means writable."""
|
|
facets = slot_element.find("facets")
|
|
if facets is None:
|
|
return False
|
|
for bool_facet in facets.findall("bool"):
|
|
if bool_facet.get("name") == "readonly":
|
|
return (bool_facet.get("val", "false").strip().lower() == "true")
|
|
return False
|
|
|
|
|
|
def get_slots_with_inheritance(type_name, kit_schemas):
|
|
"""Recursively resolves the slots for a given type by traversing its base chain."""
|
|
if type_name not in kit_schemas:
|
|
kit_prefix = type_name.split("::")[0] if "::" in type_name else type_name
|
|
raise MissingManifestError([kit_prefix], unresolved_type=type_name)
|
|
|
|
type_element = kit_schemas[type_name]
|
|
base_type = type_element.get("base")
|
|
|
|
resolved_slots = []
|
|
if base_type:
|
|
resolved_slots = get_slots_with_inheritance(base_type, kit_schemas)
|
|
|
|
current_slots = []
|
|
for slot in type_element.findall("slot"):
|
|
current_slots.append({
|
|
"name": slot.get("name"),
|
|
"type": slot.get("type"),
|
|
"default": slot.get("default", "false"),
|
|
"flags": slot.get("flags", ""),
|
|
"readonly": read_readonly_facet(slot),
|
|
"summary": read_summary_facet(slot),
|
|
"declared_id": int(slot.get("id"))
|
|
})
|
|
|
|
current_slots.sort(key=lambda s: s["declared_id"])
|
|
resolved_slots.extend(current_slots)
|
|
return resolved_slots
|
|
|
|
|
|
def compile_schema_registry(manifest_paths):
|
|
"""Loads and maps types across multiple kit files, ensuring all dependencies exist."""
|
|
kit_schemas = {}
|
|
|
|
for path in manifest_paths:
|
|
try:
|
|
tree = ET.parse(path)
|
|
root = tree.getroot()
|
|
kit_name = root.get("name")
|
|
|
|
for type_tag in root.findall("type"):
|
|
type_name = f"{kit_name}::{type_tag.get('name')}"
|
|
kit_schemas[type_name] = type_tag
|
|
except Exception as e:
|
|
print(f"Error parsing manifest {path}: {e}")
|
|
|
|
app_state.final_components_registry.clear()
|
|
for type_name in kit_schemas.keys():
|
|
flat_slots = get_slots_with_inheritance(type_name, kit_schemas)
|
|
|
|
resolved_type_structure = []
|
|
for sequence_id, slot_data in enumerate(flat_slots):
|
|
resolved_type_structure.append({
|
|
"slot_id": sequence_id,
|
|
"name": slot_data["name"],
|
|
"type": slot_data["type"],
|
|
"default": slot_data["default"],
|
|
"flags": slot_data["flags"],
|
|
"readonly": slot_data["readonly"],
|
|
"summary": slot_data["summary"]
|
|
})
|
|
app_state.final_components_registry[type_name] = resolved_type_structure
|
|
|
|
|
|
def load_schema_kits_and_manifests():
|
|
"""Scans the schema block of the SAX file and maps/loads corresponding manifest definitions."""
|
|
app_state.active_schema_kits.clear()
|
|
if app_state.xml_root_element is None:
|
|
return
|
|
|
|
schema_elem = app_state.xml_root_element.find("schema")
|
|
if schema_elem is None:
|
|
return
|
|
|
|
manifest_paths = []
|
|
missing_kits = []
|
|
for kit in schema_elem.findall("kit"):
|
|
name = kit.get("name")
|
|
checksum = kit.get("checksum")
|
|
if not name:
|
|
continue
|
|
app_state.active_schema_kits[name] = checksum
|
|
|
|
manifest_dir = os.path.join(app_state.SEDONA_HOME, "manifests", name)
|
|
manifest_path = None
|
|
|
|
if checksum:
|
|
target_file = os.path.join(manifest_dir, f"{name}-{checksum}.xml")
|
|
if os.path.exists(target_file):
|
|
manifest_path = target_file
|
|
|
|
if not manifest_path and os.path.exists(manifest_dir):
|
|
files = [f for f in os.listdir(manifest_dir) if f.startswith(f"{name}-") and f.endswith(".xml")]
|
|
if files:
|
|
files.sort()
|
|
manifest_path = os.path.join(manifest_dir, files[-1])
|
|
|
|
if manifest_path:
|
|
manifest_paths.append(manifest_path)
|
|
else:
|
|
# Every missing kit is collected before reporting, so one dialog names them
|
|
# all rather than the user fixing them one reopen at a time.
|
|
missing_kits.append(name)
|
|
|
|
if missing_kits:
|
|
raise MissingManifestError(missing_kits)
|
|
|
|
compile_schema_registry(manifest_paths)
|
|
|
|
|
|
def get_all_slots_for_type(type_str):
|
|
"""Returns every slot for a type as (name, type_string, flags, readonly) tuples."""
|
|
if type_str not in app_state.final_components_registry:
|
|
return []
|
|
return [
|
|
(slot["name"], slot["type"], slot["flags"], slot["readonly"])
|
|
for slot in app_state.final_components_registry[type_str]
|
|
if slot["name"]
|
|
]
|
|
|
|
|
|
def get_slots_for_type(type_str, linked_slots=()):
|
|
"""
|
|
Returns the slots worth painting for a given type, prefixed with double-digit
|
|
index tags taken from the manifest so hidden rows leave gaps rather than renumber.
|
|
|
|
Visibility, in order of precedence:
|
|
- a linked slot is always shown, so its wire has a row to anchor to
|
|
- '@summary false' hides the slot, even when it is runtime
|
|
- runtime slots (no 'c' flag) are shown; they hold the live signal path
|
|
- config slots that nothing links to are hidden as clutter
|
|
- the 'o' (operator) flag hides a slot outright
|
|
"""
|
|
if type_str not in app_state.final_components_registry:
|
|
return []
|
|
|
|
slots_list = []
|
|
for slot_data in app_state.final_components_registry[type_str]:
|
|
slot_name = slot_data["name"]
|
|
if not slot_name:
|
|
continue
|
|
if "o" in slot_data["flags"]:
|
|
continue
|
|
|
|
is_linked = slot_name in linked_slots
|
|
if not is_linked:
|
|
if not slot_data.get("summary", True):
|
|
continue
|
|
if "c" in slot_data["flags"]:
|
|
continue
|
|
|
|
# Format to double digits (e.g., 00, 01, 02)
|
|
padded_id = f"{slot_data['slot_id']:02d}"
|
|
display_label = f"{padded_id}. {slot_name}"
|
|
|
|
# Keep structural name unchanged for XML lookup inside comp, but pass display label
|
|
slots_list.append((slot_name, display_label))
|
|
|
|
return slots_list
|