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>
120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
"""Shared configuration, runtime state and widget handles.
|
|
|
|
Every other module reaches this state as ``app_state.<name>`` rather than importing
|
|
the names directly. A plain ``from app_state import is_dirty`` would copy the value,
|
|
so later rebinding would only ever be seen by the module that did it.
|
|
"""
|
|
|
|
import configparser
|
|
import os
|
|
|
|
VERSION = "0.0.0.028"
|
|
PROPERTIES_FILE = "editor.properties"
|
|
DEFAULT_GRID_SIZE = 10
|
|
DEFAULT_ADD_TIMESTAMP = True
|
|
|
|
# Purely a display choice, so it lives in editor.properties. The SAX file has nowhere to
|
|
# record it: meta stores a position and nothing about box size.
|
|
DEFAULT_BOX_WIDTH = 100
|
|
MIN_ALLOWED_BOX_WIDTH = 60
|
|
MAX_ALLOWED_BOX_WIDTH = 400
|
|
|
|
# Widget handles, filled in by main once the UI exists
|
|
window = None
|
|
canvas = None
|
|
tree = None
|
|
|
|
# Maps to hold tracking states
|
|
tree_element_map = {} # Treeview item ID -> ET.Element
|
|
canvas_comp_map = {} # Canvas item tag -> ET.Element
|
|
canvas_tag_by_element = {} # ET.Element -> Canvas unique tag string
|
|
component_slot_offsets = {} # Canvas unique tag -> {slot name: y offset from box top}
|
|
|
|
# File modification tracking state
|
|
is_dirty = False
|
|
current_file_path = None
|
|
active_components_list = [] # Tracks what components are actively painted
|
|
selected_canvas_tag = None # Tracks the currently clicked/highlighted canvas component tag
|
|
selected_tree_item_id = None # Tracks the currently right-clicked Treeview item
|
|
xml_root_element = None # Keep a persistent root reference to redraw tree layouts cleanly
|
|
current_sheet_parent = None # Element whose children are painted; None means the app root
|
|
|
|
# Drag-and-drop state variables
|
|
drag_data = {"x": 0, "y": 0, "tag": None}
|
|
|
|
# Global component slot registries compiled dynamically per file load
|
|
final_components_registry = {}
|
|
active_schema_kits = {}
|
|
|
|
|
|
def load_preferences():
|
|
"""Loads application configurations from editor.properties or initializes defaults."""
|
|
config = configparser.ConfigParser()
|
|
grid_size = DEFAULT_GRID_SIZE
|
|
add_timestamp = DEFAULT_ADD_TIMESTAMP
|
|
box_width = DEFAULT_BOX_WIDTH
|
|
sedona_home = ""
|
|
|
|
if os.path.exists(PROPERTIES_FILE):
|
|
try:
|
|
config.read(PROPERTIES_FILE)
|
|
grid_size = config.getint("Editor", "grid_size", fallback=DEFAULT_GRID_SIZE)
|
|
add_timestamp = config.getboolean("Editor", "add_timestamp", fallback=DEFAULT_ADD_TIMESTAMP)
|
|
box_width = config.getint("Editor", "box_width", fallback=DEFAULT_BOX_WIDTH)
|
|
sedona_home = config.get("Editor", "sedona.home", fallback="").strip()
|
|
except Exception:
|
|
pass
|
|
else:
|
|
config["Editor"] = {
|
|
"sedona.home": "",
|
|
"grid_size": str(DEFAULT_GRID_SIZE),
|
|
"add_timestamp": str(DEFAULT_ADD_TIMESTAMP),
|
|
"box_width": str(DEFAULT_BOX_WIDTH)
|
|
}
|
|
try:
|
|
with open(PROPERTIES_FILE, "w") as configfile:
|
|
config.write(configfile)
|
|
except Exception:
|
|
pass
|
|
|
|
box_width = max(MIN_ALLOWED_BOX_WIDTH, min(MAX_ALLOWED_BOX_WIDTH, box_width))
|
|
return sedona_home, grid_size, add_timestamp, box_width
|
|
|
|
|
|
def save_preferences():
|
|
"""Writes the current preference values back to editor.properties."""
|
|
config = configparser.ConfigParser()
|
|
config["Editor"] = {
|
|
"sedona.home": SEDONA_HOME,
|
|
"grid_size": str(GRID_SIZE),
|
|
"add_timestamp": str(ADD_TIMESTAMP),
|
|
"box_width": str(BOX_WIDTH)
|
|
}
|
|
with open(PROPERTIES_FILE, "w") as configfile:
|
|
config.write(configfile)
|
|
|
|
|
|
def base_window_title():
|
|
"""The window title with no file context, which handlers extend with their own suffix."""
|
|
return f"Sedona SAX Tree View Viewer & Program Editor [v{VERSION}]"
|
|
|
|
|
|
def mark_dirty():
|
|
"""Single funnel for 'the document changed', including the unsaved-state tint."""
|
|
global is_dirty
|
|
if not is_dirty:
|
|
is_dirty = True
|
|
if canvas is not None:
|
|
canvas.config(bg="#ffe6e6")
|
|
|
|
|
|
def mark_clean():
|
|
"""Clears the unsaved-state marker after a successful save or a fresh load."""
|
|
global is_dirty
|
|
is_dirty = False
|
|
if canvas is not None:
|
|
canvas.config(bg="white")
|
|
|
|
|
|
SEDONA_HOME, GRID_SIZE, ADD_TIMESTAMP, BOX_WIDTH = load_preferences()
|