sedona_editor/README.md
arda.aydin@ontrol.com.tr 9c283c7eea First Commit
Sedona sax editor AI yardimi ile yaziliyor
2026-08-13 17:59:08 +03:00

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.