Compare commits
2
Commits
8b21c5f389
...
bc4e28d3f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc4e28d3f7 | ||
|
|
b63655462f |
@@ -47,7 +47,7 @@ not inside the window. Tk always hands it to the system menu bar.
|
|||||||
| `app_state.py` | Preferences, runtime state, widget handles, `mark_dirty`/`mark_clean` | no |
|
| `app_state.py` | Preferences, runtime state, widget handles, `mark_dirty`/`mark_clean` | no |
|
||||||
| `sax_dom.py` | `meta` bit-packing codec, component path resolution | no |
|
| `sax_dom.py` | `meta` bit-packing codec, component path resolution | no |
|
||||||
| `schema.py` | Kit manifest loading, slot inheritance, slot visibility rules | no |
|
| `schema.py` | Kit manifest loading, slot inheritance, slot visibility rules | no |
|
||||||
| `undo.py` | Undo/redo stack of reversible DOM commands | no |
|
| `undo.py` | Undo stack of reversible commands (DOM, and the device in live mode) | no |
|
||||||
| `wiresheet.py` | Grid, component boxes, link routing and painting | yes |
|
| `wiresheet.py` | Grid, component boxes, link routing and painting | yes |
|
||||||
| `properties.py` | The Properties view: every slot of one component, read and written | yes |
|
| `properties.py` | The Properties view: every slot of one component, read and written | yes |
|
||||||
| `link_view.py` | The Link View: every link touching one component, drawn around it | yes |
|
| `link_view.py` | The Link View: every link touching one component, drawn around it | yes |
|
||||||
@@ -74,7 +74,16 @@ invisible to every other module. Every mutation of "the document changed" goes t
|
|||||||
(which also removes the `<kit>` entry placing it may have added) and
|
(which also removes the `<kit>` entry placing it may have added) and
|
||||||
`DeleteComponentCommand` (which restores the removed `<link>` entries at their original
|
`DeleteComponentCommand` (which restores the removed `<link>` entries at their original
|
||||||
indices, reinserting in ascending order so the indices stay valid) and `SlotValueCommand`
|
indices, reinserting in ascending order so the indices stay valid) and `SlotValueCommand`
|
||||||
(one Save from the Properties view, however many slots it changed). Nothing in
|
(one Save from the Properties view, however many slots it changed). There is no redo.
|
||||||
|
|
||||||
|
On a device the same steps are undoable, and each undo is sent there: `undo.py` has
|
||||||
|
live subclasses for moves, slot edits and renames, and `sax_file` has
|
||||||
|
`LiveComponentCommand` and `LiveLinkCommand`. Deleting a component first reads its
|
||||||
|
whole subtree back from the device (config, children, links, place among its
|
||||||
|
siblings), so undo can recreate it; the device hands out new ids, which the DOM
|
||||||
|
elements take on. An undo that does not fully reach the device warns and clears the
|
||||||
|
history, and so does every view change, since opening a sheet rebuilds the DOM from the
|
||||||
|
device. Nothing in
|
||||||
`undo.py` touches Tk; `main` registers a refresh callback at startup, which is what keeps
|
`undo.py` touches Tk; `main` registers a refresh callback at startup, which is what keeps
|
||||||
`sax_file` and `wiresheet` free to push commands without an import cycle.
|
`sax_file` and `wiresheet` free to push commands without an import cycle.
|
||||||
|
|
||||||
@@ -84,7 +93,6 @@ Undoing back to the depth of the last save clears the dirty tint rather than lea
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `Ctrl`/`Cmd` + `S` | Save (direct overwrite) |
|
| `Ctrl`/`Cmd` + `S` | Save (direct overwrite) |
|
||||||
| `Ctrl`/`Cmd` + `Z` | Undo |
|
| `Ctrl`/`Cmd` + `Z` | Undo |
|
||||||
| `Ctrl`/`Cmd` + `Y` or `Ctrl`/`Cmd` + `Shift` + `Z` | Redo |
|
|
||||||
| `Del` / `Backspace` | Delete selected component |
|
| `Del` / `Backspace` | Delete selected component |
|
||||||
| `Ctrl`/`Cmd` + `B` | Show or hide the tree/palette column |
|
| `Ctrl`/`Cmd` + `B` | Show or hide the tree/palette column |
|
||||||
| `Ctrl` + `W` | Tidy Layout: re-flow the open sheet |
|
| `Ctrl` + `W` | Tidy Layout: re-flow the open sheet |
|
||||||
|
|||||||
+36
-2
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.152"
|
VERSION = "0.0.0.154"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
@@ -148,8 +148,15 @@ def load_preferences():
|
|||||||
|
|
||||||
|
|
||||||
def save_preferences():
|
def save_preferences():
|
||||||
"""Writes the current preference values back to editor.properties."""
|
"""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()
|
config = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
config.read(PROPERTIES_FILE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
config["Editor"] = {
|
config["Editor"] = {
|
||||||
"sedona.home": SEDONA_HOME,
|
"sedona.home": SEDONA_HOME,
|
||||||
"grid_size": str(GRID_SIZE),
|
"grid_size": str(GRID_SIZE),
|
||||||
@@ -162,6 +169,32 @@ def save_preferences():
|
|||||||
config.write(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():
|
def base_window_title():
|
||||||
"""The window title with no file context, which handlers extend with their own suffix."""
|
"""The window title with no file context, which handlers extend with their own suffix."""
|
||||||
return f"Sedona SAX Offline Program Editor [v{VERSION}]"
|
return f"Sedona SAX Offline Program Editor [v{VERSION}]"
|
||||||
@@ -221,3 +254,4 @@ def base_window_title_live():
|
|||||||
|
|
||||||
|
|
||||||
SEDONA_HOME, GRID_SIZE, ADD_TIMESTAMP, BOX_WIDTH, LEFT_PANE_WIDTH, TRACE_DASP = load_preferences()
|
SEDONA_HOME, GRID_SIZE, ADD_TIMESTAMP, BOX_WIDTH, LEFT_PANE_WIDTH, TRACE_DASP = load_preferences()
|
||||||
|
LAST_PALETTE_KIT = load_last_palette_kit()
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ def refresh_dirty_display():
|
|||||||
|
|
||||||
|
|
||||||
def refresh_all_views():
|
def refresh_all_views():
|
||||||
"""Rebuilds tree and canvas from the DOM. Registered as undo/redo's view refresh."""
|
"""Rebuilds tree and canvas from the DOM. Registered as undo's view refresh."""
|
||||||
sax_file.refresh_treeview_from_dom(app_state.xml_root_element)
|
sax_file.refresh_treeview_from_dom(app_state.xml_root_element)
|
||||||
wiresheet.render_current_sheet()
|
wiresheet.render_current_sheet()
|
||||||
|
|
||||||
@@ -559,14 +559,6 @@ def do_undo(event=None):
|
|||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
def do_redo(event=None):
|
|
||||||
label = undo.redo()
|
|
||||||
if label is None:
|
|
||||||
return "break"
|
|
||||||
app_state.window.title(f"{_base_title()} — redid {label}")
|
|
||||||
return "break"
|
|
||||||
|
|
||||||
|
|
||||||
def _base_title():
|
def _base_title():
|
||||||
if app_state.live_mode:
|
if app_state.live_mode:
|
||||||
return app_state.base_window_title_live()
|
return app_state.base_window_title_live()
|
||||||
@@ -943,7 +935,7 @@ def on_tree_select(event):
|
|||||||
element = app_state.tree_element_map.get(selected_id)
|
element = app_state.tree_element_map.get(selected_id)
|
||||||
components_to_render = []
|
components_to_render = []
|
||||||
|
|
||||||
# Remembered so undo/redo and tidy can repaint this same sheet from the DOM.
|
# Remembered so undo and tidy can repaint this same sheet from the DOM.
|
||||||
app_state.current_sheet_parent = element
|
app_state.current_sheet_parent = element
|
||||||
record_visit(VIEW_SHEET, element)
|
record_visit(VIEW_SHEET, element)
|
||||||
|
|
||||||
@@ -2754,7 +2746,6 @@ tree_root_context_menu = tk.Menu(window, tearoff=0)
|
|||||||
SHORTCUTS = [
|
SHORTCUTS = [
|
||||||
("s", do_save),
|
("s", do_save),
|
||||||
("z", do_undo),
|
("z", do_undo),
|
||||||
("y", do_redo),
|
|
||||||
("b", toggle_left_column),
|
("b", toggle_left_column),
|
||||||
# Zoom, in every spelling the key can arrive as: with and without Shift, and on the
|
# Zoom, in every spelling the key can arrive as: with and without Shift, and on the
|
||||||
# numeric keypad.
|
# numeric keypad.
|
||||||
@@ -2774,10 +2765,6 @@ for key, handler in SHORTCUTS:
|
|||||||
window.bind_all(f"<Command-{key}>", handler)
|
window.bind_all(f"<Command-{key}>", handler)
|
||||||
window.bind_all(f"<Control-{key}>", handler)
|
window.bind_all(f"<Control-{key}>", handler)
|
||||||
|
|
||||||
# Redo also as Shift-Z, the usual mac chord.
|
|
||||||
window.bind_all("<Command-Shift-Z>", do_redo)
|
|
||||||
window.bind_all("<Control-Shift-Z>", do_redo)
|
|
||||||
|
|
||||||
# Delete: the mac Delete key reports as BackSpace, the Windows one as Delete.
|
# Delete: the mac Delete key reports as BackSpace, the Windows one as Delete.
|
||||||
window.bind_all("<Delete>", delete_selected_component)
|
window.bind_all("<Delete>", delete_selected_component)
|
||||||
window.bind_all("<BackSpace>", delete_selected_component)
|
window.bind_all("<BackSpace>", delete_selected_component)
|
||||||
@@ -2817,7 +2804,6 @@ menu_bar.add_cascade(label="File", menu=menu_file)
|
|||||||
|
|
||||||
menu_edit = tk.Menu(menu_bar, tearoff=0)
|
menu_edit = tk.Menu(menu_bar, tearoff=0)
|
||||||
menu_edit.add_command(label="Undo", command=do_undo, accelerator=f"{accel_mod}+Z")
|
menu_edit.add_command(label="Undo", command=do_undo, accelerator=f"{accel_mod}+Z")
|
||||||
menu_edit.add_command(label="Redo", command=do_redo, accelerator=f"{accel_mod}+Y")
|
|
||||||
menu_edit.add_separator()
|
menu_edit.add_separator()
|
||||||
menu_edit.add_command(label="Delete Component", command=delete_selected_component, accelerator="Del")
|
menu_edit.add_command(label="Delete Component", command=delete_selected_component, accelerator="Del")
|
||||||
menu_bar.add_cascade(label="Edit", menu=menu_edit)
|
menu_bar.add_cascade(label="Edit", menu=menu_edit)
|
||||||
@@ -2994,6 +2980,8 @@ def populate_palette_types(event=None):
|
|||||||
return
|
return
|
||||||
|
|
||||||
kit_name = palette_kit_names[index]
|
kit_name = palette_kit_names[index]
|
||||||
|
if event is not None and kit_name != app_state.LAST_PALETTE_KIT:
|
||||||
|
app_state.save_last_palette_kit(kit_name)
|
||||||
types = palette.list_component_types(kit_name)
|
types = palette.list_component_types(kit_name)
|
||||||
|
|
||||||
for type_name, base in types:
|
for type_name, base in types:
|
||||||
@@ -3038,9 +3026,13 @@ def refresh_palette_kits():
|
|||||||
palette_kit_var.set("")
|
palette_kit_var.set("")
|
||||||
return
|
return
|
||||||
|
|
||||||
palette_status.config(text=f"{len(palette_kit_names)} kits")
|
# Reopen the kit chosen last so the palette is never left empty; a file without that
|
||||||
palette_kit_var.set("")
|
# kit opens on its first kit instead, without forgetting the remembered one.
|
||||||
fit_tree_column(palette_tree)
|
if app_state.LAST_PALETTE_KIT in palette_kit_names:
|
||||||
|
palette_kit_combo.current(palette_kit_names.index(app_state.LAST_PALETTE_KIT))
|
||||||
|
else:
|
||||||
|
palette_kit_combo.current(0)
|
||||||
|
populate_palette_types()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -462,8 +462,9 @@ def rename_component(comp_element, new_name):
|
|||||||
for link, attr, _, new_ref in link_changes:
|
for link, attr, _, new_ref in link_changes:
|
||||||
link.set(attr, new_ref)
|
link.set(attr, new_ref)
|
||||||
|
|
||||||
if not app_state.live_mode:
|
command_class = (undo.LiveRenameCommand if app_state.live_mode
|
||||||
undo.push(undo.RenameComponentCommand(
|
else undo.RenameComponentCommand)
|
||||||
|
undo.push(command_class(
|
||||||
comp_element, old_name, new_name, link_changes,
|
comp_element, old_name, new_name, link_changes,
|
||||||
label=f"rename {old_name} to {new_name}"
|
label=f"rename {old_name} to {new_name}"
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Undo/redo as a stack of reversible commands.
|
"""Undo as a stack of reversible commands. There is no redo: an undone step is gone.
|
||||||
|
|
||||||
Commands own the DOM mutation in both directions; in live mode they also carry it to the
|
Commands own the DOM mutation in both directions -- redo() is how a command is first
|
||||||
device, and raise LiveUndoError when it does not get there. Nothing here touches Tk: the view is
|
applied, undo() takes it back. In live mode they also carry it to the device, and raise
|
||||||
|
LiveUndoError when it does not get there. Nothing here touches Tk: the view is
|
||||||
refreshed through a callback that main registers at startup, which also keeps the
|
refreshed through a callback that main registers at startup, which also keeps the
|
||||||
import graph free of cycles (sax_file and wiresheet both push commands here).
|
import graph free of cycles (sax_file and wiresheet both push commands here).
|
||||||
"""
|
"""
|
||||||
@@ -13,13 +14,12 @@ import schema
|
|||||||
from sax_dom import remove_prop, restore_children, set_prop_value, update_meta_property
|
from sax_dom import remove_prop, restore_children, set_prop_value, update_meta_property
|
||||||
|
|
||||||
_undo_stack = []
|
_undo_stack = []
|
||||||
_redo_stack = []
|
|
||||||
_saved_depth = 0 # len(_undo_stack) as of the last save
|
_saved_depth = 0 # len(_undo_stack) as of the last save
|
||||||
_refresh_view = None
|
_refresh_view = None
|
||||||
|
|
||||||
|
|
||||||
def set_refresh_callback(callback):
|
def set_refresh_callback(callback):
|
||||||
"""Registers how the view should be rebuilt after an undo or redo."""
|
"""Registers how the view should be rebuilt after an undo."""
|
||||||
global _refresh_view
|
global _refresh_view
|
||||||
_refresh_view = callback
|
_refresh_view = callback
|
||||||
|
|
||||||
@@ -36,21 +36,20 @@ def _refresh():
|
|||||||
|
|
||||||
|
|
||||||
def push(command):
|
def push(command):
|
||||||
"""Records an already-applied command. Any redo history becomes unreachable."""
|
"""Records an already-applied command."""
|
||||||
|
global _saved_depth
|
||||||
|
# Undone past the save, the saved state can no longer be reached by undoing.
|
||||||
|
if len(_undo_stack) < _saved_depth:
|
||||||
|
_saved_depth = -1
|
||||||
_undo_stack.append(command)
|
_undo_stack.append(command)
|
||||||
_redo_stack.clear()
|
|
||||||
|
|
||||||
|
|
||||||
def can_undo():
|
def can_undo():
|
||||||
return bool(_undo_stack)
|
return bool(_undo_stack)
|
||||||
|
|
||||||
|
|
||||||
def can_redo():
|
|
||||||
return bool(_redo_stack)
|
|
||||||
|
|
||||||
|
|
||||||
class LiveUndoError(Exception):
|
class LiveUndoError(Exception):
|
||||||
"""A live command's undo or redo did not fully reach the device.
|
"""A live command's undo did not fully reach the device.
|
||||||
|
|
||||||
Raised with what did not happen; live_hooks.last_error holds why.
|
Raised with what did not happen; live_hooks.last_error holds why.
|
||||||
"""
|
"""
|
||||||
@@ -65,21 +64,6 @@ def undo():
|
|||||||
except LiveUndoError as failure:
|
except LiveUndoError as failure:
|
||||||
_abandon_history("Undo", failure)
|
_abandon_history("Undo", failure)
|
||||||
return None
|
return None
|
||||||
_redo_stack.append(command)
|
|
||||||
_refresh()
|
|
||||||
return command.label
|
|
||||||
|
|
||||||
|
|
||||||
def redo():
|
|
||||||
if not _redo_stack:
|
|
||||||
return None
|
|
||||||
command = _redo_stack.pop()
|
|
||||||
try:
|
|
||||||
command.redo()
|
|
||||||
except LiveUndoError as failure:
|
|
||||||
_abandon_history("Redo", failure)
|
|
||||||
return None
|
|
||||||
_undo_stack.append(command)
|
|
||||||
_refresh()
|
_refresh()
|
||||||
return command.label
|
return command.label
|
||||||
|
|
||||||
@@ -87,8 +71,8 @@ def redo():
|
|||||||
def _abandon_history(title, failure):
|
def _abandon_history(title, failure):
|
||||||
"""After a step that did not reach the device, the history no longer matches it.
|
"""After a step that did not reach the device, the history no longer matches it.
|
||||||
|
|
||||||
The device may hold part of the step, so neither direction can be trusted to land
|
The device may hold part of the step, so the steps below it cannot be trusted to land
|
||||||
where it says. History is dropped rather than replayed against a state it never saw.
|
where they say. History is dropped rather than replayed against a state it never saw.
|
||||||
"""
|
"""
|
||||||
reset()
|
reset()
|
||||||
_refresh()
|
_refresh()
|
||||||
@@ -105,14 +89,13 @@ def reset():
|
|||||||
"""Drops all history, for when a different file is loaded."""
|
"""Drops all history, for when a different file is loaded."""
|
||||||
global _saved_depth
|
global _saved_depth
|
||||||
_undo_stack.clear()
|
_undo_stack.clear()
|
||||||
_redo_stack.clear()
|
|
||||||
_saved_depth = 0
|
_saved_depth = 0
|
||||||
|
|
||||||
|
|
||||||
def reload_schema_registry():
|
def reload_schema_registry():
|
||||||
"""Recompiles the slot registry after the <schema> block changed.
|
"""Recompiles the slot registry after the <schema> block changed.
|
||||||
|
|
||||||
A failure is swallowed: undo and redo only ever land on a state that resolved once
|
A failure is swallowed: undo only ever lands on a state that resolved once
|
||||||
already, so a missing manifest here means Sedona home moved underneath the session,
|
already, so a missing manifest here means Sedona home moved underneath the session,
|
||||||
which the next action reports in its own words.
|
which the next action reports in its own words.
|
||||||
"""
|
"""
|
||||||
@@ -360,3 +343,19 @@ class RenameComponentCommand:
|
|||||||
self.element.set("name", self.new_name)
|
self.element.set("name", self.new_name)
|
||||||
for link, attr, _, new_value in self.link_changes:
|
for link, attr, _, new_value in self.link_changes:
|
||||||
link.set(attr, new_value)
|
link.set(attr, new_value)
|
||||||
|
|
||||||
|
|
||||||
|
class LiveRenameCommand(RenameComponentCommand):
|
||||||
|
"""A rename on the device: the old name goes back there before the DOM takes it."""
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if not live_hooks.live_rename_component(self.element, self.old_name):
|
||||||
|
raise LiveUndoError(f"{self.new_name} was not renamed back to {self.old_name} "
|
||||||
|
"on the device")
|
||||||
|
super().undo()
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if not live_hooks.live_rename_component(self.element, self.new_name):
|
||||||
|
raise LiveUndoError(f"{self.old_name} was not renamed to {self.new_name} "
|
||||||
|
"on the device")
|
||||||
|
super().redo()
|
||||||
|
|||||||
Reference in New Issue
Block a user