Add link management: create, select, delete with undo/redo
Implement full link workflow between component slots on the wiresheet: - Link Mark / Link From / Link To context menu items for creating links - Link dialog with slot type validation and source/target swap button - Selectable link wires (click to highlight in red) with right-click delete - Undo/redo support for both add and delete link operations - Component deletion already cascades to remove related links Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,9 @@ current_file_path = None
|
|||||||
active_components_list = [] # Tracks what components are actively painted
|
active_components_list = [] # Tracks what components are actively painted
|
||||||
selected_canvas_tag = None # Tracks the currently clicked/highlighted canvas component tag
|
selected_canvas_tag = None # Tracks the currently clicked/highlighted canvas component tag
|
||||||
selected_tree_item_id = None # Tracks the currently right-clicked Treeview item
|
selected_tree_item_id = None # Tracks the currently right-clicked Treeview item
|
||||||
|
link_mark_element = None # Component marked as a link endpoint via "Link Mark"
|
||||||
|
selected_link_tag = None # Canvas tag of the currently selected (red-highlighted) link wire
|
||||||
|
canvas_link_map = {} # Canvas link tag -> ET.Element (<link>)
|
||||||
xml_root_element = None # Keep a persistent root reference to redraw tree layouts cleanly
|
xml_root_element = None # Keep a persistent root reference to redraw tree layouts cleanly
|
||||||
current_sheet_parent = None # Element whose children are painted; None means the app root
|
current_sheet_parent = None # Element whose children are painted; None means the app root
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ from tkinter import filedialog, messagebox, ttk
|
|||||||
import app_state
|
import app_state
|
||||||
import palette
|
import palette
|
||||||
import sax_file
|
import sax_file
|
||||||
|
import schema
|
||||||
import undo
|
import undo
|
||||||
import wiresheet
|
import wiresheet
|
||||||
from sax_dom import read_meta_property, update_meta_property
|
from sax_dom import get_component_path, read_meta_property, update_meta_property
|
||||||
|
|
||||||
|
|
||||||
def refresh_all_views():
|
def refresh_all_views():
|
||||||
@@ -88,8 +89,32 @@ def on_tree_select(event):
|
|||||||
wiresheet.render_components(components_to_render)
|
wiresheet.render_components(components_to_render)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_selected_link():
|
||||||
|
"""Removes the currently selected link from the DOM and repaints."""
|
||||||
|
if not app_state.selected_link_tag:
|
||||||
|
return
|
||||||
|
link_elem = app_state.canvas_link_map.get(app_state.selected_link_tag)
|
||||||
|
if link_elem is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
from_ref = link_elem.get("from", "")
|
||||||
|
to_ref = link_elem.get("to", "")
|
||||||
|
confirm = messagebox.askyesno(
|
||||||
|
"Delete Link",
|
||||||
|
f"Delete link?\n\n from: {from_ref}\n to: {to_ref}"
|
||||||
|
)
|
||||||
|
if not confirm:
|
||||||
|
return
|
||||||
|
|
||||||
|
sax_file.remove_link(link_elem)
|
||||||
|
|
||||||
|
|
||||||
def delete_selected_component(event=None):
|
def delete_selected_component(event=None):
|
||||||
"""Deletes the highlighted canvas component and all its descendants recursively."""
|
"""Deletes the highlighted canvas component (or selected link) and all its descendants recursively."""
|
||||||
|
if app_state.selected_link_tag:
|
||||||
|
delete_selected_link()
|
||||||
|
return
|
||||||
|
|
||||||
if not app_state.selected_canvas_tag:
|
if not app_state.selected_canvas_tag:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -128,18 +153,24 @@ def on_tree_right_click(event):
|
|||||||
def on_canvas_click(event):
|
def on_canvas_click(event):
|
||||||
canvas = app_state.canvas
|
canvas = app_state.canvas
|
||||||
canvas.itemconfig("block_outline", outline="#7f9db9", width=2)
|
canvas.itemconfig("block_outline", outline="#7f9db9", width=2)
|
||||||
|
wiresheet.deselect_link()
|
||||||
app_state.drag_data["tag"] = None
|
app_state.drag_data["tag"] = None
|
||||||
app_state.selected_canvas_tag = None
|
app_state.selected_canvas_tag = None
|
||||||
|
|
||||||
cx = canvas.canvasx(event.x)
|
cx = canvas.canvasx(event.x)
|
||||||
cy = canvas.canvasy(event.y)
|
cy = canvas.canvasy(event.y)
|
||||||
clicked_item = canvas.find_closest(cx, cy)
|
clicked_item = canvas.find_closest(cx, cy, halo=3)
|
||||||
|
|
||||||
if not clicked_item:
|
if not clicked_item:
|
||||||
return
|
return
|
||||||
|
|
||||||
tags = canvas.gettags(clicked_item[0])
|
tags = canvas.gettags(clicked_item[0])
|
||||||
group_tag = next((t for t in tags if t.startswith("comp_group_")), None)
|
group_tag = next((t for t in tags if t.startswith("comp_group_")), None)
|
||||||
|
wire_tag = next((t for t in tags if t.startswith("link_wire_")), None)
|
||||||
|
|
||||||
|
if wire_tag:
|
||||||
|
wiresheet.select_link(wire_tag)
|
||||||
|
return
|
||||||
|
|
||||||
if group_tag:
|
if group_tag:
|
||||||
app_state.drag_data["tag"] = group_tag
|
app_state.drag_data["tag"] = group_tag
|
||||||
@@ -158,22 +189,172 @@ def on_canvas_click(event):
|
|||||||
wiresheet.draw_links()
|
wiresheet.draw_links()
|
||||||
|
|
||||||
|
|
||||||
|
def do_link_mark():
|
||||||
|
"""Marks the currently selected canvas component as a link endpoint."""
|
||||||
|
if not app_state.selected_canvas_tag:
|
||||||
|
return
|
||||||
|
comp = app_state.canvas_comp_map.get(app_state.selected_canvas_tag)
|
||||||
|
if comp is not None:
|
||||||
|
app_state.link_mark_element = comp
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
dialog.title("Link")
|
||||||
|
dialog.resizable(False, False)
|
||||||
|
dialog.transient(window)
|
||||||
|
dialog.grab_set()
|
||||||
|
dialog.geometry(f"+{window.winfo_x() + 100}+{window.winfo_y() + 60}")
|
||||||
|
|
||||||
|
main_frame = tk.Frame(dialog, padx=10, pady=10)
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
src_frame = tk.LabelFrame(main_frame, padx=5, pady=5)
|
||||||
|
src_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
|
||||||
|
|
||||||
|
mid_frame = tk.Frame(main_frame)
|
||||||
|
mid_frame.grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
tgt_frame = tk.LabelFrame(main_frame, padx=5, pady=5)
|
||||||
|
tgt_frame.grid(row=0, column=2, sticky="nsew", padx=(5, 0))
|
||||||
|
|
||||||
|
src_var = tk.StringVar(value="")
|
||||||
|
tgt_var = tk.StringVar(value="")
|
||||||
|
|
||||||
|
def populate_panel(parent_frame, slots, var):
|
||||||
|
for child in parent_frame.winfo_children():
|
||||||
|
child.destroy()
|
||||||
|
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)
|
||||||
|
panel_canvas.create_window((0, 0), window=inner, anchor="nw")
|
||||||
|
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:
|
||||||
|
rb = tk.Radiobutton(inner, text=slot_name, variable=var, value=slot_name,
|
||||||
|
anchor="w", bg="#c8d4e8", relief="flat", padx=4, pady=1)
|
||||||
|
rb.pack(fill=tk.X)
|
||||||
|
inner.update_idletasks()
|
||||||
|
panel_canvas.config(scrollregion=panel_canvas.bbox("all"))
|
||||||
|
|
||||||
|
def refresh_panels():
|
||||||
|
left = sides[0]
|
||||||
|
right = sides[1]
|
||||||
|
left_name = left.get("name", "?")
|
||||||
|
right_name = right.get("name", "?")
|
||||||
|
left_type = left.get("type", "")
|
||||||
|
right_type = right.get("type", "")
|
||||||
|
|
||||||
|
src_frame.config(text=f"{left_name} [Source]")
|
||||||
|
tgt_frame.config(text=f"{right_name} [Target]")
|
||||||
|
|
||||||
|
left_slots = schema.get_all_slots_for_type(left_type)
|
||||||
|
right_slots = schema.get_all_slots_for_type(right_type)
|
||||||
|
|
||||||
|
src_var.set("")
|
||||||
|
tgt_var.set("")
|
||||||
|
populate_panel(src_frame, left_slots, src_var)
|
||||||
|
populate_panel(tgt_frame, right_slots, tgt_var)
|
||||||
|
|
||||||
|
def on_swap():
|
||||||
|
sides[0], sides[1] = sides[1], sides[0]
|
||||||
|
refresh_panels()
|
||||||
|
|
||||||
|
ttk.Button(mid_frame, text="⇄", width=3, command=on_swap).pack(pady=5)
|
||||||
|
|
||||||
|
refresh_panels()
|
||||||
|
|
||||||
|
def on_ok():
|
||||||
|
src_slot = src_var.get()
|
||||||
|
tgt_slot = tgt_var.get()
|
||||||
|
|
||||||
|
if not src_slot or not tgt_slot:
|
||||||
|
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, "?")
|
||||||
|
|
||||||
|
if st != tt:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Slot type mismatch",
|
||||||
|
f"Cannot link slots of different types.\n\n"
|
||||||
|
f"Source: {src_slot} ({st})\n"
|
||||||
|
f"Target: {tgt_slot} ({tt})",
|
||||||
|
parent=dialog
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
src_path = get_component_path(sides[0], app_state.xml_root_element)
|
||||||
|
tgt_path = get_component_path(sides[1], app_state.xml_root_element)
|
||||||
|
|
||||||
|
from_ref = f"{src_path}.{src_slot}"
|
||||||
|
to_ref = f"{tgt_path}.{tgt_slot}"
|
||||||
|
|
||||||
|
dialog.destroy()
|
||||||
|
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))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def do_link_from_mark():
|
||||||
|
"""Link From: marked component is source, right-clicked component is target."""
|
||||||
|
if app_state.link_mark_element is None or not app_state.selected_canvas_tag:
|
||||||
|
return
|
||||||
|
target = app_state.canvas_comp_map.get(app_state.selected_canvas_tag)
|
||||||
|
if target is not None:
|
||||||
|
open_link_dialog(app_state.link_mark_element, target)
|
||||||
|
|
||||||
|
|
||||||
|
def do_link_to_mark():
|
||||||
|
"""Link To: right-clicked component is source, marked component is target."""
|
||||||
|
if app_state.link_mark_element is None or not app_state.selected_canvas_tag:
|
||||||
|
return
|
||||||
|
source = app_state.canvas_comp_map.get(app_state.selected_canvas_tag)
|
||||||
|
if source is not None:
|
||||||
|
open_link_dialog(source, app_state.link_mark_element)
|
||||||
|
|
||||||
|
|
||||||
def on_canvas_right_click(event):
|
def on_canvas_right_click(event):
|
||||||
"""Detects right-clicks on nodes to instantly show context-sensitive action overlays."""
|
"""Detects right-clicks on nodes to instantly show context-sensitive action overlays."""
|
||||||
canvas = app_state.canvas
|
canvas = app_state.canvas
|
||||||
cx = canvas.canvasx(event.x)
|
cx = canvas.canvasx(event.x)
|
||||||
cy = canvas.canvasy(event.y)
|
cy = canvas.canvasy(event.y)
|
||||||
clicked_item = canvas.find_closest(cx, cy)
|
clicked_item = canvas.find_closest(cx, cy, halo=3)
|
||||||
|
|
||||||
if clicked_item:
|
if clicked_item:
|
||||||
tags = canvas.gettags(clicked_item[0])
|
tags = canvas.gettags(clicked_item[0])
|
||||||
group_tag = next((t for t in tags if t.startswith("comp_group_")), None)
|
group_tag = next((t for t in tags if t.startswith("comp_group_")), None)
|
||||||
|
wire_tag = next((t for t in tags if t.startswith("link_wire_")), None)
|
||||||
|
|
||||||
|
if wire_tag:
|
||||||
|
wiresheet.select_link(wire_tag)
|
||||||
|
link_context_menu.post(event.x_root, event.y_root)
|
||||||
|
return
|
||||||
|
|
||||||
if group_tag:
|
if group_tag:
|
||||||
|
wiresheet.deselect_link()
|
||||||
canvas.itemconfig("block_outline", outline="#7f9db9", width=2)
|
canvas.itemconfig("block_outline", outline="#7f9db9", width=2)
|
||||||
app_state.selected_canvas_tag = group_tag
|
app_state.selected_canvas_tag = group_tag
|
||||||
canvas.itemconfig(f"{group_tag} && block_outline", outline="#1f4e79", width=3)
|
canvas.itemconfig(f"{group_tag} && block_outline", outline="#1f4e79", width=3)
|
||||||
|
|
||||||
|
_rebuild_canvas_context_menu()
|
||||||
canvas_context_menu.post(event.x_root, event.y_root)
|
canvas_context_menu.post(event.x_root, event.y_root)
|
||||||
|
|
||||||
|
|
||||||
@@ -441,7 +622,25 @@ window.geometry("950x600")
|
|||||||
|
|
||||||
# --- Context Popups Structures ---
|
# --- Context Popups Structures ---
|
||||||
canvas_context_menu = tk.Menu(window, tearoff=0)
|
canvas_context_menu = tk.Menu(window, tearoff=0)
|
||||||
canvas_context_menu.add_command(label="Delete Component", command=delete_selected_component)
|
link_context_menu = tk.Menu(window, tearoff=0)
|
||||||
|
link_context_menu.add_command(label="Delete Link", command=delete_selected_link)
|
||||||
|
|
||||||
|
|
||||||
|
def _rebuild_canvas_context_menu():
|
||||||
|
"""Rebuilds the canvas context menu with dynamic Link From/Link To labels."""
|
||||||
|
canvas_context_menu.delete(0, tk.END)
|
||||||
|
canvas_context_menu.add_command(label="Delete Component", command=delete_selected_component)
|
||||||
|
canvas_context_menu.add_separator()
|
||||||
|
canvas_context_menu.add_command(label="Link Mark", command=do_link_mark)
|
||||||
|
|
||||||
|
if app_state.link_mark_element is not None:
|
||||||
|
mark_name = app_state.link_mark_element.get("name", "?")
|
||||||
|
canvas_context_menu.add_command(
|
||||||
|
label=f'Link From "{mark_name}"', command=do_link_from_mark
|
||||||
|
)
|
||||||
|
canvas_context_menu.add_command(
|
||||||
|
label=f'Link To "{mark_name}"', command=do_link_to_mark
|
||||||
|
)
|
||||||
|
|
||||||
tree_comp_context_menu = tk.Menu(window, tearoff=0)
|
tree_comp_context_menu = tk.Menu(window, tearoff=0)
|
||||||
tree_comp_context_menu.add_command(label="Delete Component", command=delete_tree_selected_component)
|
tree_comp_context_menu.add_command(label="Delete Component", command=delete_tree_selected_component)
|
||||||
|
|||||||
+56
@@ -65,12 +65,15 @@ def reset_to_startup_state():
|
|||||||
app_state.current_sheet_parent = None
|
app_state.current_sheet_parent = None
|
||||||
app_state.selected_canvas_tag = None
|
app_state.selected_canvas_tag = None
|
||||||
app_state.selected_tree_item_id = None
|
app_state.selected_tree_item_id = None
|
||||||
|
app_state.link_mark_element = None
|
||||||
|
app_state.selected_link_tag = None
|
||||||
|
|
||||||
app_state.active_components_list.clear()
|
app_state.active_components_list.clear()
|
||||||
app_state.tree_element_map.clear()
|
app_state.tree_element_map.clear()
|
||||||
app_state.canvas_comp_map.clear()
|
app_state.canvas_comp_map.clear()
|
||||||
app_state.canvas_tag_by_element.clear()
|
app_state.canvas_tag_by_element.clear()
|
||||||
app_state.component_slot_offsets.clear()
|
app_state.component_slot_offsets.clear()
|
||||||
|
app_state.canvas_link_map.clear()
|
||||||
app_state.final_components_registry.clear()
|
app_state.final_components_registry.clear()
|
||||||
app_state.active_schema_kits.clear()
|
app_state.active_schema_kits.clear()
|
||||||
|
|
||||||
@@ -423,6 +426,59 @@ def add_component(kit_name, type_name, grid_x, grid_y):
|
|||||||
return element
|
return element
|
||||||
|
|
||||||
|
|
||||||
|
def remove_link(link_element):
|
||||||
|
"""Removes a <link> element from the <links> section and repaints."""
|
||||||
|
if app_state.xml_root_element is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
links_elem = app_state.xml_root_element.find("links")
|
||||||
|
if links_elem is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = list(links_elem).index(link_element)
|
||||||
|
links_elem.remove(link_element)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
|
from_ref = link_element.get("from", "")
|
||||||
|
to_ref = link_element.get("to", "")
|
||||||
|
from_slot = from_ref.rsplit(".", 1)[-1] if "." in from_ref else from_ref
|
||||||
|
to_slot = to_ref.rsplit(".", 1)[-1] if "." in to_ref else to_ref
|
||||||
|
undo.push(undo.DeleteLinkCommand(
|
||||||
|
links_elem, index, link_element,
|
||||||
|
label=f"delete link {from_slot}→{to_slot}"
|
||||||
|
))
|
||||||
|
|
||||||
|
app_state.selected_link_tag = None
|
||||||
|
app_state.mark_dirty()
|
||||||
|
wiresheet.render_current_sheet()
|
||||||
|
|
||||||
|
|
||||||
|
def add_link(from_path_slot, to_path_slot):
|
||||||
|
"""Adds a <link> element to the <links> section of the SAX DOM."""
|
||||||
|
if app_state.xml_root_element is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
links_elem = app_state.xml_root_element.find("links")
|
||||||
|
if links_elem is None:
|
||||||
|
links_elem = ET.SubElement(app_state.xml_root_element, "links")
|
||||||
|
|
||||||
|
link_elem = ET.SubElement(links_elem, "link")
|
||||||
|
link_elem.set("from", from_path_slot)
|
||||||
|
link_elem.set("to", to_path_slot)
|
||||||
|
|
||||||
|
from_slot = from_path_slot.rsplit(".", 1)[-1] if "." in from_path_slot else from_path_slot
|
||||||
|
to_slot = to_path_slot.rsplit(".", 1)[-1] if "." in to_path_slot else to_path_slot
|
||||||
|
undo.push(undo.AddLinkCommand(
|
||||||
|
links_elem, link_elem,
|
||||||
|
label=f"add link {from_slot}→{to_slot}"
|
||||||
|
))
|
||||||
|
|
||||||
|
app_state.mark_dirty()
|
||||||
|
wiresheet.render_current_sheet()
|
||||||
|
|
||||||
|
|
||||||
def execute_cascading_deletion(comp_element):
|
def execute_cascading_deletion(comp_element):
|
||||||
"""Shared application logic to wipe an element, its children, and related links from DOM and runtime lists."""
|
"""Shared application logic to wipe an element, its children, and related links from DOM and runtime lists."""
|
||||||
comp_name = comp_element.get("name", "Unknown")
|
comp_name = comp_element.get("name", "Unknown")
|
||||||
|
|||||||
@@ -161,6 +161,17 @@ def load_schema_kits_and_manifests():
|
|||||||
compile_schema_registry(manifest_paths)
|
compile_schema_registry(manifest_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_slots_for_type(type_str):
|
||||||
|
"""Returns every slot for a type as (name, type_string) tuples, for the link dialog."""
|
||||||
|
if type_str not in app_state.final_components_registry:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
(slot["name"], slot["type"])
|
||||||
|
for slot in app_state.final_components_registry[type_str]
|
||||||
|
if slot["name"]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_slots_for_type(type_str, linked_slots=()):
|
def get_slots_for_type(type_str, linked_slots=()):
|
||||||
"""
|
"""
|
||||||
Returns the slots worth painting for a given type, prefixed with double-digit
|
Returns the slots worth painting for a given type, prefixed with double-digit
|
||||||
|
|||||||
@@ -124,6 +124,41 @@ class AddComponentCommand:
|
|||||||
self.parent.insert(self.index, self.element)
|
self.parent.insert(self.index, self.element)
|
||||||
|
|
||||||
|
|
||||||
|
class AddLinkCommand:
|
||||||
|
"""Adding a single link to the <links> section."""
|
||||||
|
|
||||||
|
def __init__(self, links_parent, link_element, label="add link"):
|
||||||
|
self.links_parent = links_parent
|
||||||
|
self.link_element = link_element
|
||||||
|
self.label = label
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.link_element in list(self.links_parent):
|
||||||
|
self.links_parent.remove(self.link_element)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.link_element not in list(self.links_parent):
|
||||||
|
self.links_parent.append(self.link_element)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteLinkCommand:
|
||||||
|
"""Removing a single link from the <links> section."""
|
||||||
|
|
||||||
|
def __init__(self, links_parent, index, link_element, label="delete link"):
|
||||||
|
self.links_parent = links_parent
|
||||||
|
self.index = index
|
||||||
|
self.link_element = link_element
|
||||||
|
self.label = label
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.link_element not in list(self.links_parent):
|
||||||
|
self.links_parent.insert(self.index, self.link_element)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.link_element in list(self.links_parent):
|
||||||
|
self.links_parent.remove(self.link_element)
|
||||||
|
|
||||||
|
|
||||||
class DeleteComponentCommand:
|
class DeleteComponentCommand:
|
||||||
"""A cascading delete: the component subtree plus every link that referenced it."""
|
"""A cascading delete: the component subtree plus every link that referenced it."""
|
||||||
|
|
||||||
|
|||||||
+33
-3
@@ -180,10 +180,16 @@ def draw_knob(x, y):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
LINK_SELECT_COLOR = "#e00000"
|
||||||
|
LINK_SELECT_WIDTH = 2
|
||||||
|
|
||||||
|
|
||||||
def draw_links():
|
def draw_links():
|
||||||
"""Paints wires between slots on this sheet and knobs for links leaving it."""
|
"""Paints wires between slots on this sheet and knobs for links leaving it."""
|
||||||
canvas = app_state.canvas
|
canvas = app_state.canvas
|
||||||
canvas.delete("link")
|
canvas.delete("link")
|
||||||
|
app_state.canvas_link_map.clear()
|
||||||
|
app_state.selected_link_tag = None
|
||||||
if app_state.xml_root_element is None:
|
if app_state.xml_root_element is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -200,6 +206,7 @@ def draw_links():
|
|||||||
|
|
||||||
boxes = [b for b in (get_box_bounds(t) for t in path_to_tag.values()) if b]
|
boxes = [b for b in (get_box_bounds(t) for t in path_to_tag.values()) if b]
|
||||||
|
|
||||||
|
link_index = 0
|
||||||
for link in links_elem.findall("link"):
|
for link in links_elem.findall("link"):
|
||||||
from_ref = link.get("from", "")
|
from_ref = link.get("from", "")
|
||||||
to_ref = link.get("to", "")
|
to_ref = link.get("to", "")
|
||||||
@@ -218,19 +225,21 @@ def draw_links():
|
|||||||
if src_box is None or dst_box is None:
|
if src_box is None or dst_box is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
wire_tag = f"link_wire_{link_index}"
|
||||||
|
link_index += 1
|
||||||
|
app_state.canvas_link_map[wire_tag] = link
|
||||||
|
|
||||||
points = route_link_path(src_box, src_y, dst_box, dst_y, boxes)
|
points = route_link_path(src_box, src_y, dst_box, dst_y, boxes)
|
||||||
flat = [coord for point in points for coord in point]
|
flat = [coord for point in points for coord in point]
|
||||||
canvas.create_line(
|
canvas.create_line(
|
||||||
*flat, fill=LINK_COLOR, width=1,
|
*flat, fill=LINK_COLOR, width=1,
|
||||||
arrow=tk.LAST, arrowshape=(8, 9, 3), tags=("link",)
|
arrow=tk.LAST, arrowshape=(8, 9, 3), tags=("link", wire_tag)
|
||||||
)
|
)
|
||||||
elif src_tag:
|
elif src_tag:
|
||||||
# Source is here, target is on another sheet.
|
|
||||||
src_box, src_y = resolve_slot_anchor(src_tag, from_slot)
|
src_box, src_y = resolve_slot_anchor(src_tag, from_slot)
|
||||||
if src_box is not None:
|
if src_box is not None:
|
||||||
draw_knob(src_box[2] + KNOB_RADIUS, src_y)
|
draw_knob(src_box[2] + KNOB_RADIUS, src_y)
|
||||||
elif dst_tag:
|
elif dst_tag:
|
||||||
# Target is here, source is on another sheet.
|
|
||||||
dst_box, dst_y = resolve_slot_anchor(dst_tag, to_slot)
|
dst_box, dst_y = resolve_slot_anchor(dst_tag, to_slot)
|
||||||
if dst_box is not None:
|
if dst_box is not None:
|
||||||
draw_knob(dst_box[0] - KNOB_RADIUS, dst_y)
|
draw_knob(dst_box[0] - KNOB_RADIUS, dst_y)
|
||||||
@@ -240,6 +249,27 @@ def draw_links():
|
|||||||
canvas.tag_lower("link", "component")
|
canvas.tag_lower("link", "component")
|
||||||
|
|
||||||
|
|
||||||
|
def select_link(wire_tag):
|
||||||
|
"""Highlights a link wire in red."""
|
||||||
|
canvas = app_state.canvas
|
||||||
|
deselect_link()
|
||||||
|
if wire_tag and wire_tag in app_state.canvas_link_map:
|
||||||
|
canvas.itemconfig(wire_tag, fill=LINK_SELECT_COLOR, width=LINK_SELECT_WIDTH)
|
||||||
|
canvas.tag_raise(wire_tag)
|
||||||
|
app_state.selected_link_tag = wire_tag
|
||||||
|
|
||||||
|
|
||||||
|
def deselect_link():
|
||||||
|
"""Resets the selected link wire back to its default colour."""
|
||||||
|
canvas = app_state.canvas
|
||||||
|
if app_state.selected_link_tag:
|
||||||
|
try:
|
||||||
|
canvas.itemconfig(app_state.selected_link_tag, fill=LINK_COLOR, width=1)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
app_state.selected_link_tag = None
|
||||||
|
|
||||||
|
|
||||||
# --- Component painting ---
|
# --- Component painting ---
|
||||||
|
|
||||||
def render_components(components_to_render):
|
def render_components(components_to_render):
|
||||||
|
|||||||
Reference in New Issue
Block a user