Skip to content

feat(ui): lists-left tabbed/collapsible dock panel (Slice 3 headless backbone)#4

Open
dustenhubbard wants to merge 11 commits into
feat/ui-v1from
feat/ui-v1-lists-panel
Open

feat(ui): lists-left tabbed/collapsible dock panel (Slice 3 headless backbone)#4
dustenhubbard wants to merge 11 commits into
feat/ui-v1from
feat/ui-v1-lists-panel

Conversation

@dustenhubbard

Copy link
Copy Markdown
Owner

UI v1 Slice 3 — lists-left tabbed/collapsible panel (headless backbone)

Behavior backbone for Slice 3. The look is intentionally out of scope and needs interactive visual sign-off (checklist below). Built test-first; every step reviewed by an adversarial 5-lens council (correctness / simplicity / alternative / reachability / muscle-memory). Held off main.

What changed

  • Global QSettings defaults (default_settings.py): lists_panel_collapsed (False) and open_tables (["object"]) — global (resolve via Series.qsettings_defaults), not per-series, and untouched by the per-series options prune.
  • Tabbed left group (manager.py): setDockNestingEnabled(True) + tabifyDockWidget so the five list docks (object/trace/section/ztrace/flag) share one left tab group.
  • closeAll skip-bug fix (manager.py): closing a dock removes it from the very list closeAll iterates → iterate a copy so no dock is skipped. (Pre-existing bug.)
  • Auto-open promoted lists (field_widget_1_base.py): restore the saved open_tables (default Object) on series open; skip the welcome series and unknown/stale types. open_tables is kept in sync as docks open/close; programmatic teardown (closeAll) suppresses the sync so a series switch preserves the preference. Always writes back a new list (the qsettings defaults are a shallow copy).
  • Collapse toggle (menubar.py + main_window.py): a "Collapse lists panel" View-menu checkbox (no accelerator → no shortcut collision) that hides/shows the docks via setHidden (collapse ≠ close, so open_tables is untouched) and persists.
  • Restore + sync on open / restart (main_window.py): _applyListsPanelState (after createMenuBar) re-applies the collapsed state and syncs the checkbox; checkActions keeps the checkbox in sync alongside the other toggles. State lives in global QSettings, so it survives the in-app restart (which recreates the window and re-enters openSeries).

Headless evidence (offscreen, QT_QPA_PLATFORM=offscreen)

  • 82 tests pass in one fresh run (full suite). New tests/test_lists_panel.py (B1–B5 + an end-to-end open→collapse→restart lifecycle) and a session-autouse QSettings-isolation fixture in tests/conftest.py (redirects HOME/XDG/APPDATA + setPath, asserts the .conf resolves under a temp dir, so a run can never touch real preferences).
  • A Step-0 gate first proved offscreen reliably reports dockWidgetArea / tabifiedDockWidgets / isHidden() (and that isVisible() is unreliable when the window is never shown — so assertions use isHidden()).
  • Each step's council had full lens coverage; surviving findings were fixed test-first — notably the B1 shallow-copy alias (returned lists are never mutated in place; written back fresh) and a guard for unknown/stale open_tables types so a corrupted setting can't crash field loading.

⚠️ Needs interactive visual sign-off (not headlessly verifiable)

  • Left panel renders as tabs (Objects / Traces / Z-traces / …) and reads cleanly.
  • The collapse toggle visibly reclaims the field viewport, and the re-open path (View → Collapse lists panel) is discoverable.
  • Object list still promoted / default-on; muscle-memory intact (floating palette, in-field tracing shortcuts, scrollable sections).
  • Live light/dark theme toggle still applies cleanly with the panel present.
  • No visual crowding between the (future) right-biased palette and the brightness/contrast + section-increment buttons.

Out of scope (deliberately deferred / interactive)

  • Tools-right palette nudge — the mode/tool buttons are already right-anchored (mode_x = 0.99); biasing the trace-selection strip is a one-line cosmetic tweak that risks overlapping the brightness/contrast buttons, so it's left for an interactive pass with eyes on it.
  • A custom chevron re-open handle (the View-menu checkbox + Qt dock buttons cover re-open for now).

Follow-up noted (not fixed here — avoids scope creep)

  • Series.qsettings_defaults = default_settings.copy() is a shallow copy, so all mutable defaults are aliased. This slice is safe (it never mutates a returned list in place), but a deepcopy would harden every setting — worth a small standalone fix.

