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:
2026-09-07 19:39:45 +03:00
co-authored by Claude Opus 5
parent b119e02b9e
commit 1546c1c8e0
3 changed files with 160 additions and 72 deletions
+123 -61
View File
@@ -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:
return
# 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()