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:
@@ -198,11 +198,19 @@ def do_link_mark():
|
|||||||
app_state.link_mark_element = comp
|
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):
|
def open_link_dialog(source_element, target_element):
|
||||||
"""Opens a dialog to select source and target slots, validates types, and creates a link."""
|
"""Opens a dialog to select source and target slots, validates types, and creates a link."""
|
||||||
window = app_state.window
|
window = app_state.window
|
||||||
|
|
||||||
# Mutable direction state: [left_element, right_element]
|
|
||||||
sides = [source_element, target_element]
|
sides = [source_element, target_element]
|
||||||
|
|
||||||
dialog = tk.Toplevel(window)
|
dialog = tk.Toplevel(window)
|
||||||
@@ -227,9 +235,71 @@ def open_link_dialog(source_element, target_element):
|
|||||||
src_var = tk.StringVar(value="")
|
src_var = tk.StringVar(value="")
|
||||||
tgt_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():
|
for child in parent_frame.winfo_children():
|
||||||
child.destroy()
|
child.destroy()
|
||||||
|
widgets_list.clear()
|
||||||
|
info_dict.clear()
|
||||||
|
|
||||||
panel_canvas = tk.Canvas(parent_frame, width=160, height=300, highlightthickness=0)
|
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)
|
panel_scroll = ttk.Scrollbar(parent_frame, orient=tk.VERTICAL, command=panel_canvas.yview)
|
||||||
inner = tk.Frame(panel_canvas)
|
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.config(yscrollcommand=panel_scroll.set)
|
||||||
panel_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
panel_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
panel_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
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,
|
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)
|
rb.pack(fill=tk.X)
|
||||||
|
widgets_list.append(rb)
|
||||||
|
|
||||||
inner.update_idletasks()
|
inner.update_idletasks()
|
||||||
panel_canvas.config(scrollregion=panel_canvas.bbox("all"))
|
panel_canvas.config(scrollregion=panel_canvas.bbox("all"))
|
||||||
|
|
||||||
@@ -260,8 +338,12 @@ def open_link_dialog(source_element, target_element):
|
|||||||
|
|
||||||
src_var.set("")
|
src_var.set("")
|
||||||
tgt_var.set("")
|
tgt_var.set("")
|
||||||
populate_panel(src_frame, left_slots, src_var)
|
populate_panel(src_frame, left_slots, src_var, src_widgets, src_slot_info, False)
|
||||||
populate_panel(tgt_frame, right_slots, tgt_var)
|
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():
|
def on_swap():
|
||||||
sides[0], sides[1] = sides[1], sides[0]
|
sides[0], sides[1] = sides[1], sides[0]
|
||||||
@@ -271,6 +353,9 @@ def open_link_dialog(source_element, target_element):
|
|||||||
|
|
||||||
refresh_panels()
|
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():
|
def on_ok():
|
||||||
src_slot = src_var.get()
|
src_slot = src_var.get()
|
||||||
tgt_slot = tgt_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)
|
messagebox.showwarning("Link", "Select a slot from both source and target.", parent=dialog)
|
||||||
return
|
return
|
||||||
|
|
||||||
left_type = sides[0].get("type", "")
|
st = src_slot_info.get(src_slot, ("?", ""))[0]
|
||||||
right_type = sides[1].get("type", "")
|
tt = tgt_slot_info.get(tgt_slot, ("?", ""))[0]
|
||||||
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, "?")
|
|
||||||
|
|
||||||
if st != tt:
|
if st != tt:
|
||||||
messagebox.showwarning(
|
messagebox.showwarning(
|
||||||
@@ -307,7 +387,7 @@ def open_link_dialog(source_element, target_element):
|
|||||||
sax_file.add_link(from_ref, to_ref)
|
sax_file.add_link(from_ref, to_ref)
|
||||||
|
|
||||||
btn_frame = tk.Frame(main_frame)
|
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="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)
|
ttk.Button(btn_frame, text="Cancel", width=10, command=dialog.destroy).pack(side=tk.LEFT, padx=5)
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ def read_summary_facet(slot_element):
|
|||||||
return True
|
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):
|
def get_slots_with_inheritance(type_name, kit_schemas):
|
||||||
"""Recursively resolves the slots for a given type by traversing its base chain."""
|
"""Recursively resolves the slots for a given type by traversing its base chain."""
|
||||||
if type_name not in kit_schemas:
|
if type_name not in kit_schemas:
|
||||||
@@ -72,7 +83,8 @@ def get_slots_with_inheritance(type_name, kit_schemas):
|
|||||||
"name": slot.get("name"),
|
"name": slot.get("name"),
|
||||||
"type": slot.get("type"),
|
"type": slot.get("type"),
|
||||||
"default": slot.get("default", "false"),
|
"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),
|
"summary": read_summary_facet(slot),
|
||||||
"declared_id": int(slot.get("id"))
|
"declared_id": int(slot.get("id"))
|
||||||
})
|
})
|
||||||
@@ -110,6 +122,7 @@ def compile_schema_registry(manifest_paths):
|
|||||||
"type": slot_data["type"],
|
"type": slot_data["type"],
|
||||||
"default": slot_data["default"],
|
"default": slot_data["default"],
|
||||||
"flags": slot_data["flags"],
|
"flags": slot_data["flags"],
|
||||||
|
"readonly": slot_data["readonly"],
|
||||||
"summary": slot_data["summary"]
|
"summary": slot_data["summary"]
|
||||||
})
|
})
|
||||||
app_state.final_components_registry[type_name] = resolved_type_structure
|
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):
|
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:
|
if type_str not in app_state.final_components_registry:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
(slot["name"], slot["type"])
|
(slot["name"], slot["type"], slot["flags"], slot["readonly"])
|
||||||
for slot in app_state.final_components_registry[type_str]
|
for slot in app_state.final_components_registry[type_str]
|
||||||
if slot["name"]
|
if slot["name"]
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user