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.

DockEdge

The strip along the dock's right edge that drags its width.

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.

clamp_width(width: int) → int[source]

width held between DRAG_MIN and DRAG_MAX.

Parameters:

width – logical pixels.

clipped_items() → list[source]

Rows whose name had to be shortened to fit.

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

column_width() → int[source]

The width the column wears: the one the user dragged, else fitting.

A width the user chose is kept through every refresh, every hide and show, and every session; with none stored the column fits its longest name, as fitting_width() has always done.

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.

Parameters:
  • watched – the object the event is for; only a SectionHeader is handled here.

  • event – the event; Enter and Leave set the header’s hovered property, and a left-button release toggles its section.

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.

Parameters:

host_key – the app key of the host; ignored.

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.

Parameters:

host_key – the app key of the host; ignored.

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.

Parameters:

event – the leave event, passed to the base class after every row is unlit.

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.

THE SIZE IS FIXED PER SCALE, NOT PER ROW STATE. The old dock grew and shrank an icon on hover, which relayed out the whole column under the pointer; that is what ICON_PX being one number stops. It still has to follow the interface scale, so the base is recorded on each row and re-derived from there – see spacr.qt.preferences._set_scaled_icon_size() for why it is never recomputed from the size the row is already wearing.

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.

Parameters:

section – the category name.

sections() → List[str][source]

Every category heading, in the order they are drawn.

set_column_width(width: int, *, remember: bool = True) → int[source]

Give the column width within its bounds and maybe remember it.

Parameters:
  • width – logical pixels; 0 or less goes back to the fitting width and forgets the dragged one.

  • remember – store it for the next session.

Returns:

the width applied.

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.

Parameters:

section – the category name to open or close.

class spacr.qt.widgets.dock.DockEdge(dock: Dock, parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

The strip along the dock’s right edge that drags its width.

Dragging it sets the width of the dock while the dock is shown. A sibling of the dock’s slot rather than a splitter handle, because the dock is a fixed-width layout member and everything that measures it (the drawer, the backdrop, the fitting width) reads that fixed width. Dragging sets it; releasing stores it; a double-click forgets it and the column fits its names again. It is shown and hidden with the dock, so a hidden dock has no edge to catch.

Parameters:
  • dock – the Dock it resizes.

  • parent – parent widget; ownership only.

Build the edge for dock; hidden until the dock is shown.

enterEvent(event) → None[source]

Light the line up under the pointer.

Parameters:

event – the event.

is_collapsed() → bool[source]

Whether the dock slot is collapsed; the handle stays visible.

keyPressEvent(event) → None[source]

Let a keyboard user activate the same collapse control.

Parameters:

event – key press; Space, Return and Enter toggle the dock.

leaveEvent(event) → None[source]

Put the line back.

Parameters:

event – the event.

mouseDoubleClickEvent(event) → None[source]

Forget the dragged width; the column fits its names again.

Parameters:

event – the event.

mouseMoveEvent(event) → None[source]

Follow the pointer, within the dock’s bounds; stored on release.

Parameters:

event – the event.

mousePressEvent(event) → None[source]

Begin a drag from the dock’s present width.

Parameters:

event – the event.

mouseReleaseEvent(event) → None[source]

End the drag and remember the width it left.

Parameters:

event – the event.

paintEvent(_event) → None[source]

Draw the one-pixel line a splitter handle draws.

Parameters:

_event – the paint event; the whole strip is drawn.

retranslate_dynamic_content(language=None) → None[source]

Explain the current collapse action and the resize gesture.

set_collapsed(collapsed: bool) → None[source]

Hide or restore the slot without changing its dock’s saved width.

Parameters:

collapsed – true hides the slot; false restores its contents.

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.