Link popup: go to the sheet the other end of the link is on
Right-clicking a wire or a knob now offers Go to Link Wire Sheet "<name>", naming the sheet the far end is on -- the end a grey knob stands in for. A link with both ends on the open sheet has nowhere to go, so the entry is left off rather than greyed out. find_component_by_path turns a link's from/to reference back into the component it points at, the inverse of get_component_path, and the sheet is that component's parent; a parent of <app> is the file's root node, which the popup names after the file. Arriving selects the same link and scrolls it to the middle of the view. It is found by its element rather than by the tag it was picked on, since a repaint hands out fresh wire tags, and it waits for that repaint from an after_idle callback: ttk queues its selection event rather than dispatching it, so the sheet is not painted when the tree node is selected -- and painting a sheet clears the selected link, which was wiping the selection this feature had just made. The jump is an ordinary sheet opening, so Back returns to where the link was picked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,8 @@ import sax_file
|
||||
import schema
|
||||
import undo
|
||||
import wiresheet
|
||||
from sax_dom import get_component_path, read_meta_property, update_meta_property
|
||||
from sax_dom import (find_component_by_path, find_parent_element, get_component_path,
|
||||
read_meta_property, update_meta_property)
|
||||
|
||||
# Tree column measurements, in pixels. The padding covers the expander arrow and the
|
||||
# cell's own inset, which sit ahead of the text ttk lets us measure.
|
||||
@@ -262,6 +263,121 @@ def delete_selected_link():
|
||||
sax_file.remove_link(link_elem)
|
||||
|
||||
|
||||
def link_endpoint_components(link_elem):
|
||||
"""The two components a link joins, resolved from its from/to references."""
|
||||
root = app_state.xml_root_element
|
||||
ends = []
|
||||
for attribute in ("from", "to"):
|
||||
reference = link_elem.get(attribute, "")
|
||||
path = reference.rsplit(".", 1)[0] if "." in reference else ""
|
||||
ends.append(find_component_by_path(root, path))
|
||||
return ends
|
||||
|
||||
|
||||
def link_target_sheet(link_elem):
|
||||
"""Where a link leads: (sheet element, the component at that end), or None.
|
||||
|
||||
The far end is the one that is not on the open sheet, which is exactly the end a
|
||||
knob stands in for. A link with both ends on this sheet has nowhere to go, and
|
||||
neither has one whose references name a component this document does not have.
|
||||
"""
|
||||
root = app_state.xml_root_element
|
||||
if root is None or link_elem is None:
|
||||
return None
|
||||
|
||||
here = sax_file.current_sheet_container()
|
||||
for component in link_endpoint_components(link_elem):
|
||||
if component is None:
|
||||
continue
|
||||
sheet = find_parent_element(root, component)
|
||||
if sheet is not None and sheet is not here:
|
||||
return sheet, component
|
||||
return None
|
||||
|
||||
|
||||
def tree_item_for_sheet(sheet_element):
|
||||
"""The navigation tree item that opens this sheet, or None if it has no node.
|
||||
|
||||
The app element is the file's root node, which the tree holds without an entry in
|
||||
the element map, and which the history records as None for the same reason.
|
||||
"""
|
||||
root = app_state.xml_root_element
|
||||
app_element = root.find("app") if root is not None else None
|
||||
if sheet_element is app_element or sheet_element is root:
|
||||
return _tree_item_for_element(None)
|
||||
return _tree_item_for_element(sheet_element)
|
||||
|
||||
|
||||
def scroll_canvas_to(tag):
|
||||
"""Scrolls the sheet so the tagged items sit in the middle of the view."""
|
||||
canvas = app_state.canvas
|
||||
box = canvas.bbox(tag)
|
||||
region = str(canvas.cget("scrollregion")).split()
|
||||
if box is None or len(region) != 4:
|
||||
return
|
||||
|
||||
span_x = float(region[2]) or 1.0
|
||||
span_y = float(region[3]) or 1.0
|
||||
middle_x = (box[0] + box[2]) / 2 - canvas.winfo_width() / 2
|
||||
middle_y = (box[1] + box[3]) / 2 - canvas.winfo_height() / 2
|
||||
canvas.xview_moveto(max(0.0, middle_x / span_x))
|
||||
canvas.yview_moveto(max(0.0, middle_y / span_y))
|
||||
|
||||
|
||||
def select_link_element(link_elem):
|
||||
"""Selects whichever wire or knob now stands for this link, and scrolls it into view.
|
||||
|
||||
A repaint hands out fresh wire tags, so the link is found again by its element
|
||||
rather than by the tag it had on the sheet it was picked on.
|
||||
"""
|
||||
for wire_tag, painted in app_state.canvas_link_map.items():
|
||||
if painted is link_elem:
|
||||
wiresheet.select_link(wire_tag)
|
||||
scroll_canvas_to(wire_tag)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def go_to_link_sheet():
|
||||
"""Link popup: open the sheet the far end of this link is on, with the link selected."""
|
||||
link_elem = app_state.canvas_link_map.get(app_state.selected_link_tag)
|
||||
destination = link_target_sheet(link_elem) if link_elem is not None else None
|
||||
if destination is None:
|
||||
app_state.set_status("This link has no other sheet to go to.", warn=True)
|
||||
return
|
||||
|
||||
sheet_element, _ = destination
|
||||
item_id = tree_item_for_sheet(sheet_element)
|
||||
if item_id is None:
|
||||
app_state.set_status(
|
||||
f"'{sheet_element.get('name', 'That sheet')}' is not in the navigation tree.",
|
||||
warn=True,
|
||||
)
|
||||
return
|
||||
|
||||
tree = app_state.tree
|
||||
# Selecting the node is what paints the sheet, and what records the visit so Back
|
||||
# returns to the sheet the link was picked on.
|
||||
tree.selection_set(item_id)
|
||||
tree.focus(item_id)
|
||||
tree.see(item_id)
|
||||
|
||||
# ttk queues its selection event rather than dispatching it here, so the sheet is
|
||||
# not painted yet -- and painting a sheet clears whatever link was selected. Picking
|
||||
# the link out is therefore left until that has happened.
|
||||
tree.after_idle(select_link_on_open_sheet, link_elem)
|
||||
|
||||
|
||||
def select_link_on_open_sheet(link_elem):
|
||||
"""Selects a link on the sheet that has just been painted, and says which it is."""
|
||||
if select_link_element(link_elem):
|
||||
app_state.set_status(
|
||||
f"Link {link_elem.get('from', '?')} → {link_elem.get('to', '?')}"
|
||||
)
|
||||
else:
|
||||
app_state.set_status("That link is not painted on this sheet.", warn=True)
|
||||
|
||||
|
||||
def delete_selected_component(event=None):
|
||||
"""Deletes the highlighted canvas component (or selected link) and all its descendants recursively."""
|
||||
if app_state.selected_link_tag:
|
||||
@@ -584,6 +700,7 @@ def on_canvas_right_click(event):
|
||||
|
||||
if wire_tag:
|
||||
wiresheet.select_link(wire_tag)
|
||||
_rebuild_link_context_menu()
|
||||
link_context_menu.post(event.x_root, event.y_root)
|
||||
return
|
||||
|
||||
@@ -1236,7 +1353,32 @@ window.minsize(1, MIN_WINDOW_HEIGHT)
|
||||
# --- Context Popups Structures ---
|
||||
canvas_context_menu = tk.Menu(window, tearoff=0)
|
||||
link_context_menu = tk.Menu(window, tearoff=0)
|
||||
link_context_menu.add_command(label="Delete Link", command=delete_selected_link)
|
||||
|
||||
|
||||
def _rebuild_link_context_menu():
|
||||
"""Rebuilds the link popup, naming the sheet the far end of the link is on.
|
||||
|
||||
A link with both ends on the open sheet is already fully in view, so the jump is
|
||||
left off rather than greyed out.
|
||||
"""
|
||||
link_context_menu.delete(0, tk.END)
|
||||
link_context_menu.add_command(label="Delete Link", command=delete_selected_link)
|
||||
|
||||
link_elem = app_state.canvas_link_map.get(app_state.selected_link_tag)
|
||||
destination = link_target_sheet(link_elem) if link_elem is not None else None
|
||||
if destination is None:
|
||||
return
|
||||
|
||||
sheet_element = destination[0]
|
||||
sheet_name = sheet_element.get("name")
|
||||
if not sheet_name:
|
||||
# The app element, which the tree shows under the file's own name.
|
||||
sheet_name = os.path.basename(app_state.current_file_path or "app")
|
||||
|
||||
link_context_menu.add_separator()
|
||||
link_context_menu.add_command(
|
||||
label=f'Go to Link Wire Sheet "{sheet_name}"', command=go_to_link_sheet
|
||||
)
|
||||
|
||||
|
||||
def _rebuild_canvas_context_menu():
|
||||
|
||||
Reference in New Issue
Block a user