Files
sedona_editor/app_state.py
T
ArdaandClaude Opus 5 01dadea610 Manage kits: reconcile the schema on save, and a dialog to repin builds
Placing a palette component no longer edits <schema> straight away. The kit is
noted as pending, its manifest is compiled so the box can be painted, and the
status bar says the kit will be declared on save. The <schema> block is then
reconciled when the file is written, in the one place that can report the whole
kit list the file will carry, with each kit's version and checksum.

What a kit's manifest depends on counts as used too, transitively, so placing a
component can bring in more than one entry: hvac pulls in types, ontrolPion
pulls in ontrolPlatform.

New Kits > Manage Kits... dialog lists every kit in three groups -- needed by
the app, declared but unused, and merely installed -- with the pinned build, the
newest installed build, and an Action dropdown of every build a kit ships.
The tick means the schema declares the kit: grey and fixed for kits the app
needs, unticked to remove, ticked to add at the newest build. "Upgrade all kits
to latest version" moves every declared kit; "Update kits" applies the ticks and
the Action column, additions and removals included.

Every schema change is applied as a batch, then verified: a missing manifest or
a component type that stops resolving reverts the whole block and reports why,
and a kit the app needs cannot be removed. Changes are one undo step, which
recompiles the slot registry in both directions since a repin can change a
component's slots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:06:25 +03:00

149 lines
5.5 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 = os.path.join(os.path.dirname(os.path.abspath(__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
status_label = None # the window's bottom status bar
# Status bar tones. A hint that something needs attention later is worth a colour, so a
# placement that leaves the schema out of step is not read as a plain confirmation.
STATUS_INFO_COLOUR = "#333333"
STATUS_WARN_COLOUR = "#a04b00"
# 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
link_mark_element = None # Component marked as a link endpoint via "Link Mark"
selected_link_tag = None # Canvas tag of the currently selected (red-highlighted) link wire
canvas_link_map = {} # Canvas link tag -> ET.Element (<link>)
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 = {} # kit name -> checksum, as the open file's <schema> declares them
# Kits a placed component needs that the file's <schema> does not declare yet, as
# kit name -> checksum resolved from Sedona home. Filled in when a palette component is
# placed and read back when saving, which is where the <schema> block is reconciled.
pending_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 Offline Program Editor [v{VERSION}]"
def set_status(message, warn=False):
"""Posts a one-line note to the status bar. A no-op before the UI exists."""
if status_label is None:
return
status_label.config(
text=message,
foreground=STATUS_WARN_COLOUR if warn else STATUS_INFO_COLOUR
)
def clear_status():
"""Empties the status bar, for when the note no longer describes anything on screen."""
set_status("")
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()