"""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//-.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 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", ""), # Capture visibility flags ('o' maps to operator/hidden) "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"], "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_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