# 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 | | `undo.py` | Undo/redo stack of reversible DOM commands | 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.`**, 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 `` entry placing it may have added) and `DeleteComponentCommand` (which restores the removed `` entries at their original indices, reinserting in ascending order so the indices stay valid). 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 | 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. ### 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 `` in the file's `` block is resolved to `/manifests//-.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 `` entry just added to `` 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 `` (either end) | **Show** — overrides everything below | | 2 | `` 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 `` 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 `` 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 - 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 display-only; there is no editing of a slot from the wiresheet.