Grey out incompatible slots in the link dialog

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>
This commit is contained in:
2026-08-20 21:58:17 +03:00
co-authored by Claude Opus 4.6
parent 424caf777c
commit 47bfaf04e0
2 changed files with 110 additions and 17 deletions
+94 -14
View File
@@ -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)