247 lines
8.8 KiB
Python
247 lines
8.8 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 find_manifest_path(kit_name, checksum):
|
|
"""The manifest file describing a kit, or None when Sedona home holds none.
|
|
|
|
A declared checksum is honoured when its file exists; otherwise the last manifest by
|
|
filename is taken, the tolerance that lets a file with no checksums still open.
|
|
"""
|
|
manifest_dir = os.path.join(app_state.SEDONA_HOME, "manifests", kit_name)
|
|
|
|
if checksum:
|
|
target_file = os.path.join(manifest_dir, f"{kit_name}-{checksum}.xml")
|
|
if os.path.exists(target_file):
|
|
return target_file
|
|
|
|
if os.path.exists(manifest_dir):
|
|
files = [
|
|
f for f in os.listdir(manifest_dir)
|
|
if f.startswith(f"{kit_name}-") and f.endswith(".xml")
|
|
]
|
|
if files:
|
|
files.sort()
|
|
return os.path.join(manifest_dir, files[-1])
|
|
|
|
return None
|
|
|
|
|
|
def load_schema_kits_and_manifests():
|
|
"""Loads the manifests for the file's <schema> kits, plus any kit still only pending.
|
|
|
|
A component placed from the palette records its kit in ``pending_schema_kits`` rather
|
|
than writing to <schema> straight away — that reconciliation happens on save. Its
|
|
types still have to resolve now, or the box could not be painted, so pending kits are
|
|
compiled into the registry alongside the declared ones.
|
|
"""
|
|
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
|
|
|
|
for kit in schema_elem.findall("kit"):
|
|
name = kit.get("name")
|
|
if name:
|
|
app_state.active_schema_kits[name] = kit.get("checksum")
|
|
|
|
kits_to_load = dict(app_state.active_schema_kits)
|
|
for name, checksum in app_state.pending_schema_kits.items():
|
|
kits_to_load.setdefault(name, checksum)
|
|
|
|
manifest_paths = []
|
|
missing_kits = []
|
|
for name, checksum in kits_to_load.items():
|
|
manifest_path = find_manifest_path(name, checksum)
|
|
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
|