spacr.qt.widgets.dock

The left navigation dock: an icon, a name, and a category heading.

A row is a button with an icon and its name, always both. The only thing the pointer changes is the colour, and the explanation goes to the strip along the bottom of the window rather than into a popup.

WHAT WAS REMOVED, AND WHY EACH ONE WAS THE BUG. The dock this replaces was 1,116 lines across Sidebar and _DockRow in spacr.qt.app, and nearly all of it was machinery that existed to defeat itself:

  • a translucent slab painted in paintEvent — the “black box” of four separate commits, which turned out to be the dock painting itself rather than any stylesheet;

  • a per-row icon-size model (resting_icon_px, _place_icon, _set_icon_px, _forget_icon_sizes, _rest_every_icon) that grew and shrank icons under the pointer, which is what made hovering relayout the column and blink;

  • the name painted only while hovered, so a resting dock was a column of unlabelled glyphs;

  • a second, indented level of folded modules with its own expand state (_fold_children, _open_hosts), which is the “sub categories”.

None of that is here. A row is a button with an icon and its name, always both. The only thing the pointer changes is the colour.

WHERE THE EXPLANATION WENT. Not into a popup tooltip — those are explicitly unwanted — but into the strip along the bottom of the window, which already exists as spacr.qt.widgets.module_hint_bar and already holds the last hovered module for thirty seconds with its API and tutorial links. This dock only says which module is under the pointer, via Dock.module_hovered; the bar decides how to explain it.

WHAT IS KEPT, BECAUSE SOMETHING ELSE READS IT. Categories still collapse — the list is longer than a short screen. Rows still carry navKey and headers are still SidebarSection, because the theme, the tutorial script and the maturity tests all reach the dock through those names. And refresh_visibility() still applies the Alpha/Beta maturity filter and hides a heading whose modules are all filtered out, which is a separate reason for a row to be absent from its section being shut.

NOTHING HERE IMPORTS spacr.qt.app. The registry lives there and would be a circular import, so the rows, the icon lookup and the maturity predicate are all injected.

Classes

Dock

The navigation column: categories, each holding icon+name rows.

DockRow

One module: its icon, then its name, both always drawn.

SectionHeader

A category heading. A label rather than a button, because it is

Module Contents