STATUS delta (for the notes repo, once its in-flight rebase is resolved)

  • Slice 2 (tool icons) ✅ done. Slice 3 backbone ✅ landed here, awaiting visual sign-off. Next: Slice 4 (right-edge 3D-scene slide-over).

Do not merge — held for interactive visual sign-off. An open PR (or a clear stop report) is the finalized state for this autonomous run.

Add lists_panel_collapsed (False) and open_tables (["object"]) to the global
default_settings dict so they resolve via Series.qsettings_defaults
(QSettings("KHLab","PyReconstruct")), not the per-series options dict — the
series load-prune (series.py:440-444) therefore never touches them. Backs UI v1
Slice 3 (lists-left tabbed/collapsible panel).

Tests (offscreen): defaults registered and global-not-per-series/internal,
getOption returns defaults when unset, setOption/getOption round-trip, and
cross-instance resolution via global QSettings. Adds tests/conftest.py
session-autouse fixture isolating HOME/XDG_CONFIG_HOME/APPDATA + QSettings paths
to a temp dir so the suite never touches real preferences.
Tab the on-demand list docks (object/trace/section/ztrace/flag) into a single
left dock group: enable dock nesting in TableManager and tabify each new dock
onto the first already-open list dock. Backs UI v1 Slice 3 (lists-left panel).

Also fix a pre-existing mutate-while-iterate bug in closeAll: each close() fires
closeEvent, which removes the table from the same self.tables[name] list being
iterated, so a plain loop skipped about half the tables. Iterate a copy.

Tests (offscreen): closeAll closes every table for a multi-table list; newTable
places each dock in the left area and groups them via tabifiedDockWidgets; a
single dock does not crash; dock nesting is enabled on init. Adds a shared qapp
fixture to conftest.
A B2-council lens raised that anchoring newTable to the first open dock could
fragment the group if the object (anchor) dock closes. It cannot: once docks are
tabified they share one group, so any remaining dock is an equivalent anchor.
Add a regression test that closes the anchor and confirms a later dock still
joins the single group.
On series open, restore the saved list docks (default Object) via a new
FieldWidgetBase._restoreOpenTables, skipping the welcome series. Keep the global
open_tables option in sync: TableManager.newTable adds a type, and
DataTable.closeEvent -> TableManager.onTableClosed drops it when the last dock of
that type closes. closeAll suppresses the sync so a series switch keeps the
user's saved preference. Each update writes back a new list (the qsettings
defaults are a shallow copy, so the returned list is never mutated in place).

Tests (offscreen): sync add/idempotent, remove-when-last-closes,
keep-if-another-open, suppress during closeAll, no corruption of the global
default, restore opens saved tables, welcome series opens nothing.
A B3-council lens noted that a stale or corrupted open_tables entry (e.g. a list
type removed in a future version) would raise KeyError in newTable and crash
field loading. Skip unknown types in _restoreOpenTables instead.

Test (offscreen): restore with an invalid type opens only the valid lists.
Add a "Collapse lists panel" checkbox action (no accelerator, so no shortcut
collision) to the View menu, wired to MainWindow.toggleListsPanel.
TableManager.setListsPanelCollapsed hides or shows every list dock and persists
lists_panel_collapsed; collapse HIDES (not closes) the docks, so open_tables is
untouched.

Tests (offscreen): listsPanelCollapsed reads the option; setListsPanelCollapsed
hides/shows all docks and persists; collapse leaves open_tables intact;
MainWindow.toggleListsPanel flips both the state and the menu checkbox.
Apply the saved lists_panel_collapsed state to the docks and sync the View-menu
checkbox after the menu is rebuilt on series open (MainWindow._applyListsPanelState,
called in openSeries after createMenuBar, once docks are restored), and re-sync
the checkbox in checkActions alongside the other toggle actions. The state lives
in global QSettings, so it survives the in-app restart (which recreates the
window and re-enters openSeries).

Resolves the B4-council findings (restore-on-open and checkbox sync were B5's
remit).

Tests (offscreen): apply-collapsed hides docks and checks the box; apply-expanded
shows docks and unchecks; collapsed state persists across a simulated restart.
Compose the slice: each "boot" is a fresh series + manager + field + window
reading the same global QSettings (mirroring run.py recreating MainWindow).
Proves open_tables and lists_panel_collapsed both persist and are re-applied on
restart, and the View-menu checkbox stays in sync.
Assert the View-menu checkbox updates in real time on toggle (Boot 1), and tighten
the post-restart restoration check to assert exactly one Object dock.
@dustenhubbard dustenhubbard added this to the v1.30.0 milestone Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant