174 lines
6.8 KiB
Markdown
174 lines
6.8 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.024** (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 |
|
|
| `wiresheet.py` | Grid, component boxes, link routing and painting | 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, `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.
|
|
|
|
### Testing without a window
|
|
|
|
`app_state`, `sax_dom`, `schema` 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 directory is currently **fatal** — the app reports it and calls
|
|
`sys.exit(1)`. See [Known gaps](#known-gaps).
|
|
|
|
### 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.
|
|
|
|
## 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
|
|
|
|
- A kit missing from `<sedona home>/manifests/` exits the whole app rather than warning
|
|
and declining to open that file.
|
|
- 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.
|
|
- No undo, and no keyboard shortcuts for save.
|
|
- `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 display-only; there is no editing of a slot from the wiresheet.
|