GUI: open, edit, validate, save¶
How a case file gets into the editor, what happens when it is edited and checked, and how it
gets back to disk. Every dialog the user can meet on the way is drawn, with the function that
shows it. The contracts of the MainWindow methods are in their docstrings (the standard is
Function contracts and flow charts); the run path has its own chart.
Qt slots swallow exceptions: an exception that escapes a slot is printed and the event loop goes on, so nothing is shown to the user. A box labelled "dialog" below is shown on purpose; "printed, nothing shown" marks the paths where it is not.
Open¶
What enters: a path, from File -> Open, the Recent and Example menus, the guided tour, the command line, or a crash restore. What leaves: the model holds the case, its sidecar layout and the path, clean, with an empty undo history, or the previous case is untouched.
flowchart TD
M["File menu: Open"] --> D1{"_maybe_discard_changes"}
R["Open Recent, Open Example"] --> D1
T["guided tour"] --> D1
D1 -- "dirty, answer No" --> STAY["window unchanged"]
D1 -- "File menu Open, clean or Yes" --> FD["file dialog (Cancel: nothing)"]
D1 -- "Recent or Example, clean or Yes" --> E1{"file exists?"}
E1 -- no --> W1["warning dialog: File missing / Example missing"]
E1 -- yes --> OC
FD --> OC
CL["command line case: main"] --> OC
RS["crash restore"] --> OC
OC{"open_case_file"} --> V{"load_vehicle_case_dict"}
V -- "missing, not UTF-8, TOML syntax" --> X1["critical dialog: Open failed, Could not parse path"]
V -- "vehicle case" --> VD["VehicleCaseDialog, network case untouched"]
VD -- "bad number" --> X2["critical dialog: Could not read path"]
V -- "gas network" --> L{"CaseModel.load"}
L -- "unknown key, bad type" --> X3["critical dialog: Could not load path"]
L --> VL["_validate_loaded_case: second pass, issues only"]
VL --> RP["CaseModel.replace: caseReplaced, caseIdentityChanged"]
RP --> SY["_sync_case_to_canvas, run result cleared, halos forgotten"]
SY --> CAM["camera restored only if a sidecar exists"]
CAM --> REC["push_recent, OSError ignored"]
REC --> ISS{"issues?"}
ISS -- yes --> FLAG["halos, console message, status: N issue(s), flagged red"]
ISS -- no --> OK["status: Loaded name"]
A case that fails validation still opens, so it can be fixed in the editor; only a file that does not parse is refused, and then the previous case, path and undo history stay as they were. An empty file is not an error: it opens as an empty case.
Edit and validate¶
What enters: an inspector edit, a canvas drag or a palette press. Every discrete edit pushes an undo checkpoint first; a handler that ends up changing nothing takes it back. All of them mark the case dirty through one of the model's four change signals.
flowchart TD
IN["inspector field edit"] --> SE{"_on_spec_edited"}
PAL["palette press, drop"] --> PR["_on_palette_request: _push_undo, add spec, add node"]
DRG["canvas drag"] --> NM["_on_node_moved: layout position, dirty, no undo entry"]
PRT["port drag"] --> CR["_on_connection_requested: _push_undo, retarget or add port"]
CR -- "no pipe end plus consumer" --> RV["checkpoint taken back, nothing changes"]
SE -- "blank id" --> BLK["id restored, status: An id cannot be empty"]
SE -- "id already on the canvas, or its actor names clash" --> COL["id restored, status: id already in use, or it clashes with the other id"]
SE --> CL{"_classify_spec_edit"}
CL -- structural --> ST["emit_structure_changed: lines, list, preview"]
CL -- init --> IC["emit_init_changed: preview, modes, list"]
CL -- field --> FL["emit_field_changed: CFL, internals, Case Settings"]
ST --> DIRTY["dirty flag set, title gets a star"]
IC --> DIRTY
FL --> DIRTY
PR --> DIRTY
NM --> DIRTY
DIRTY --> UT["_update_title"]
DIRTY --> AS["_on_autosave_tick every 30 s: staged copy to the autosave folder"]
AS -- "write fails" --> ASF["logged and shown once per session"]
UD["Undo, Redo"] --> UNDO["action_undo: snapshot restored, dirty"]
VAL["Run menu: Validate"] --> AV{"action_validate"}
AV -- "no pipes" --> NP["warning dialog: Nothing to validate"]
AV --> STG["stage a copy with absolute csv paths, load_case"]
STG -- "staging error" --> SF["critical dialog: Validate failed"]
STG -- "CaseValidationError" --> HAL["_set_validation_warnings: halos, console, status"]
STG -- "ValueError, OSError" --> RF["_report_staged_load_failure: console, status, no halos"]
STG -- "clean" --> CLR["halos cleared, console: no issues"]
Validation never changes the live case: it checks a staged copy in a temporary folder that is always removed. Run has the same pre-flight plus its own checks (see the run chart).
Save¶
What enters: Save, Save As or the window close. What leaves: the case TOML and its layout sidecar on disk (each written through a temporary name and renamed), the model clean with its new path, or an error dialog with the old files untouched.
flowchart TD
S["File menu: Save"] --> P0{"current path?"}
P0 -- none --> SA
P0 -- "bundled example" --> NOTE["status: stays unchanged, choose a new file"]
NOTE --> SA
P0 -- "own file" --> SP
SA["action_save_as: file dialog"] -- Cancel --> NONE["nothing written"]
SA --> SUF{"suffix is .toml, any case?"}
SUF -- "no: .toml appended" --> EX{"longer name exists?"}
EX -- "yes, answer No" --> NONE
EX -- "no, or answer Yes" --> SP
SUF -- yes --> SP
SP{"_save_to_path"} -- "inside the bundled examples" --> BE["warning dialog: Bundled example"]
SP --> CAMS["camera copied into the layout"]
CAMS --> SO{"_validate_species_ordering_with_user_prompt"}
SO -- "Cancel" --> NONE
SO -- "Fix automatically: reorder, no undo entry" --> MS
SO -- "Save anyway, or none wrong" --> MS
MS{"CaseModel.save"} --> SC["save_case: temp file, os.replace"]
SC --> SL["save_layout: temp file, os.replace"]
SL --> DONE["set_path, mark_clean, push_recent, autosave payload removed, status: Saved name"]
MS -- "any exception" --> ERR["critical dialog: Save failed, path and error; old file intact, still dirty"]
CLOSE["window close"] --> CQ{"solve running?"}
CQ -- "yes, answer No" --> KEEP["window stays open"]
CQ -- "no solve, or answer Yes" --> CD{"_maybe_discard_changes"}
CD -- "dirty, answer No" --> KEEP
CD -- "clean, or Yes" --> TIDY["cancel solve, remove lock and autosave, save dock layout, accept"]
A failed save leaves the old file byte for byte: the temporary file is removed and the model
stays dirty with its old path. The one gap on this path is a failure between the two renames:
the new TOML is then on disk with the old sidecar next to it, and the dialog says the save
failed. A successful Save removes the autosave for that path, so a later crash does not offer
the older copy; a failed Save keeps it (since 2026-10-11).
After a crash, the next launch offers the newest autosave (maybe_offer_orphan_restore):
a restored case is unsaved work with no path of its own, so closing asks and Save asks for a name.
Functions¶
aeolus1d_gui/app.py::MainWindow.action_openaeolus1d_gui/app.py::MainWindow._open_recentaeolus1d_gui/app.py::MainWindow._open_bundledaeolus1d_gui/app.py::MainWindow._open_case_file_if_discardedaeolus1d_gui/app.py::MainWindow._maybe_discard_changesaeolus1d_gui/app.py::MainWindow.open_case_fileaeolus1d_gui/app.py::MainWindow._sync_case_to_canvasaeolus1d_gui/app.py::MainWindow._set_validation_warningsaeolus1d_gui/app.py::MainWindow._on_spec_editedaeolus1d_gui/app.py::MainWindow._classify_spec_editaeolus1d_gui/app.py::MainWindow._on_palette_requestaeolus1d_gui/app.py::MainWindow._on_node_movedaeolus1d_gui/app.py::MainWindow._on_connection_requestedaeolus1d_gui/app.py::MainWindow._push_undoaeolus1d_gui/app.py::MainWindow.action_undoaeolus1d_gui/app.py::MainWindow._update_titleaeolus1d_gui/app.py::MainWindow._on_autosave_tickaeolus1d_gui/app.py::MainWindow.action_validateaeolus1d_gui/app.py::MainWindow._report_staged_load_failureaeolus1d_gui/app.py::MainWindow.action_saveaeolus1d_gui/app.py::MainWindow.action_save_asaeolus1d_gui/app.py::MainWindow._save_to_pathaeolus1d_gui/app.py::MainWindow._validate_species_ordering_with_user_promptaeolus1d_gui/app.py::MainWindow.closeEventaeolus1d_gui/app.py::MainWindow.maybe_offer_orphan_restoreaeolus1d_gui/model.py::CaseModel.loadaeolus1d_gui/model.py::_validate_loaded_caseaeolus1d_gui/model.py::CaseModel.replaceaeolus1d_gui/model.py::CaseModel.saveaeolus1d_gui/io/case_io.py::save_caseaeolus1d_gui/io/case_io.py::resolve_relative_csv_paths_for_staged_runaeolus1d_gui/io/layout_io.py::save_layoutaeolus1d_gui/io/layout_io.py::load_layoutaeolus1d_gui/io/recent.py::push_recentaeolus1d_gui/io/autosave.py::autosave_pathaeolus1d_gui/io/autosave.py::discover_orphanssrc/aeolus1d/io/parsers.py::load_casesrc/aeolus1d/io/vehicle_case.py::load_vehicle_case_dictsrc/aeolus1d/io/schema.py::validate_species_ordering