Link View: every link of a component, drawn around it

Right-clicking a component now offers Link View alongside Properties, from
the wiresheet and from the navigation tree alike. It takes over the editor
pane the same way and answers one question -- what is wired to this
component, and where does it go.

The chosen component sits in the middle wearing the stroke a selected box
wears on the sheet, with what links into it on the left and what it links
out to on the right, so every arrow points the way the signal travels.
Pale yellow ground and no grid: nothing here can be positioned, because
the layout is derived from the links every time it is drawn and no part of
it is stored in the file. Read-only throughout -- no drag, no add, no
delete -- so a component's wiring can be read without the risk of
rearranging it.

Every line is grey, since this is not a wiresheet and a wire-blue line
would invite the reader to treat it as one. A link on the same sheet is
solid; one that crosses is dashed, keeps the grey knob that sheet paints
on the slot, and has the far component drawn in with its sheet named
beneath it. A reference the document cannot resolve gets a placeholder
naming the path, because a link the view silently dropped would be the one
thing a reader could not find out from here.

Clicking a wire or a component follows the link: it opens the sheet of the
end that is not the component the view is about, with the link selected in
red. A link that stays on one sheet therefore goes back to that sheet
where both ends are drawn, and one that crosses opens the other sheet --
the end the view could only show as a knob.

Supporting changes:

- draw_component_box is factored out of render_components, so the sheet
  and this view paint a component with the same code and a box cannot read
  differently between them. draw_knob takes its canvas for the same
  reason.
- The editor pane's hand-over is now shared: _take_editor_pane,
  _give_editor_pane_back and close_editor_panel replace the
  Properties-only versions, so "you have not saved that" is still asked
  once, in one place, whichever panel has the pane.
- Fixes the jump to a link's other sheet, which arrived with the link not
  selected. An after_idle callback is not late enough -- ttk dispatches
  its own selection event later than one of those, so the repaint wiped
  the selection. The link is left in _pending_link_selection for
  on_tree_select to honour once it has painted.
- Each gap is sized to hold a lane per wire crossing it, and lanes are
  handed out nearest-target-first, so a fan of a dozen links nests instead
  of crowding a dozen verticals into a 30px band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 18:54:44 +03:00
