Follows Ontrol's Workbench tool (BPutWithFilesTool / PutApp): the backup's app.sax is encoded for the device's kits and put as app.sab.writing, renamed to app.sab.stage; the platform service's deleteFile clears fonts/* and images/*, then images, fonts (from Sedona home's bank, in the device's cut) and the boot logo go. The zip is checked against the device first: same platform (Orion, Rion or P-ION, from the platform id), and kits the device can load. Falls back to the backup's app.sab, with its kits.scode if needed. - sab_app.encode_sax: .sax -> .sab, ported from Py_sab2sax with Java's value parsing (fromLiteral, nullbool/nullfloat, Integer.decode); byte-identical to sedonac on the saved backups - app_backup.native_image: port of OntOxImageUtil's png -> native .bin - sox.put_file / rename_file - Get with Files: Orion's boot logo is system/logo.jpg - A restart that goes unanswered is taken as the device going down Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
10 KiB
Python
264 lines
10 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.165"
|
|
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
|
|
|
|
# The tree and palette column is held at this width whatever the window does, so a wider
|
|
# window only ever gives the wiresheet more room. The sash can still be dragged; this is
|
|
# the width the column is set to at startup and whenever the preference changes.
|
|
DEFAULT_LEFT_PANE_WIDTH = 230
|
|
MIN_ALLOWED_LEFT_PANE_WIDTH = 120
|
|
MAX_ALLOWED_LEFT_PANE_WIDTH = 600
|
|
|
|
# Called whenever "is there anything unwritten" may have changed, so the file name on
|
|
# the tree can follow it. main registers it; anything before that is a no-op.
|
|
dirty_display_callback = None
|
|
|
|
# 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
|
|
|
|
# The unsaved-state tint. The wiresheet paints it on the canvas and the Properties view
|
|
# paints it on itself, so "there is something here you have not written yet" is one
|
|
# colour wherever the change was made.
|
|
DIRTY_TINT = "#ffe6e6"
|
|
CLEAN_TINT = "white"
|
|
|
|
# The file name on the tree's root row turns this colour while anything is unwritten,
|
|
# which is the one place a file name appears on screen -- a Tk window title cannot be
|
|
# coloured.
|
|
#
|
|
# The tag's colour is configured once and never changed; what moves is whether the row
|
|
# carries the tag. Reconfiguring a tag does not reliably repaint rows that are already
|
|
# drawn -- which is why the name only ever went red on the paths that happened to rebuild
|
|
# the tree, and stayed black after a box was dragged. Setting a row's tags repaints that
|
|
# row, so the two states cannot drift apart.
|
|
DIRTY_NAME_COLOUR = "#c00000"
|
|
DIRTY_NAME_TAG = "unsaved_file"
|
|
|
|
# Marks the tree row a palette drag would drop into.
|
|
TREE_DROP_TAG = "palette_drop_target"
|
|
|
|
# A themed Treeview maps the selected state to SystemHighlightText, and that mapping beats
|
|
# a row tag's own foreground -- so the file name turned white-on-blue the moment its row
|
|
# was selected, which is exactly when the root sheet is the one being looked at. The
|
|
# navigation tree therefore runs on a style of its own that drops the selected foreground
|
|
# and picks a light selection background instead, so a tag's colour shows in every state
|
|
# and black text still reads on the highlight.
|
|
NAV_TREE_STYLE = "Nav.Treeview"
|
|
NAV_SELECTION_BG = "#cce4f7"
|
|
|
|
# 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_canvas_tags = set() # Every selected box, the one above included; Ctrl+click adds
|
|
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
|
|
selected_link_tags = [] # Every selected link wire, in picking order; Ctrl+click adds.
|
|
# selected_link_tag is the last of these.
|
|
unroutable_link_count = 0 # Links drawn as knobs because no wire could avoid a component
|
|
meta_scale = DEFAULT_GRID_SIZE # Pixels per meta coordinate step on the painted sheet
|
|
zoom = 1.0 # Scale the open sheet is painted at; 1.0 is one pixel per pixel
|
|
knob_link_ends = {} # wire tag -> (x1, y1, x2, y2) for a link shown as two knobs
|
|
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
|
|
left_pane_width = DEFAULT_LEFT_PANE_WIDTH
|
|
sedona_home = ""
|
|
trace_dasp = False
|
|
|
|
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)
|
|
left_pane_width = config.getint(
|
|
"Editor", "left_pane_width", fallback=DEFAULT_LEFT_PANE_WIDTH
|
|
)
|
|
sedona_home = config.get("Editor", "sedona.home", fallback="").strip()
|
|
trace_dasp = config.getboolean("Editor", "trace_dasp", fallback=False)
|
|
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),
|
|
"left_pane_width": str(DEFAULT_LEFT_PANE_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))
|
|
left_pane_width = max(
|
|
MIN_ALLOWED_LEFT_PANE_WIDTH, min(MAX_ALLOWED_LEFT_PANE_WIDTH, left_pane_width)
|
|
)
|
|
return sedona_home, grid_size, add_timestamp, box_width, left_pane_width, trace_dasp
|
|
|
|
|
|
def save_preferences():
|
|
"""Writes the current preference values back to editor.properties.
|
|
|
|
The file is read first so the sections other code owns ([Device], [Palette]) survive.
|
|
"""
|
|
config = configparser.ConfigParser()
|
|
try:
|
|
config.read(PROPERTIES_FILE)
|
|
except Exception:
|
|
pass
|
|
config["Editor"] = {
|
|
"sedona.home": SEDONA_HOME,
|
|
"grid_size": str(GRID_SIZE),
|
|
"add_timestamp": str(ADD_TIMESTAMP),
|
|
"box_width": str(BOX_WIDTH),
|
|
"left_pane_width": str(LEFT_PANE_WIDTH),
|
|
"trace_dasp": str(TRACE_DASP),
|
|
}
|
|
with open(PROPERTIES_FILE, "w") as configfile:
|
|
config.write(configfile)
|
|
|
|
|
|
def load_last_palette_kit():
|
|
"""The kit the palette dropdown showed last, or "" when none was ever chosen."""
|
|
config = configparser.ConfigParser()
|
|
try:
|
|
config.read(PROPERTIES_FILE)
|
|
return config.get("Palette", "kit", fallback="").strip()
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def save_last_palette_kit(kit_name):
|
|
"""Remembers the palette's kit so the next start, or a kit refresh, reopens it."""
|
|
global LAST_PALETTE_KIT
|
|
LAST_PALETTE_KIT = kit_name
|
|
config = configparser.ConfigParser()
|
|
try:
|
|
config.read(PROPERTIES_FILE)
|
|
if "Palette" not in config:
|
|
config["Palette"] = {}
|
|
config["Palette"]["kit"] = kit_name
|
|
with open(PROPERTIES_FILE, "w") as configfile:
|
|
config.write(configfile)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
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 refresh_dirty_display():
|
|
"""Nudges whatever shows 'there is something unwritten'. A no-op before the UI exists."""
|
|
if dirty_display_callback is not None:
|
|
dirty_display_callback()
|
|
|
|
|
|
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=DIRTY_TINT)
|
|
refresh_dirty_display()
|
|
|
|
|
|
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=CLEAN_TINT)
|
|
refresh_dirty_display()
|
|
|
|
|
|
# ── Live device connection state ────────────────────────────────
|
|
live_mode = False
|
|
device_session = None # device_session.DeviceSession instance
|
|
device_host = ""
|
|
device_port = 1876
|
|
connection_status = "offline" # offline | connecting | connected | error
|
|
|
|
|
|
def base_window_title_live():
|
|
"""Window title for live mode."""
|
|
return f"Sedona Live Editor [v{VERSION}] — {device_host}:{device_port}"
|
|
|
|
|
|
SEDONA_HOME, GRID_SIZE, ADD_TIMESTAMP, BOX_WIDTH, LEFT_PANE_WIDTH, TRACE_DASP = load_preferences()
|
|
LAST_PALETTE_KIT = load_last_palette_kit()
|