Back and Forward walk the panels too, not just the sheets
Opening a Properties or Link view was invisible to the history, so Back from one did two things at once: it took the panel down and stepped to the sheet before the one it was opened over. There was no way back to a picture you had just been reading. A history entry is now a (kind, element) pair naming what the editor pane held -- a sheet, a Properties view, or a Link View -- and restoring one puts that back: a sheet entry selects its tree node, a panel entry re-opens the panel. So opening a panel is a move like opening a sheet. Back from a Properties view lands on whatever came before it and Forward brings the panel back, and following a link out of the Link View now has a way home: Back returns to the picture the link was followed from, and a second Back to the sheet that was open before that. The panel's own Close is deliberately not recorded. It means "leave this view", and recording it would put a sheet entry after the panel's, so Back would re-open the panel that had just been closed. Leaving the position alone means Back steps to what came before the panel and Forward re-opens it, which is what the arrows are for. Both arrows still go through close_editor_panel first, so unsaved Properties edits are asked about exactly as before and a refusal leaves the view and the position where they were. Pruning now has two kinds of entry to check -- a sheet needs a tree node, a panel needs its component still in the document -- and collapses any pair it leaves naming the same view, since a Back that visibly does nothing is worse than a shorter trail. sheet_history and its functions are renamed for what they now hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -123,14 +123,36 @@ the boxes back to 1:1 for the routing pass and scales the result again, since th
|
||||
reads box positions straight off the canvas. Canvas coordinates therefore carry the zoom,
|
||||
which is why the drag, release and drop handlers snap against `meta_scale * zoom`.
|
||||
|
||||
**Back** and **Forward** walk the sheets opened this session, mirrored by the two arrow
|
||||
buttons in the toolbar. The history records the element whose children are painted -- the
|
||||
same thing `app_state.current_sheet_parent` holds -- rather than a tree item id, because
|
||||
rebuilding the tree hands out fresh ids while the elements themselves survive. Opening a
|
||||
different file retires the history; a delete or an undo drops only the entries whose
|
||||
element has left the document, keeping the position on screen. Walking the history drives
|
||||
the tree selection, which is what paints the sheet, and is suppressed from recording so a
|
||||
walk cannot rewrite the trail it is following.
|
||||
**Back** and **Forward** walk everything the **editor pane** has held this session,
|
||||
mirrored by the two arrow buttons in the toolbar -- sheets and the two panels alike. Each
|
||||
entry is a `(kind, element)` pair naming one of the three:
|
||||
|
||||
| Entry | What Back or Forward puts back |
|
||||
|---|---|
|
||||
| `(VIEW_SHEET, element)` | selects that element's tree node, which is what paints its children |
|
||||
| `(VIEW_PROPERTIES, comp)` | re-opens the Properties view of that component |
|
||||
| `(VIEW_LINKS, comp)` | re-opens the Link View of that component |
|
||||
|
||||
So opening a panel is a move like opening a sheet: **Back** from a Properties view lands
|
||||
on whatever the pane showed before it rather than skipping past to the sheet before that,
|
||||
and **Forward** brings the panel back. Following a link out of the Link View is an
|
||||
ordinary sheet opening, so Back from where it lands returns to the picture the link was
|
||||
followed from, and a second Back to the sheet that was open before that.
|
||||
|
||||
The panel's own **Close** is not recorded. It means "leave this view", and recording it
|
||||
would put a sheet entry after the panel's -- so Back would re-open the panel that had
|
||||
just been closed. Leaving the position where it is instead means Back still steps to what
|
||||
came before the panel, and Forward re-opens it, which is what the two arrows are for.
|
||||
|
||||
Entries hold elements rather than tree item ids, because rebuilding the tree hands out
|
||||
fresh ids while the elements themselves survive. Opening a different file retires the
|
||||
history. A delete or an undo drops the entries that can no longer be shown -- a sheet
|
||||
with no node left, a component out of the document -- keeping the position on screen, and
|
||||
collapses any pair that pruning has left naming the same view, since a Back that visibly
|
||||
does nothing is worse than a shorter trail. Restoring an entry is suppressed from
|
||||
recording, so a walk cannot rewrite the trail it is following, and both arrows go through
|
||||
`close_editor_panel` first -- so unsaved Properties edits are still asked about, and a
|
||||
refusal leaves both the view and the position where they were.
|
||||
|
||||
### The component popup
|
||||
|
||||
@@ -161,6 +183,9 @@ than leaving the selection looking like it did nothing. So does deleting the com
|
||||
is showing, from anywhere -- every delete in the UI goes through `delete_component`,
|
||||
because `sax_file` does its own view refresh rather than going through `refresh_all_views`.
|
||||
|
||||
All three are recorded in one history, so **Back** and **Forward** walk them together --
|
||||
see **The View menu** above.
|
||||
|
||||
### The Properties view
|
||||
|
||||
**Properties**, from either component popup, is the one place a slot's value is meant to
|
||||
@@ -477,8 +502,9 @@ was wiped by the repaint that followed it. The link is therefore left in
|
||||
`_pending_link_selection` for `on_tree_select` to honour once it has painted, and that
|
||||
handler reads and clears the request first thing -- so a selection that turns out not to
|
||||
paint a sheet drops it rather than leaving it to be applied to some later one. The link is
|
||||
then scrolled to the middle of the view. Arriving is an ordinary sheet opening, so **Back**
|
||||
returns to the sheet the link was followed from.
|
||||
then scrolled to the middle of the view. Arriving is an ordinary sheet opening, so
|
||||
**Back** returns to whatever the pane held before it -- the sheet the wire was
|
||||
right-clicked on, or the Link View the link was clicked in.
|
||||
|
||||
### Testing without a window
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
||||
import configparser
|
||||
import os
|
||||
|
||||
VERSION = "0.0.0.056"
|
||||
VERSION = "0.0.0.057"
|
||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||
DEFAULT_GRID_SIZE = 10
|
||||
DEFAULT_ADD_TIMESTAMP = True
|
||||
|
||||
@@ -34,10 +34,16 @@ PALETTE_REQUESTED_ROWS = 5
|
||||
# ttk has no way to hold a pane at a floor once the room is simply not there.
|
||||
MIN_WINDOW_HEIGHT = 300
|
||||
|
||||
# How many visited sheets Back and Forward can walk through. A sheet is one tree node's
|
||||
# children, so a session touches few of them; the cap only stops an unbounded list.
|
||||
# How many visited views Back and Forward can walk through. A session touches few of
|
||||
# them; the cap only stops an unbounded list.
|
||||
HISTORY_LIMIT = 50
|
||||
|
||||
# What a history entry can be showing. The editor pane holds one of these three at a
|
||||
# time, so the history that walks it has to be able to name each of them.
|
||||
VIEW_SHEET = "sheet"
|
||||
VIEW_PROPERTIES = "properties"
|
||||
VIEW_LINKS = "links"
|
||||
|
||||
# Tk class names of the widgets that own the keyboard while they have focus. Del and
|
||||
# Backspace mean "edit this text" in each of them, never "delete the selected component".
|
||||
TEXT_ENTRY_CLASSES = ("Entry", "TEntry", "TCombobox", "Text", "Spinbox", "TSpinbox")
|
||||
@@ -45,15 +51,26 @@ TEXT_ENTRY_CLASSES = ("Entry", "TEntry", "TCombobox", "Text", "Spinbox", "TSpinb
|
||||
MENU_BACK_LABEL = "Back"
|
||||
MENU_FORWARD_LABEL = "Forward"
|
||||
|
||||
# --- Sheet view history ---
|
||||
# Each entry is the element whose children are painted, which is what
|
||||
# app_state.current_sheet_parent holds; None is the file's root node. Elements are kept
|
||||
# rather than tree item ids because a rebuild of the tree hands out fresh ids, while the
|
||||
# elements themselves survive anything short of a delete or a new file.
|
||||
sheet_history = []
|
||||
history_index = -1 # position in sheet_history that is on screen
|
||||
_suppress_history = False # set while Back or Forward drives the tree selection
|
||||
_history_file_path = None # the file sheet_history was recorded against
|
||||
# --- Editor pane history ---
|
||||
# Each entry is (kind, element), naming what the pane was showing:
|
||||
#
|
||||
# (VIEW_SHEET, element) the sheet whose children are painted, which is what
|
||||
# app_state.current_sheet_parent holds; None is the
|
||||
# file's root node
|
||||
# (VIEW_PROPERTIES, comp) the Properties view of that component
|
||||
# (VIEW_LINKS, comp) the Link View of that component
|
||||
#
|
||||
# Opening a Properties or Link view is a move like opening a sheet, so Back from one
|
||||
# lands on what the pane showed before it rather than skipping past to the sheet before
|
||||
# that, and Forward brings it back.
|
||||
#
|
||||
# Elements are kept rather than tree item ids because a rebuild of the tree hands out
|
||||
# fresh ids, while the elements themselves survive anything short of a delete or a new
|
||||
# file.
|
||||
view_history = []
|
||||
history_index = -1 # position in view_history that is on screen
|
||||
_suppress_history = False # set while Back or Forward restores an entry
|
||||
_history_file_path = None # the file view_history was recorded against
|
||||
|
||||
# Filled in once the widgets exist. History can be recorded before then, so every
|
||||
# function that touches them checks first.
|
||||
@@ -204,6 +221,7 @@ def show_properties(comp):
|
||||
on_dirty_change=refresh_dirty_display,
|
||||
)
|
||||
properties_panel.frame.pack(fill=tk.BOTH, expand=True)
|
||||
record_visit(VIEW_PROPERTIES, comp)
|
||||
app_state.set_status(f"Properties: {comp.get('name', '?')}")
|
||||
|
||||
|
||||
@@ -246,6 +264,7 @@ def show_link_view(comp):
|
||||
on_close=dismiss_link_view,
|
||||
)
|
||||
link_panel.frame.pack(fill=tk.BOTH, expand=True)
|
||||
record_visit(VIEW_LINKS, comp)
|
||||
app_state.set_status(f"Link View: {comp.get('name', '?')}")
|
||||
|
||||
|
||||
@@ -488,7 +507,7 @@ def on_tree_select(event):
|
||||
|
||||
# Remembered so undo/redo and tidy can repaint this same sheet from the DOM.
|
||||
app_state.current_sheet_parent = element
|
||||
record_sheet_visit(element)
|
||||
record_visit(VIEW_SHEET, element)
|
||||
|
||||
if element is not None:
|
||||
components_to_render = [child for child in element if child.tag == "comp"]
|
||||
@@ -1289,27 +1308,50 @@ def _tree_item_for_element(element):
|
||||
return None
|
||||
|
||||
|
||||
def _prune_sheet_history():
|
||||
"""Drops entries whose sheet no longer exists, keeping the position on screen."""
|
||||
def _entry_is_reachable(entry):
|
||||
"""Whether a history entry still names something that could be shown.
|
||||
|
||||
A sheet entry needs a tree node to select, which is what paints it. A panel entry
|
||||
needs only its component, since a panel takes the whole pane and is built straight
|
||||
from the document.
|
||||
"""
|
||||
kind, element = entry
|
||||
if kind == VIEW_SHEET:
|
||||
return _tree_item_for_element(element) is not None
|
||||
return _component_in_document(element)
|
||||
|
||||
|
||||
def _prune_view_history():
|
||||
"""Drops entries that can no longer be shown, keeping the position on screen."""
|
||||
global history_index
|
||||
|
||||
kept = []
|
||||
new_index = -1
|
||||
for position, element in enumerate(sheet_history):
|
||||
if _tree_item_for_element(element) is None:
|
||||
for position, entry in enumerate(view_history):
|
||||
if not _entry_is_reachable(entry):
|
||||
continue
|
||||
kept.append(element)
|
||||
|
||||
# Dropping an entry can leave the two either side of it naming the same view,
|
||||
# and a Back that visibly does nothing is worse than a shorter trail. The
|
||||
# position still lands on whichever of the two it was on, since they are the
|
||||
# same view either way.
|
||||
if kept and kept[-1][0] == entry[0] and kept[-1][1] is entry[1]:
|
||||
if position <= history_index:
|
||||
new_index = len(kept) - 1
|
||||
continue
|
||||
|
||||
kept.append(entry)
|
||||
if position <= history_index:
|
||||
new_index = len(kept) - 1
|
||||
|
||||
sheet_history[:] = kept
|
||||
view_history[:] = kept
|
||||
history_index = min(new_index, len(kept) - 1)
|
||||
|
||||
|
||||
def update_history_controls():
|
||||
"""Greys out Back and Forward when there is nothing to walk to in that direction."""
|
||||
can_back = history_index > 0
|
||||
can_forward = 0 <= history_index < len(sheet_history) - 1
|
||||
can_forward = 0 <= history_index < len(view_history) - 1
|
||||
|
||||
for button, enabled in ((btn_back, can_back), (btn_forward, can_forward)):
|
||||
if button is not None:
|
||||
@@ -1324,19 +1366,20 @@ def update_history_controls():
|
||||
)
|
||||
|
||||
|
||||
def reset_sheet_history():
|
||||
def reset_view_history():
|
||||
"""Starts the history over, for a newly opened file."""
|
||||
global history_index
|
||||
sheet_history.clear()
|
||||
view_history.clear()
|
||||
history_index = -1
|
||||
update_history_controls()
|
||||
|
||||
|
||||
def record_sheet_visit(element):
|
||||
"""Adds a freshly opened sheet to the history, dropping anything ahead of it.
|
||||
def record_visit(kind, element):
|
||||
"""Adds a freshly opened view to the history, dropping anything ahead of it.
|
||||
|
||||
Called from the tree selection handler, so it sees every way a sheet is opened.
|
||||
Back and Forward suppress it: walking the history must not rewrite it.
|
||||
Every way of filling the editor pane comes through here: the tree selection handler
|
||||
for a sheet, and the two panels as they are put up. Back and Forward suppress it,
|
||||
because walking the history must not rewrite the trail it is following.
|
||||
"""
|
||||
global history_index, _history_file_path
|
||||
|
||||
@@ -1347,70 +1390,89 @@ def record_sheet_visit(element):
|
||||
# whether or not the tree rebuild that came with it was announced.
|
||||
if app_state.current_file_path != _history_file_path:
|
||||
_history_file_path = app_state.current_file_path
|
||||
sheet_history.clear()
|
||||
view_history.clear()
|
||||
history_index = -1
|
||||
|
||||
if 0 <= history_index < len(sheet_history) and sheet_history[history_index] is element:
|
||||
# Opening what is already on screen is not a move. Compared by identity, since two
|
||||
# sheets are the same sheet only when they are the same element.
|
||||
if 0 <= history_index < len(view_history):
|
||||
current_kind, current_element = view_history[history_index]
|
||||
if current_kind == kind and current_element is element:
|
||||
return
|
||||
|
||||
del sheet_history[history_index + 1:]
|
||||
sheet_history.append(element)
|
||||
if len(sheet_history) > HISTORY_LIMIT:
|
||||
del sheet_history[0]
|
||||
history_index = len(sheet_history) - 1
|
||||
del view_history[history_index + 1:]
|
||||
view_history.append((kind, element))
|
||||
if len(view_history) > HISTORY_LIMIT:
|
||||
del view_history[0]
|
||||
history_index = len(view_history) - 1
|
||||
update_history_controls()
|
||||
|
||||
|
||||
def _show_sheet_history_entry(position):
|
||||
"""Opens the sheet the history holds at this position, without recording the move."""
|
||||
def _show_history_entry(position):
|
||||
"""Puts back what the pane held at this position, without recording the move.
|
||||
|
||||
The pane is free by the time this runs: both ways in ask about unsaved edits and
|
||||
take whatever panel is up back down first.
|
||||
"""
|
||||
global history_index, _suppress_history
|
||||
|
||||
element = sheet_history[position]
|
||||
item_id = _tree_item_for_element(element)
|
||||
if item_id is None:
|
||||
kind, element = view_history[position]
|
||||
if not _entry_is_reachable((kind, element)):
|
||||
return
|
||||
|
||||
tree = app_state.tree
|
||||
history_index = position
|
||||
was_selected = tree.selection() == (item_id,)
|
||||
|
||||
_suppress_history = True
|
||||
try:
|
||||
# Selecting the node is what paints the sheet, via on_tree_select.
|
||||
tree.selection_set(item_id)
|
||||
tree.focus(item_id)
|
||||
tree.see(item_id)
|
||||
if kind == VIEW_SHEET:
|
||||
_show_sheet_entry(element)
|
||||
elif kind == VIEW_PROPERTIES:
|
||||
show_properties(element)
|
||||
else:
|
||||
show_link_view(element)
|
||||
finally:
|
||||
_suppress_history = False
|
||||
|
||||
if was_selected:
|
||||
# ttk fires no selection event when the node is already the selected one, so
|
||||
# the repaint this entry is asking for has to be made here instead.
|
||||
app_state.current_sheet_parent = element
|
||||
wiresheet.render_current_sheet()
|
||||
|
||||
history_index = position
|
||||
update_history_controls()
|
||||
|
||||
|
||||
def _show_sheet_entry(element):
|
||||
"""Selects the tree node whose children are this sheet, which is what paints it."""
|
||||
tree = app_state.tree
|
||||
item_id = _tree_item_for_element(element)
|
||||
was_selected = tree.selection() == (item_id,)
|
||||
|
||||
tree.selection_set(item_id)
|
||||
tree.focus(item_id)
|
||||
tree.see(item_id)
|
||||
|
||||
if was_selected:
|
||||
# ttk fires no selection event when the node is already the selected one, so
|
||||
# the repaint this entry is asking for has to be made here instead. Leaving a
|
||||
# panel lands here every time: closing it puts the tree's own sheet back on
|
||||
# screen without the selection ever having moved.
|
||||
app_state.current_sheet_parent = element
|
||||
wiresheet.render_current_sheet()
|
||||
|
||||
|
||||
def go_back(event=None):
|
||||
"""View menu and toolbar: step to the previously opened sheet."""
|
||||
"""View menu and toolbar: step to whatever the editor pane held before this."""
|
||||
if not close_editor_panel():
|
||||
return "break"
|
||||
_prune_sheet_history()
|
||||
_prune_view_history()
|
||||
if history_index > 0:
|
||||
_show_sheet_history_entry(history_index - 1)
|
||||
_show_history_entry(history_index - 1)
|
||||
else:
|
||||
update_history_controls()
|
||||
return "break"
|
||||
|
||||
|
||||
def go_forward(event=None):
|
||||
"""View menu and toolbar: step back towards the most recently opened sheet."""
|
||||
"""View menu and toolbar: step back towards the most recently opened view."""
|
||||
if not close_editor_panel():
|
||||
return "break"
|
||||
_prune_sheet_history()
|
||||
if 0 <= history_index < len(sheet_history) - 1:
|
||||
_show_sheet_history_entry(history_index + 1)
|
||||
_prune_view_history()
|
||||
if 0 <= history_index < len(view_history) - 1:
|
||||
_show_history_entry(history_index + 1)
|
||||
else:
|
||||
update_history_controls()
|
||||
return "break"
|
||||
@@ -1437,9 +1499,9 @@ def on_tree_filled():
|
||||
|
||||
if app_state.current_file_path != _history_file_path:
|
||||
_history_file_path = app_state.current_file_path
|
||||
reset_sheet_history()
|
||||
reset_view_history()
|
||||
else:
|
||||
_prune_sheet_history()
|
||||
_prune_view_history()
|
||||
update_history_controls()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user