Go to file
Arda Aydin c8026f00db Warn and reset instead of exiting when a kit manifest is missing
A kit with no manifest under the Sedona home folder called sys.exit(1),
tearing down the whole app over one unopenable file.

schema now raises MissingManifestError instead, collecting every
unresolved kit first so one dialog names them all rather than the user
fixing them one reopen at a time. describe_missing_manifests builds the
body, pointing at the configured Sedona home and at Preferences.

open_file catches it, reports it, and calls the new
reset_to_startup_state to return to the just-launched state: no file,
empty tree, empty sheet, cleared registries and undo stack. The generic
except now resets too, since a half-parsed file previously left
current_file_path and a stale root behind.

Placing a palette component hits the same loader, where a full reset
would be too destructive: it rolls back the <kit> entry it just added,
rebuilds the previous registry, and abandons only that placement.

schema no longer imports sys or tkinter, so it reports nothing itself
and stays testable without a display. base_window_title moves to
app_state so the reset can restore the title without duplicating it.

README: document the new behaviour, drop the fixed gap, and correct the
stale "no undo, no keyboard shortcuts for save" claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:25:01 +03:00
testSax First Commit 2026-08-13 17:59:08 +03:00
app_state.py Warn and reset instead of exiting when a kit manifest is missing 2026-08-13 23:25:01 +03:00
editor.properties First Commit 2026-08-13 17:59:08 +03:00
main.py Warn and reset instead of exiting when a kit manifest is missing 2026-08-13 23:25:01 +03:00
palette.py First Commit 2026-08-13 17:59:08 +03:00
README.md Warn and reset instead of exiting when a kit manifest is missing 2026-08-13 23:25:01 +03:00
run.py First Commit 2026-08-13 17:59:08 +03:00
sax_dom.py First Commit 2026-08-13 17:59:08 +03:00
sax_file.py Warn and reset instead of exiting when a kit manifest is missing 2026-08-13 23:25:01 +03:00
schema.py Warn and reset instead of exiting when a kit manifest is missing 2026-08-13 23:25:01 +03:00
undo.py First Commit 2026-08-13 17:59:08 +03:00
wiresheet.py First Commit 2026-08-13 17:59:08 +03:00

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).

Requirements

Python 3.13 from Homebrew, which brings Tk 9:

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:

/opt/homebrew/bin/python3.13 -c "import tkinter; r=tkinter.Tk(); print(r.tk.call('info','patchlevel'))"

Running

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.<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). 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:

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> 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.