co-authored by Claude Opus 5
parent eb5ad92015
commit 56749872b4
5 changed files with 1095 additions and 165 deletions
+94 -26
View File
@@ -50,13 +50,15 @@ not inside the window. Tk always hands it to the system menu bar.
| `undo.py` | Undo/redo stack of reversible DOM commands | no |
| `wiresheet.py` | Grid, component boxes, link routing and painting | yes |
| `properties.py` | The Properties view: every slot of one component, read and written | yes |
| `link_view.py` | The Link View: every link touching one component, drawn around it | yes |
| `sax_file.py` | Open/save, navigation tree, cascading delete | yes |
| `main.py` | Widgets, menus, event handlers, dialogs, main loop | yes |
| `run.py` | Entry point | yes |
Imports form a DAG — `sax_dom`/`schema` depend on nothing UI-shaped, `wiresheet` and
`sax_file` build on them, `properties` builds on `schema` and `undo`, and `main` wires it
together. Event handlers live in `main`, which is what keeps the graph acyclic.
`sax_file` build on them, `properties` builds on `schema` and `undo`, `link_view` builds
on `wiresheet`, and `main` wires it together. Event handlers live in `main`, which is what
keeps the graph acyclic.
### Shared state
@@ -133,23 +135,36 @@ walk cannot rewrite the trail it is following.
### The component popup
Right-clicking a component offers the same actions from the wiresheet and from the
navigation tree -- Properties, Delete Component, Link Mark, and Link From / Link To naming
the marked component once one is marked. `fill_component_popup` builds both, and its entries close
over the component rather than reading a selection back, which is what lets each popup act
on the row or box that was actually clicked. The tree's root row keeps its own popup.
navigation tree -- Properties, Link View, Delete Component, Link Mark, and Link From /
Link To naming the marked component once one is marked. `fill_component_popup` builds
both, and its entries close over the component rather than reading a selection back,
which is what lets each popup act on the row or box that was actually clicked. The tree's
root row keeps its own popup.
### The editor pane
The pane to the right of the tree holds one of three things: the **wiresheet**, the
**Properties view** or the **Link View**. The latter two take the pane over rather than
floating above it, so a sheet and a table of one component's slots are never both half in
view. The canvas is unpacked, never destroyed, so it keeps its items and its scroll
position and **Close** puts it back exactly as it was. It is still painted while it is
hidden, which costs nothing and means there is no catching up to do when it returns.
`_take_editor_pane` and `_give_editor_pane_back` in `main` are the hand-over, and
`close_editor_panel` is the single way out of whichever panel has the pane: "you have not
saved that" is therefore asked once, in one place, whether the user pressed Close, picked
another sheet, walked the history or opened a different component. Only the Properties
view has anything to ask about.
Choosing a node in the tree means "show me that sheet", so it stands a panel down rather
than leaving the selection looking like it did nothing. So does deleting the component one
is showing, from anywhere -- every delete in the UI goes through `delete_component`,
because `sax_file` does its own view refresh rather than going through `refresh_all_views`.
### The Properties view
**Properties**, from either component popup, **takes over the editor pane** -- the sheet
and the slot table are the same piece of screen, so only one of them is ever the thing
being looked at. The canvas is unpacked, never destroyed, so it keeps its items and its
scroll position and **Close** puts it back exactly as it was. It is still painted while it
is hidden, which costs nothing and means there is no catching up to do when it returns.
Choosing a node in the tree means "show me that sheet", so it stands the view down rather
than leaving the selection looking like it did nothing. So does deleting the component it
is showing, from anywhere -- every delete in the UI goes through `delete_component`,
because `sax_file` does its own view refresh rather than going through `refresh_all_views`.
**Properties**, from either component popup, is the one place a slot's value is meant to
be changed, and the only view here that writes to the document.
It lists every slot the manifest declares -- bar one kind, see **Hidden slots** below --
in manifest order, each row numbered with the same slot id the wiresheet box uses. The
@@ -348,6 +363,46 @@ an `Entry`, a `TCombobox`, a `Text`. A backspace aimed at a text field must neve
component. Save still checks the component is in the document before writing, since an
undo can take it out from under the view.
### The Link View
**Link View**, from either component popup, answers one question -- what is wired to this
component, and where does it go -- and takes over the editor pane to do it. It is
read-only: no drag, no add, no delete, nothing that writes to the document, so a
component's wiring can be read without the risk of rearranging it.
The chosen component is drawn in the middle, with the components linking **into** it on
the left and the ones it links **out to** on the right, so every arrow points the way the
signal travels and the picture reads left to right the way a wiresheet does. The chosen
box wears the same dark stroke a selected box wears on the sheet. The ground is **pale
yellow** and carries **no grid**: nothing here can be positioned, because the layout is
derived from the links every time it is drawn and no part of it is stored in the file
(`meta` records a component's place on its own sheet, which says nothing about where it
belongs relative to another one).
Every line is **grey**, because this is not a wiresheet and a wire-blue line would invite
the reader to treat it as one:
| Link | Drawn as |
|---|---|
| link on the same wiresheet | solid grey line |
| link to another wiresheet | dashed grey line, plus the grey knob that sheet paints on the slot, with the far component drawn in and its sheet named beneath it |
Boxes come from `wiresheet.draw_component_box` -- the same function the sheet paints
with -- so a component reads exactly as it does on its own sheet and a wire lands on the
same slot row. A component wired twice on one side is one box with two wires, as it would
be on a sheet; a reference the document cannot resolve gets a dashed placeholder naming
the path, because a link the view silently dropped would be the one thing a reader could
not find out from here.
Each gap is sized by `column_gap` to hold a lane for every wire crossing it, and lanes are
handed out nearest-target-first, so a wire reaching a distant box turns outside one
reaching a near box instead of across it. A dozen links widen the gap rather than
crowding a dozen verticals into a 30px band.
Hovering a wire darkens it and names the link in the footer; clicking a wire or a
component follows it. `collect_links` and `sheet_display_name` are pure DOM work, so what
the view reads out of a document can be exercised without a window.
### Following a link to its other sheet
Right-clicking a wire or a knob offers **Go to Link Wire Sheet "<name>"** alongside Delete
@@ -355,21 +410,32 @@ Link, naming the sheet the far end of that link is on -- the end a grey knob sta
for. The entry is left off a link with both ends on the open sheet, which has nowhere to
go. `find_component_by_path` in `sax_dom` 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.
component's parent; a parent of `<app>` is the file's root node, which
`link_view.sheet_display_name` names after the file.
The jump selects the same link again once it arrives. It has to be found by its element
rather than by the tag it was picked on, because a repaint hands out fresh wire tags, and
it has to wait for that repaint: ttk queues its selection event rather than dispatching
it, so the sheet is not painted at the point the tree node is selected -- and painting a
sheet clears whatever link was selected. The selection is therefore made from an
`after_idle` callback, and the link is scrolled to the middle of the view. Arriving is an
ordinary sheet opening, so **Back** returns to the sheet the link was picked on.
Clicking a link in the Link View follows it the same way, through the same
`go_to_sheet_with_link`: it opens the sheet of the end that is **not** the component the
view is about, so a link that stays on one sheet goes back to that sheet where both ends
are drawn, and one that crosses opens the other sheet -- the end the view could only show
as a knob.
Either way the link is selected again once the sheet arrives. It has to be found by its
element rather than by the tag it was picked on, because a repaint hands out fresh wire
tags -- and it has to be picked out **after** that repaint, since painting a sheet clears
whatever link was selected. An `after_idle` callback is not late enough: ttk dispatches
its own selection event later than one of those, so a link selected from an idle callback
was wiped by the repaint that followed it. The link is therefore left in
`_pending_link_selection` for `on_tree_select` to honour once it has painted, and that
handler reads and clears the request first thing -- so a selection that turns out not to
paint a sheet drops it rather than leaving it to be applied to some later one. The link is
then scrolled to the middle of the view. Arriving is an ordinary sheet opening, so **Back**
returns to the sheet the link was followed from.
### Testing without a window
`app_state`, `sax_dom`, `schema`, `undo` and `wiresheet` import with no display, so the
rules can be exercised directly:
rules can be exercised directly. `link_view.collect_links` is pure DOM work too, and reads
a component's links without one:
```python
import xml.etree.ElementTree as ET
@@ -500,3 +566,5 @@ And two things worth knowing about the manifests themselves rather than the code
which is `x1 + radius` rather than `x1`, so snapping is offset by the corner radius.
- Slot values are edited from the Properties view only; a wiresheet box row is still
display-only.
- The Link View stacks each side in one column, so a component with a dozen peers makes a
picture taller than the pane. It scrolls, but it does not wrap into a second column.