diff --git a/README.md b/README.md index 2cfd393..afda58e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app_state.py b/app_state.py index bc3420b..d573ecd 100644 --- a/app_state.py +++ b/app_state.py @@ -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 diff --git a/main.py b/main.py index 41e7b98..8f8158c 100644 --- a/main.py +++ b/main.py @@ -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()