class spacr.qt.widgets.dock.Dock(rows: Iterable[Row], icon_for: Callable[[str], object] | None = None, is_visible: Callable[[str], bool] | None = None, parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

The navigation column: categories, each holding icon+name rows.

Parameters:
  • rows – the modules to draw, in order, as (key, name, desc, section). Grouping IS ordering: a new heading starts whenever the section changes, so a row out of place draws its heading twice.

  • icon_for – optional key -> QIcon | None for the row icons.

  • is_visible – optional key -> bool maturity predicate. Injected rather than imported so this module does not depend on spacr.qt.app, which is what defines the registry.

  • parent – parent widget; ownership only.

Build the dock column: section headings with their module rows under them.

The container is made transparent rather than coloured. The application sheet carries a blanket QWidget { background-color: bg }, so any untagged container paints an opaque rectangle – and a plain widget holding a rounded panel is exactly that, a square of window colour behind rounded corners. Colouring it only changes which colour the rectangle is.

The rounded panel is a child frame rather than this widget’s own background, which satisfies both constraints at once: the container stays transparent where the theme pins it transparent, and the panel is still drawn.

Parameters:
  • rows – the module rows to list, in order.

  • icon_for – called with a key for that module’s icon.

  • is_visible – called with a key to decide whether to list it.

  • parent – parent widget, or None.

apply_theme() None[source]

Paint the rounded panel, and the one rule hover uses.

THE PANEL IS THE ONLY THING THAT PAINTS. Everything inside it is transparent on purpose: a title or a heading carrying a fill of its own would draw a square corner over the rounded one directly beneath it, which is the exact shape this was asked to stop being.

The three values come from HomePanelBox rather than being chosen again here – pane_surface('surface_alt'), border_soft and an 8 px radius – so the dock and that box stay the same material when either is restyled.

clipped_items() list[source]

Rows whose name had to be shortened to fit.

Empty in a healthy layout, and a test asserts that.

eventFilter(watched, event)[source]

Light a heading under the pointer, and toggle it on release.

ON RELEASE, NOT PRESS: a press that toggled would fire while the pointer was still down, so a drag that began on a heading and ended elsewhere would still have shut the section.

The hover state is a PROPERTY rather than a colour set from here, because the stylesheet is the one place that decides what the dock looks like.

expand_host(host_key: str) None[source]

Accepted and does nothing: there are no folded child rows.

The second level was removed on request. This remains so the callers that opened a host on navigation do not have to know that, and because a method that quietly disappeared would fail at the call site rather than here, where the reason is written down.

fitting_width() int[source]

Width that shows the longest visible name in full, within bounds.

Font scale moves both bounds; the widest visible row moves the result between them. Public because the locked dock re-applies it after being re-parented out of the drawer, which had resized it.

host_is_expanded(host_key: str) bool[source]

Always False: there are no folded child rows to expand.

hovered_row() DockRow | None[source]

The row under the pointer, or None.

leaveEvent(event)[source]

The pointer left the column: no row is lit.

The rows’ own Leave covers a pointer stepping between them; this covers one that leaves the dock altogether, including straight off the bottom row onto the empty stretch below it, where no other row will ever be entered.

refresh_icons() None[source]

Re-ask the provider for every row’s icon.

A QIcon bakes its pixmap when it is built, so re-applying the stylesheet does not recolour icons that already exist. Every icon is set once at one size and never resized — the old dock’s growing and shrinking icons are what made hover relayout the column.

refresh_visibility() None[source]

Show a row if its category is open AND maturity allows it.

Two separate reasons for a row to be absent, and they are kept separate: a shut section hides rows that are perfectly mature, and the Alpha/Beta filter hides rows inside an open one. A heading stays put whether its section is open or shut — it is what you click to open it — and hides only when every module beneath it is filtered out.

row_height() int[source]

The height of a row, or 0 if the dock is empty.

rows() List[DockRow][source]

Every module row, in the order they are drawn.

section_is_open(section: str) bool[source]

Whether section’s rows are currently shown.

sections() List[str][source]

Every category heading, in the order they are drawn.

sync_hover(entered=None) str | None[source]

Report which row the pointer is on.

The old dock needed this to repair hover state it had broken by relaying out under the pointer. Nothing relayouts now, so this only answers the question. Kept because tools/diagnose_dock.py asks.

toggle_section(section: str) bool[source]

Open a closed category or close an open one. Returns the new state.

class spacr.qt.widgets.dock.DockRow(key: str, name: str, desc: str = '', parent=None)[source]

Bases: spacr.qt.widgets.eliding.ElidingPushButton

One module: its icon, then its name, both always drawn.

The row paints nothing of its own — the colour comes from the stylesheet Dock installs, so there is one place that decides what hover looks like and no paintEvent to disagree with it.

Build one module row.

Parameters:
  • key – the module’s registry key. Stamped onto the row three times over – as navKey, as moduleAppKey and as the attribute – because three different readers ask for it: the icon refresh, the bottom hint strip’s filter, and this module.

  • name – the module’s name, drawn beside the icon and set as the accessible name so a screen reader still gets the whole of it when the column elides it.

  • desc – the one-line summary. Not drawn here at all: it is stamped as moduleSummarySource for the strip along the bottom of the window, which is where descriptions go.

  • parent – parent widget.

enterEvent(event)[source]

Light the row as the pointer arrives.

Parameters:

event – the Qt enter event.

is_hovered() bool[source]

Whether the pointer is currently on this row.

leaveEvent(event)[source]

Drop the highlight as the pointer leaves.

Parameters:

event – the Qt leave event.

class spacr.qt.widgets.dock.SectionHeader(section: str, parent=None)[source]

Bases: PySide6.QtWidgets.QLabel

A category heading. A label rather than a button, because it is already styled as a heading and a button would have to be un-styled back into one; the click arrives through Dock.eventFilter().

Parameters:
  • section – the category name. Shown as the heading AND kept on the sectionName property, which is how the dock finds the rows a click should fold – the visible text is translated, the property is not.

  • parent – parent widget; ownership only.

Build one dock section heading.

The section name is also stored under its legacy property name, which is what the theme styles and what the maturity test looks headers up by.

Parameters:
  • section – the section’s name.

  • parent – parent widget, or None.