diff --git a/main.py b/main.py index 9adb83c..62f7413 100644 --- a/main.py +++ b/main.py @@ -198,11 +198,19 @@ def do_link_mark(): app_state.link_mark_element = comp +def _slot_linkable(slot_flags, slot_type, readonly, is_target): + """Returns False if a slot cannot participate in a link on the given side.""" + if is_target and readonly: + return False + if "a" in slot_flags or slot_type == "void": + return False + return True + + def open_link_dialog(source_element, target_element): """Opens a dialog to select source and target slots, validates types, and creates a link.""" window = app_state.window - # Mutable direction state: [left_element, right_element] sides = [source_element, target_element] dialog = tk.Toplevel(window) @@ -227,9 +235,71 @@ def open_link_dialog(source_element, target_element): src_var = tk.StringVar(value="") tgt_var = tk.StringVar(value="") - def populate_panel(parent_frame, slots, var): + src_widgets = [] + tgt_widgets = [] + src_slot_info = {} + tgt_slot_info = {} + + status_var = tk.StringVar(value="") + + def _update_peer_states(selected_var, selected_info, peer_widgets, peer_info, peer_is_target): + """Grey out incompatible slots on the peer side after a selection.""" + chosen = selected_var.get() + if not chosen or chosen not in selected_info: + for rb in peer_widgets: + name = rb.cget("value") + info = peer_info.get(name) + if info and _slot_linkable(info[1], info[0], info[2], peer_is_target): + rb.config(state=tk.NORMAL, fg="black") + else: + rb.config(state=tk.DISABLED, fg="#999999") + return + + sel_type = selected_info[chosen][0] + + for rb in peer_widgets: + name = rb.cget("value") + info = peer_info.get(name) + if not info: + rb.config(state=tk.DISABLED, fg="#999999") + continue + + peer_type, peer_flags, peer_ro = info + if not _slot_linkable(peer_flags, peer_type, peer_ro, peer_is_target): + rb.config(state=tk.DISABLED, fg="#999999") + elif peer_type != sel_type: + rb.config(state=tk.DISABLED, fg="#999999") + else: + rb.config(state=tk.NORMAL, fg="black") + + def _on_src_change(*_args): + _update_peer_states(src_var, src_slot_info, tgt_widgets, tgt_slot_info, True) + _update_status_label() + + def _on_tgt_change(*_args): + _update_peer_states(tgt_var, tgt_slot_info, src_widgets, src_slot_info, False) + _update_status_label() + + def _update_status_label(): + s = src_var.get() + t = tgt_var.get() + left_name = sides[0].get("name", "?") + right_name = sides[1].get("name", "?") + if s and t: + status_var.set(f"Link {left_name}.{s} -> {right_name}.{t}") + elif s: + status_var.set(f"Link {left_name}.{s} -> ?") + elif t: + status_var.set(f"Link ? -> {right_name}.{t}") + else: + status_var.set("") + + def populate_panel(parent_frame, slots, var, widgets_list, info_dict, is_target): for child in parent_frame.winfo_children(): child.destroy() + widgets_list.clear() + info_dict.clear() + panel_canvas = tk.Canvas(parent_frame, width=160, height=300, highlightthickness=0) panel_scroll = ttk.Scrollbar(parent_frame, orient=tk.VERTICAL, command=panel_canvas.yview) inner = tk.Frame(panel_canvas) @@ -237,10 +307,18 @@ def open_link_dialog(source_element, target_element): panel_canvas.config(yscrollcommand=panel_scroll.set) panel_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) panel_scroll.pack(side=tk.RIGHT, fill=tk.Y) - for slot_name, slot_type in slots: + + for slot_name, slot_type, slot_flags, slot_ro in slots: + info_dict[slot_name] = (slot_type, slot_flags, slot_ro) + linkable = _slot_linkable(slot_flags, slot_type, slot_ro, is_target) rb = tk.Radiobutton(inner, text=slot_name, variable=var, value=slot_name, - anchor="w", bg="#c8d4e8", relief="flat", padx=4, pady=1) + anchor="w", bg="#c8d4e8", relief="flat", padx=4, pady=1, + state=tk.NORMAL if linkable else tk.DISABLED, + fg="black" if linkable else "#999999", + disabledforeground="#999999") rb.pack(fill=tk.X) + widgets_list.append(rb) + inner.update_idletasks() panel_canvas.config(scrollregion=panel_canvas.bbox("all")) @@ -260,8 +338,12 @@ def open_link_dialog(source_element, target_element): src_var.set("") tgt_var.set("") - populate_panel(src_frame, left_slots, src_var) - populate_panel(tgt_frame, right_slots, tgt_var) + populate_panel(src_frame, left_slots, src_var, src_widgets, src_slot_info, False) + populate_panel(tgt_frame, right_slots, tgt_var, tgt_widgets, tgt_slot_info, True) + _update_status_label() + + src_var.trace_add("write", _on_src_change) + tgt_var.trace_add("write", _on_tgt_change) def on_swap(): sides[0], sides[1] = sides[1], sides[0] @@ -271,6 +353,9 @@ def open_link_dialog(source_element, target_element): refresh_panels() + status_label = tk.Label(main_frame, textvariable=status_var, anchor="w", fg="#333333") + status_label.grid(row=1, column=0, columnspan=3, sticky="w", pady=(8, 0)) + def on_ok(): src_slot = src_var.get() tgt_slot = tgt_var.get() @@ -279,13 +364,8 @@ def open_link_dialog(source_element, target_element): messagebox.showwarning("Link", "Select a slot from both source and target.", parent=dialog) return - left_type = sides[0].get("type", "") - right_type = sides[1].get("type", "") - left_type_map = {s[0]: s[1] for s in schema.get_all_slots_for_type(left_type)} - right_type_map = {s[0]: s[1] for s in schema.get_all_slots_for_type(right_type)} - - st = left_type_map.get(src_slot, "?") - tt = right_type_map.get(tgt_slot, "?") + st = src_slot_info.get(src_slot, ("?", ""))[0] + tt = tgt_slot_info.get(tgt_slot, ("?", ""))[0] if st != tt: messagebox.showwarning( @@ -307,7 +387,7 @@ def open_link_dialog(source_element, target_element): sax_file.add_link(from_ref, to_ref) btn_frame = tk.Frame(main_frame) - btn_frame.grid(row=1, column=0, columnspan=3, pady=(15, 0)) + btn_frame.grid(row=2, column=0, columnspan=3, pady=(15, 0)) ttk.Button(btn_frame, text="OK", width=10, command=on_ok).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="Cancel", width=10, command=dialog.destroy).pack(side=tk.LEFT, padx=5) diff --git a/schema.py b/schema.py index 7cb157e..6d84adc 100644 --- a/schema.py +++ b/schema.py @@ -53,6 +53,17 @@ def read_summary_facet(slot_element): 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: @@ -72,7 +83,8 @@ def get_slots_with_inheritance(type_name, kit_schemas): "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) + "flags": slot.get("flags", ""), + "readonly": read_readonly_facet(slot), "summary": read_summary_facet(slot), "declared_id": int(slot.get("id")) }) @@ -110,6 +122,7 @@ def compile_schema_registry(manifest_paths): "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 @@ -162,11 +175,11 @@ def load_schema_kits_and_manifests(): def get_all_slots_for_type(type_str): - """Returns every slot for a type as (name, type_string) tuples, for the link dialog.""" + """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["name"], slot["type"], slot["flags"], slot["readonly"]) for slot in app_state.final_components_registry[type_str] if slot["name"] ]