@hidden is now filtered by default. niagara.html says hidden slots "are
designed to be invisible to the user" and that interfaces should rarely
display them; every one under Sedona home is a void action a kit uses
internally, such as HdwAI.checkType. A "Hidden slots (n)" checkbox in the
footer brings them back and the answer sticks for the session. The checkbox
only appears on a type that has some, and the rows are always built -- the
toggle grid_removes and re-grids them, so an emptied grid row collapses rather
than leaving a gap.
A bool now says which state it is in by the colour of its field rather than by
a dot beside it that had to be learnt: green true, red false, blue null, with
the words left black so they read as easily as every other value. Dropping the
dot also lets a bool's field line up with the numbers above and below it.
Two things that needed changing to get there:
- a dropdown is offered only where there is a choice to make. A read-only
bool or enum gets a plain field, since a disabled dropdown is an arrow
that does nothing -- and a read-only bool is still filled, because a
runtime bool has a state too.
- the dropdown is a tk.OptionMenu, not a ttk.Combobox. On the Windows themes
a Combobox draws its field with a native element and ignores
fieldbackground outright, so a bool could never be filled with its colour.
An OptionMenu reports through its variable rather than an event, which means
loading a row looks exactly like choosing one -- so the load is flagged and the
unsaved-state tint left alone until every row is in, or the view would open
already claiming edits nobody made.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
495 lines
27 KiB
Markdown
495 lines
27 KiB
Markdown
# Sedona SAX Tree View Viewer & Program Editor
|
|
|
|
A Tkinter desktop editor for Sedona Framework application files (`.sax`). It shows the
|
|
component hierarchy as a navigation tree, paints the selected folder's children as a
|
|
wiresheet of draggable component boxes, draws the links between them, and writes box
|
|
positions back into each component's `meta` property on save.
|
|
|
|
Current version: **0.0.0.051** (see [Versioning](#versioning)).
|
|
|
|
## Requirements
|
|
|
|
Python **3.13 from Homebrew**, which brings Tk 9:
|
|
|
|
```bash
|
|
brew install python-tk@3.13
|
|
```
|
|
|
|
Do **not** run this with `/usr/bin/python3`. Apple's system Python ships Tk 8.5.9, a
|
|
2010 build that is broken on current macOS: windows open at the right size with the
|
|
right titles, but no widget contents ever paint. A blank white panel is the symptom,
|
|
and it looks exactly like a layout bug in this code. It isn't.
|
|
|
|
Verify which Tk an interpreter has:
|
|
|
|
```bash
|
|
/opt/homebrew/bin/python3.13 -c "import tkinter; r=tkinter.Tk(); print(r.tk.call('info','patchlevel'))"
|
|
```
|
|
|
|
## Running
|
|
|
|
```bash
|
|
cd sedona_editor
|
|
/opt/homebrew/bin/python3.13 run.py
|
|
```
|
|
|
|
On first launch, set **Sedona home folder** in the Preferences menu — the directory
|
|
containing `manifests/` and `kits/`. Nothing will load until it is set. Preferences are
|
|
stored in `editor.properties`, read from the current working directory.
|
|
|
|
On macOS the menu bar (File / App / Preferences) appears at the **top of the screen**,
|
|
not inside the window. Tk always hands it to the system menu bar.
|
|
|
|
## Module layout
|
|
|
|
| Module | Responsibility | Needs a display |
|
|
|---|---|---|
|
|
| `app_state.py` | Preferences, runtime state, widget handles, `mark_dirty`/`mark_clean` | no |
|
|
| `sax_dom.py` | `meta` bit-packing codec, component path resolution | no |
|
|
| `schema.py` | Kit manifest loading, slot inheritance, slot visibility rules | no |
|
|
| `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 |
|
|
| `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.
|
|
|
|
### Shared state
|
|
|
|
Modules reach shared state as **`app_state.<name>`**, always qualified. A bare
|
|
`from app_state import is_dirty` would bind a copy, so a later rebinding would be
|
|
invisible to every other module. Every mutation of "the document changed" goes through
|
|
`app_state.mark_dirty()`, which also applies the unsaved-state pink canvas tint.
|
|
|
|
### Undo and keyboard shortcuts
|
|
|
|
`undo.py` holds a stack of commands that own their DOM mutation in both directions:
|
|
`MetaMoveCommand` (a box drag, or a whole Tidy Layout as one step), `AddComponentCommand`
|
|
(which also removes the `<kit>` entry placing it may have added) and
|
|
`DeleteComponentCommand` (which restores the removed `<link>` entries at their original
|
|
indices, reinserting in ascending order so the indices stay valid) and `SlotValueCommand`
|
|
(one Save from the Properties view, however many slots it changed). Nothing in
|
|
`undo.py` touches Tk; `main` registers a refresh callback at startup, which is what keeps
|
|
`sax_file` and `wiresheet` free to push commands without an import cycle.
|
|
|
|
Undoing back to the depth of the last save clears the dirty tint rather than leaving it lit.
|
|
|
|
| Shortcut | Action |
|
|
|---|---|
|
|
| `Ctrl`/`Cmd` + `S` | Save (direct overwrite) |
|
|
| `Ctrl`/`Cmd` + `Z` | Undo |
|
|
| `Ctrl`/`Cmd` + `Y` or `Ctrl`/`Cmd` + `Shift` + `Z` | Redo |
|
|
| `Del` / `Backspace` | Delete selected component |
|
|
| `Ctrl`/`Cmd` + `B` | Show or hide the tree/palette column |
|
|
| `Ctrl` + `W` | Tidy Layout: re-flow the open sheet |
|
|
| `Alt` + `Left` / `Alt` + `Right` | Back and forward through the sheets opened this session |
|
|
| `Ctrl`/`Cmd` + `+` / `-` | Zoom the sheet in and out |
|
|
| `Ctrl`/`Cmd` + `0` | Zoom back to 100% |
|
|
| `Ctrl`/`Cmd` + `9` | Zoom to Fit: show the whole sheet |
|
|
| `Ctrl` + wheel | Zoom around the pointer |
|
|
|
|
Both `Command-` and `Control-` are bound for each, so the same build works on macOS and
|
|
Windows; menu accelerators render as `Cmd` or `Ctrl` per platform. Tidy Layout is the
|
|
exception, on `Control-` alone: `Cmd` + `W` closes the window on macOS.
|
|
|
|
### The View menu
|
|
|
|
**Tree / Palette** hides the left column so the wiresheet has the whole window, and puts
|
|
it back at its configured width. ttk has no hide flag for a pane, so the column is dropped
|
|
from the paned window and re-inserted at the front.
|
|
|
|
**Zoom In**, **Zoom Out** and **Zoom 100%** step a ladder of scales from 25% to 300%,
|
|
with the zoom buttons, the percentage and the fit icon sitting beside the navigation
|
|
arrows in the toolbar. **Zoom to Fit** is the one thing that lands between the ladder's
|
|
rungs -- a sheet fits at whatever scale it fits at -- and stepping in or out from there
|
|
rejoins the ladder. It measures the sheet from item coordinates rather than
|
|
`canvas.bbox()`, which is inflated by the outline stroke: a stroke is the same two pixels
|
|
at every zoom, so a bbox divided back to 1:1 reports a slightly smaller sheet the further
|
|
in it is zoomed, and fitting an already fitted sheet would creep instead of standing
|
|
still. Anything painted left of or above the canvas origin is measured from the origin
|
|
instead, since no amount of scrolling can bring it into view. The sheet
|
|
is painted and routed at 1:1 -- box metrics, wire stubs, lane gaps, clearances and knob
|
|
radii are all tuned in those pixels -- and `apply_sheet_zoom` scales the finished picture,
|
|
so a zoomed sheet keeps exactly the proportions of a 1:1 one and the router makes the same
|
|
decisions at every scale. Text scales because the three box fonts are resized after the
|
|
boxes were measured with them. `redraw_links()` is what a drag or a drop calls: it puts
|
|
the boxes back to 1:1 for the routing pass and scales the result again, since the router
|
|
reads box positions straight off the canvas. Canvas coordinates therefore carry the zoom,
|
|
which is why the drag, release and drop handlers snap against `meta_scale * zoom`.
|
|
|
|
**Back** and **Forward** walk the sheets opened this session, mirrored by the two arrow
|
|
buttons in the toolbar. The history records the element whose children are painted -- the
|
|
same thing `app_state.current_sheet_parent` holds -- rather than a tree item id, because
|
|
rebuilding the tree hands out fresh ids while the elements themselves survive. Opening a
|
|
different file retires the history; a delete or an undo drops only the entries whose
|
|
element has left the document, keeping the position on screen. Walking the history drives
|
|
the tree selection, which is what paints the sheet, and is suppressed from recording so a
|
|
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.
|
|
|
|
### 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`.
|
|
|
|
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
|
|
box's visibility rules do not apply here: they are right for a diagram and wrong for the
|
|
one place a value is meant to be changed, so `HvacControl` shows all 47 rows rather than
|
|
the 8 its box paints.
|
|
|
|
The header names the component, its type and its **absolute path** -- the same path a
|
|
`<link>` writes, and the one thing on screen that separates this `Cascade` from the three
|
|
others in the file. It is read from the document rather than held, so a renamed parent
|
|
shows up on the next Refresh.
|
|
|
|
Each row carries the slot's declared type between its name and its value, written the way
|
|
a slot sheet writes it: the manifest stores the primitives bare and everything else already
|
|
qualified, so `int` is shown as `sys::int` and `sys::Buf` is left as it stands.
|
|
|
|
Which rows can be typed into is the slot's own business, not the view's:
|
|
|
|
| Slot | Field |
|
|
|---|---|
|
|
| config (`c` flag) and not `@readonly` | white, editable |
|
|
| everything else | grey, read-only |
|
|
|
|
A runtime slot holds a live value that no offline file carries, so nothing here could
|
|
usefully write one; a `@readonly` config slot is the manifest saying the same about a
|
|
value that *is* persisted. Both read the same way on screen.
|
|
|
|
A **dropdown is offered only where there is a choice to make**. A read-only bool or enum
|
|
gets a plain field instead, since a disabled dropdown is an arrow that does nothing -- and
|
|
a read-only bool is still filled by its state, because a runtime bool has a state too.
|
|
|
|
The dropdown is a `tk.OptionMenu` rather than a `ttk.Combobox`, for one reason: on the
|
|
Windows themes a Combobox draws its field with a native element and **ignores
|
|
`fieldbackground` outright**, so a bool could never be filled with its colour. The plain Tk
|
|
widget takes the colour it is given. It reports a change through its variable rather than
|
|
an event, which also catches a value set from anywhere else -- and means loading a row
|
|
looks exactly like choosing one, so the load is flagged and the tint left alone until every
|
|
row is in.
|
|
|
|
Each row is drawn from the slot's facets, so it reads the way the kit author meant it to:
|
|
|
|
- `@trueText` / `@falseText` turn a bool into its own two words -- `Disabled` / `Enabled`
|
|
rather than false / true -- and **the field is filled by the state**: green for true,
|
|
red for false, blue for null. It began as a coloured dot beside the field, which meant
|
|
learning what the dot stood for, then as coloured text; a block of colour carries down a
|
|
long list of slots where a letterform does not, and the words stay black so they read as
|
|
easily as every other value. Dropping the dot also let a bool's field line up with the
|
|
numbers above and below it.
|
|
- **Null is a value, not an absence.** Sedona models it as NaN for a float and a sentinel
|
|
for the integer types, and a config slot can genuinely hold it: `control::WriteFloat`
|
|
pairs an `in` property with a `setNull` action, and the sample app carries
|
|
`<prop name="in" val="null"/>` on four of them. So `null` shows against a blue dot,
|
|
types back in as `null`, and reads as `@nullText` where a kit declared one --
|
|
`ontrolBaseDriver::DeviceNetwork.status` calls it `Disabled`. `@allowNull false` is the
|
|
one case that refuses it, and takes it out of a bool's dropdown.
|
|
- `@range` turns an integer slot into a named dropdown; the file still stores the index.
|
|
- `@precision` fixes how many decimals a float shows, `@unit` puts the symbol after the
|
|
field (`celsius` reads `°C`).
|
|
- `@min` / `@max` bound what Save will accept, and are shown as `[0 - 1000]`. A row prints
|
|
a range when the manifest declared one, and otherwise only where the **type** supplies a
|
|
narrow one: `byte` reads `[0 - 255]` and `short` reads `[0 - 65535]` unfaceted, because
|
|
those bounds are worth knowing. An `int`, a `long` or a float prints nothing, since its
|
|
full span means no more than "a whole number" and crowds the row to say it -- and an end
|
|
those types leave undeclared prints as `-inf` / `+inf` rather than reaching for the
|
|
type. This is display only: Save still holds every integer to its type, so an `int` of
|
|
`9999999999` is rejected whether or not its row advertised the limit.
|
|
- On a **`Buf`** those same two facets count *characters*, not magnitude, and the count is
|
|
what the buffer can hold: `TcpDevice.ipAddr` is a `Buf(17)` declaring `@max 16`, the
|
|
seventeenth byte being the null terminator. The row reads `[max 16 chars]` and Save
|
|
refuses a longer string -- overrunning a Buf is not a display problem, the device gets a
|
|
string it has no room for.
|
|
|
|
The value shown is the file's `<prop>`, or the manifest default when the file carries
|
|
none, or the type's zero when the manifest declares none either. Runtime slots reach the
|
|
view through that second and third step, which is why they read `0.0` here and `null` on
|
|
the wiresheet: the box is reporting what the file says, and this is reporting what the
|
|
device would start from.
|
|
|
|
`meta` gets a row of its own rather than an editable integer: typing 822411269 into a
|
|
field is no way to set either of the two unrelated things it packs. The `»` beside it
|
|
opens them apart -- the four user group bits as checkboxes, and the position as x and y,
|
|
which a drag would otherwise be the only way to set. Both are in grid units, the figure
|
|
`meta_scale` multiplies to reach pixels, and each is one byte, so the editor holds them to
|
|
0-255. The row shows the groups it summarises and the position beside them.
|
|
|
|
Saving a changed meta writes the position first and the groups second, because each pass
|
|
preserves what the other owns: the position pass keeps the group bits it reads, and the
|
|
group pass keeps the position it was just handed.
|
|
|
|
The table is laid out at its natural width and never stretched. Every widget is gridded
|
|
straight into one frame, so grid's own column sizing is what lines the four columns up
|
|
across the rows, and no column carries a weight -- widening the view shows more white to
|
|
the right of the table instead of walking the fields across it, and narrowing it scrolls
|
|
sideways rather than squeezing them. Each scrollbar appears only while it has somewhere to
|
|
scroll; `Shift` + wheel scrolls sideways.
|
|
|
|
### The file name goes red
|
|
|
|
While anything is unwritten, the file name on the tree's **root row** turns
|
|
`DIRTY_NAME_COLOUR`. That row is the only place a file name appears on screen -- a Tk
|
|
window title cannot be coloured.
|
|
|
|
The tag's colour is configured **once**, when the tree is built, and never changed; what
|
|
moves is whether the row carries the tag. Reconfiguring a tag does not reliably repaint
|
|
rows that are already drawn, so a version that recoloured the tag went red only on the
|
|
paths that happened to rebuild the tree -- a Properties Save did, dragging a box did not,
|
|
and the two disagreed in a way that looked like the drag was not marking anything dirty at
|
|
all. Setting a row's tags repaints that row, so the colour cannot fall behind the state.
|
|
|
|
`anything_unwritten()` is the whole rule, and it counts **both**: the document being dirty,
|
|
and the Properties view holding edits its Save has not been pressed for. A typed value is
|
|
as unwritten as a dragged box, and a name that only went red for the second of those would
|
|
be telling half the story. So it goes red the moment a field is edited, before anything has
|
|
reached the document, and comes back when the edit is typed away, discarded, or saved and
|
|
written.
|
|
|
|
The navigation tree runs on a style of its own, `NAV_TREE_STYLE`, for one reason: a themed
|
|
Treeview maps the selected state to `SystemHighlightText`, and **that mapping beats a row
|
|
tag's own foreground**. The name turned white-on-blue the moment its row was selected --
|
|
which is exactly when the root sheet is the one being looked at, so the red was invisible
|
|
in the most ordinary case. The style drops the selected foreground entry altogether and
|
|
takes a light selection background instead, so a tag's colour shows in every state and
|
|
plain black still reads on the highlight. Scoped to this tree, so the palette keeps the
|
|
system selection.
|
|
|
|
`app_state.refresh_dirty_display()` is the single funnel, called from `mark_dirty` and
|
|
`mark_clean` through a callback `main` registers -- the same shape `undo` uses for its view
|
|
refresh, and what keeps `app_state` free of any idea that a Properties view exists.
|
|
|
|
### Hidden slots
|
|
|
|
`@hidden` is the one facet the view filters on, and only by default. niagara.html says
|
|
hidden slots "are designed to be invisible to the user" and that interfaces should rarely
|
|
display them; every one under Sedona home is a `void` action a kit uses internally, such
|
|
as `HdwAI.checkType`. A **Hidden slots (n)** checkbox in the footer brings them back, and
|
|
the answer sticks for the session rather than being asked again for every component.
|
|
|
|
The rows are always built -- the checkbox `grid_remove`s and re-`grid`s them, which keeps
|
|
each widget's options so putting a row back needs no arithmetic, and an emptied grid row
|
|
collapses rather than leaving a gap. The checkbox itself only appears on a type that has
|
|
something to reveal, so most components carry no control for a thing they do not have.
|
|
|
|
### Unsaved edits in the Properties view
|
|
|
|
An edited field paints the whole view in **`app_state.DIRTY_TINT`** -- the same colour
|
|
the canvas wears when the file is dirty, now one constant rather than two spellings of
|
|
`#ffe6e6`, so "there is something here you have not written yet" looks the same wherever
|
|
the change was made. The fields themselves keep their own white or grey: whether a value
|
|
*can* be typed into is a different thing from whether it *has* been.
|
|
|
|
Dirtiness is read from the widgets rather than tracked as a flag, so typing a value and
|
|
then typing it back leaves the view clean -- it is exactly the set of rows Save would
|
|
write. The tint follows the fields live, and comes off on Save and on Refresh.
|
|
|
|
Every way of leaving the view goes through `close_properties`, so the question is asked
|
|
once, in one place: **Close**, picking another sheet in the tree, walking **Back** or
|
|
**Forward**, or right-clicking a different component. The box has three answers rather
|
|
than two, because "I did not mean to leave" and "leave, I did not want those edits" are
|
|
different things and a two-way box makes the user guess which one Cancel is. A Save that
|
|
fails its range checks keeps the view open, since leaving then would discard exactly the
|
|
values being complained about.
|
|
|
|
Refusing to leave has to put the tree back on the sheet that is still showing. The
|
|
**element** is held for that, not the tree item id, for the same reason the history holds
|
|
elements: a rebuild of the tree hands out fresh ids. ttk queues its selection event rather
|
|
than dispatching it, so the flag that stops the restoring selection being read as a fresh
|
|
choice is cleared from an `after_idle` rather than inline.
|
|
|
|
Nothing asks when the question has nowhere to go -- the component deleted, a different
|
|
file opened. Those go straight to `dismiss_properties`, which is also what the panel's own
|
|
Close calls once the question has been put and answered.
|
|
|
|
Discarded edits are discarded: the panel is destroyed, and the next one is built from the
|
|
document, so it opens on the file's values with no tint.
|
|
|
|
**Save** writes the edited rows into the DOM and pushes them as **one** undoable step, so
|
|
a dozen changed slots undo together. Only rows whose text actually moved are written, so a
|
|
component keeps exactly the props the file gave it plus the ones edited here -- and if any
|
|
edited row fails its range check, *nothing* is written and the view names every offending
|
|
row at once. Save changes the document, not the file; `Ctrl`/`Cmd` + `S` still writes the
|
|
`.sax`. **Refresh** re-reads every row from the DOM, discarding pending edits, which is
|
|
also how the view catches up after an undo.
|
|
|
|
`Del` and `Backspace` are bound application-wide, so they also reach a field being typed
|
|
in. `delete_selected_component` therefore ignores the key while the Properties view is up,
|
|
while focus is in another window, and while it is in any widget that owns the keyboard --
|
|
an `Entry`, a `TCombobox`, a `Text`. A backspace aimed at a text field must never delete a
|
|
component. Save still checks the component is in the document before writing, since an
|
|
undo can take it out from under the view.
|
|
|
|
### Following a link to its other sheet
|
|
|
|
Right-clicking a wire or a knob offers **Go to Link Wire Sheet "<name>"** alongside Delete
|
|
Link, naming the sheet the far end of that link is on -- the end a grey knob stands in
|
|
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.
|
|
|
|
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.
|
|
|
|
### Testing without a window
|
|
|
|
`app_state`, `sax_dom`, `schema`, `undo` and `wiresheet` import with no display, so the
|
|
rules can be exercised directly:
|
|
|
|
```python
|
|
import xml.etree.ElementTree as ET
|
|
import app_state, schema, wiresheet
|
|
|
|
app_state.SEDONA_HOME = "../sedona"
|
|
app_state.xml_root_element = ET.parse("../DDC_8-ATP_PION_17.sax").getroot()
|
|
schema.load_schema_kits_and_manifests()
|
|
|
|
idx = wiresheet.build_linked_slot_index()
|
|
print(schema.get_slots_for_type("ontrolControl::HvacControl",
|
|
idx.get("/AHU20_1/TmpCont/HvacCon", ())))
|
|
```
|
|
|
|
## How it reads a Sedona app
|
|
|
|
### Manifests
|
|
|
|
Each `<kit name="..."/>` in the file's `<schema>` block is resolved to
|
|
`<sedona home>/manifests/<kit>/<kit>-<checksum>.xml`, falling back to the
|
|
highest-sorting file in that directory when the checksum is absent. Slots are
|
|
flattened along the `base` chain, so inherited slots keep their declaration order.
|
|
|
|
A kit with no manifest aborts **that load only**. `schema.load_schema_kits_and_manifests()`
|
|
raises `schema.MissingManifestError` naming every unresolved kit at once, the caller shows a
|
|
dialog asking the user to check the Sedona home folder, and `sax_file.reset_to_startup_state()`
|
|
returns the app to its just-launched state — no file, empty tree, empty sheet. The app stays
|
|
open, so the folder can be corrected in Preferences and the file opened again.
|
|
|
|
The same error raised while placing a palette component only abandons that placement: the
|
|
`<kit>` entry just added to `<schema>` is removed and the previous registry is rebuilt.
|
|
|
|
### Slot visibility
|
|
|
|
A component type can declare far more slots than are worth seeing; `HvacControl` has
|
|
47. Rows are filtered by these rules, in precedence order:
|
|
|
|
| Precedence | Condition | Result |
|
|
|---|---|---|
|
|
| 1 | Slot appears in `<links>` (either end) | **Show** — overrides everything below |
|
|
| 2 | `<bool name="summary" val="false"/>` facet | Hide |
|
|
| 3 | Runtime slot (no `c` in `flags`) | **Show** |
|
|
| 4 | Config slot (`c`), unlinked | Hide |
|
|
| 5 | `o` flag (operator) | Hide |
|
|
|
|
Rule 1 outranks rule 2 deliberately: a linked slot with `@summary false` still needs a
|
|
row for its wire to terminate on, otherwise the wire points at the box's centre.
|
|
|
|
Row numbers are the manifest slot ids, so hidden rows leave **gaps** in the numbering
|
|
(`01, 02, 03, 04, 05, 08, 11...`) rather than renumbering. The numbers stay meaningful
|
|
against the manifest.
|
|
|
|
Consequence worth knowing: `meta` is a config slot that nothing links to, so it is
|
|
hidden from every box. It holds the packed position, not process data.
|
|
|
|
### Why outputs read `null`
|
|
|
|
Config slots carry `flags="c"` and are persisted in the `.sax`. Runtime slots carry no
|
|
flag and are **never** persisted — they only exist on a live device. So `out` and `in`
|
|
have no `<prop>` in the file and the renderer substitutes the string `null`. That is
|
|
correct for an offline app dump, not a fault.
|
|
|
|
### The `meta` property
|
|
|
|
A packed 32-bit integer, decoded in `sax_dom.py`:
|
|
|
|
```
|
|
bits 31-24 x position, in grid units
|
|
bits 23-16 y position, in grid units
|
|
bits 15-8 reserved (preserved verbatim on write)
|
|
bits 7-0 user group bits 1-4 in the low nibble, reserved high nibble
|
|
```
|
|
|
|
Writes re-encode only x and y and preserve every other bit, so dragging a box never
|
|
disturbs the rest of the value.
|
|
|
|
### Links
|
|
|
|
`<links>` sits at the document root, with `from`/`to` references of the form
|
|
`/path/to/Component.slotName`.
|
|
|
|
- Both endpoints on the current sheet → an orthogonal wire is routed **around** the
|
|
boxes. Routing tries a clear vertical channel between the two boxes first, then a
|
|
lane above or below everything (which is what feedback links right-to-left need),
|
|
and only falls back to a direct dog-leg if nothing is clear.
|
|
- One endpoint off-sheet → a **knob** is drawn at the slot's edge, marking a connection
|
|
that leaves this sheet. In the sample app, 137 of 217 links are same-sheet, so knobs
|
|
are not a rare case.
|
|
|
|
Wires and knobs are painted behind the component boxes and re-routed on every drag
|
|
step, since they are not part of the dragged canvas group.
|
|
|
|
### Facets this does not act on
|
|
|
|
Read against `SedonaDoc/niagara.html`, which is what tells Niagara -- and so this view
|
|
-- how to model a slot. These are recognised there and deliberately not acted on here:
|
|
|
|
| Facet | Where it is used | Why not |
|
|
|---|---|---|
|
|
| `fieldWidth` | 3082 slots | A per-slot column count for the text field. Applying it would make the value column ragged; a table reads better with one width. |
|
|
| `radix`, `showSeparators`, `multiLine`, `fieldEditor` | unused by any kit under Sedona home | Nothing to render. |
|
|
|
|
And two things worth knowing about the manifests themselves rather than the code:
|
|
|
|
- 24 slots declare **`readOnly`** with a capital O. Niagara matches flag names exactly, so
|
|
it is not the `readonly` flag and does nothing there either -- it is stored as an
|
|
ordinary facet. Every one of them is a runtime slot, so they are already grey here for
|
|
the other reason, and honouring the typo would change nothing on screen.
|
|
- 118 **config `Buf`** slots omit `@asStr`, so by the letter of `components.html` they
|
|
serialise as base64 rather than text. Their names (`text`, `trueText`, `buttonText`) say
|
|
otherwise, so they are edited as text here.
|
|
|
|
## Versioning
|
|
|
|
`VERSION` lives in `app_state.py` and is shown in the window title.
|
|
|
|
- **Feature** → bump the version.
|
|
- **Bug fix** → no bump.
|
|
- **Refactor with no behaviour change** → no bump.
|
|
|
|
## Known gaps
|
|
|
|
- The yellow square in each box header is drawn but wired to nothing; it is the natural
|
|
place for a per-box collapse/expand toggle.
|
|
- Undo covers box moves, adds and deletes. Nothing else pushes a command, so a change made
|
|
outside those three is not reversible.
|
|
- `on_canvas_release` snaps using the rounded-rectangle polygon's first coordinate,
|
|
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.
|