spacr.qt.widgets.collapsible_splitter

Drag an edge to resize a pane; collapsing is where the drag stops.

Two requests that turn out to be one: every container collapses (“whenever anything is collapsed it should auto lock to the bottom of the container it is in”), and every container can be “expanded or shrinkable by draging the edges”. A QSplitter already does both – a handle to drag, and a child that can be taken down to nothing – so both halves live here as ONE mechanism, and anything that collapses in a splitter also resizes, and the other way round.

THE THREE PIECES, AND WHAT SLICE C REUSES

CollapsibleSplitter

A QSplitter whose children are registered as named panes:

split = CollapsibleSplitter(Qt.Vertical, persist_key="mask::shell")
split.add_pane(card, "Figures", folder=card.folder, extent=420)
split.add_pane(console_wrap, "Console", folder=console_folder)
split.add_pane(settings, "Settings", mode=EDGE, fold_key="mask/Settings")

A pane is one of three kinds:

  • HEADER – it has a Folder (a clickable heading over a body). Collapsed, its body is hidden, the pane shrinks to its heading, and the heading sits at the BOTTOM of the room the pane is given (lock_folded_to_bottom()). This is the kind for anything with a name to click: the console, System, a figure panel.

  • EDGE – it collapses to nothing and the handle beside it becomes the strip that brings it back: click the handle to hide or show the pane, drag it to resize. The kind for a column with no heading of its own (the settings column, which collapses to the left).

  • plain (mode=None, no folder) – resizable, never collapsed.

Dragging an expanded pane below its minimum collapses it (Qt’s own collapse, turned into the pane’s folded state). Collapsed HEADER panes are held at their heading’s height, so a window resize never hands them room they would show as a gap. When nothing in the splitter is left expanded, the FIRST collapsed pane takes the spare room and, locked to the bottom, stacks every heading at the bottom of the container.

Sizes are remembered per pane NAME (not per index, so adding a pane to a screen does not scramble the old layout) under persist_key, in the preferences store. Only what the user dragged is stored; a collapse made on the user’s behalf is never stored as a size.

API: add_pane(widget, name, *, folder=None, mode=None, stretch=1, extent=0, minimum=None, focus=False, fold_key="", index=None) -> Pane; pane(name), panes(), is_collapsed(name), set_collapsed(name, collapsed, *, by_user=False), toggle_pane(name, *, by_user=True), rebalance(grow=None), and the pane_toggled(name, collapsed, by_user) signal. A widget that is later re-wrapped in a container which is put in its place (the settings search strip does this to the settings column) keeps its registration: the container inherits it.

FocusCollapse

The automatic half. Some widgets are FOCUS widgets (a live preview, the figures panel): while any of them is on screen, the TARGET panes collapse (console, System, the buttons, the settings column), and when the last one goes they come back. It never fights the user: a target the user expands by hand is PINNED open and left alone until the VIEW ends. See the class for what a view is.

lock_folded_to_bottom()

For a folded panel that is NOT in a splitter (a section in a scroll area, a figure tile in a column): keeps its heading at the bottom of whatever room its layout gives it, which is the “auto lock to the bottom” rule on its own.

FoldSection, CollapsibleSplitter.add_section(), fold_card()

Slice C’s use of the three above inside the screens: a heading that folds a table, a figure, a side panel; the same heading in a splitter, so the section also trades room with its neighbours by dragging; and the same fold on a titled card. Folding never shows or builds a panel its owner keeps hidden.

Classes

CollapsibleSplitter

A splitter whose panes resize by dragging and collapse at the limit.

FocusCollapse

Collapse the surroundings while a focus widget is on screen.

FoldSection

A body under a clickable heading; folded, the heading sits at the bottom.

Pane

One registered child of a CollapsibleSplitter.

Functions

fold_card(card[, name, persist_key])

Make a titled Card fold by its title.

get_pane_extents(→ dict)

The sizes the user dragged the panes of persist_key to.

lock_folded_to_bottom(→ None)

Keep container's heading at the bottom of its room while folded.

set_pane_extents(→ None)

Remember {pane name: pixels} for persist_key.

splitter_of(→ Optional[CollapsibleSplitter])

The CollapsibleSplitter widget sits directly in, if any.

Module Contents

class spacr.qt.widgets.collapsible_splitter.CollapsibleSplitter(orientation, parent=None, *, persist_key: str = '')[source]

Bases: PySide6.QtWidgets.QSplitter

A splitter whose panes resize by dragging and collapse at the limit.

See the module docstring for the pane kinds and the API.

Parameters:
  • orientation – Qt.Vertical stacks the panes.

  • parent – parent widget.

  • persist_key – where the dragged sizes are remembered, e.g. "mask::shell"; empty remembers nothing (what a test wants).

Build an empty splitter; add panes with add_pane().

addWidget(widget: PySide6.QtWidgets.QWidget) → None[source]

Add as Qt does, and let a wrapping container inherit a pane.

Parameters:

widget – the child to add.

add_pane(widget: PySide6.QtWidgets.QWidget, name: str, *, folder=None, mode: str | None = None, stretch: int = 1, extent: int = 0, minimum: int | None = None, focus: bool = False, fold_key: str = '', index: int | None = None, hint: str = '') → Pane[source]

Register widget as the pane name, adding it if need be.

Parameters:
  • widget – the pane. Added at index (or the end) unless it is already a child of this splitter.

  • name – stable English name, used for the stored size and the handle’s tooltip; translated where it is shown.

  • folder – the heading’s Folder; makes it a HEADER pane.

  • mode – EDGE for a pane that collapses to nothing.

  • stretch – 0 keeps its own size as the splitter grows.

  • extent – the size to open at when the user never dragged it.

  • minimum – its minimum while open; None keeps the widget’s.

  • focus – showing the widget also opens the pane.

  • fold_key – remembers an EDGE pane’s collapse across restarts.

  • hint – what dragging the pane’s handle does, for its tooltip.

Returns:

the Pane.

add_section(widget: PySide6.QtWidgets.QWidget, name: str, *, persist_key: str = '', stretch: int = 1, extent: int = 0, minimum: int | None = None, focus: bool = False, index: int | None = None, actions=()) → FoldSection[source]

Wrap widget in a FoldSection and add it as a pane.

The short way to give a splitter child a heading that folds it: the section’s Folder makes it a HEADER pane, so it resizes by its edge, collapses at the limit of a drag, and folded, is its heading at the bottom of its room.

Parameters:
  • widget – the content. Must not already be in this splitter.

  • name – stable English name for the heading and the stored size.

  • persist_key – "<module>/<section>" to remember a user fold.

  • stretch – 0 keeps its own size as the splitter grows.

  • extent – the size to open at when the user never dragged it.

  • minimum – its minimum while open.

  • focus – showing it also opens it.

  • index – where to insert it; the end by default.

  • actions – widgets for the right-hand end of the heading.

Returns:

the section; its pane is self.pane(name).

createHandle()[source]

Every handle is a _PaneHandle.

eventFilter(watched, event) → bool[source]

Follow a pane being shown or hidden by its owner.

Parameters:
  • watched – a pane’s widget.

  • event – the event; only show and hide are read.

Returns:

False, so the event goes on as usual.

extents() → dict[source]

{pane name: size to open at} for every registered pane.

insertWidget(index: int, widget: PySide6.QtWidgets.QWidget) → None[source]

Insert as Qt does, and let a wrapping container inherit a pane.

Parameters:
  • index – where to insert it.

  • widget – the child to insert.

is_collapsed(name: str) → bool[source]

Whether the pane name is collapsed; False for no such pane.

Parameters:

name – the pane’s name.

pane(name: str) → Pane | None[source]

The pane called name, or None.

Parameters:

name – the name it was added under.

panes() → list[source]

Every registered pane, in the order registered.

rebalance(grow: Pane | None = None, fresh: bool = False, refit: bool = False) → list[source]

Give every visible pane its share and return the new sizes.

Collapsed HEADER panes get their heading, collapsed EDGE panes get nothing, panes with stretch 0 get their own size, and the rest of the room is shared by the others in proportion to their current size. A pane being opened (grow), one too small to have a real size, and every pane on the first layout (fresh) are sized from their remembered size instead. refit sizes a fixed-size pane that wraps to the height its width now needs.

Returns:

the sizes set, or [] when there was nothing to lay out.

resizeEvent(event) → None[source]

A new width can wrap a row onto another line; make room for it.

A resize of the window is not a drag – no handle is held – so re-fitting here fights nobody.

Parameters:

event – the resize event.

set_collapsed(name: str, collapsed: bool, *, by_user: bool = False) → bool[source]

Collapse or open the pane name.

Parameters:
  • name – the pane’s name.

  • collapsed – True to collapse it.

  • by_user – True when a person asked for it. Only such a change is remembered, and only such a change pins a pane against FocusCollapse.

Returns:

whether the pane is now in the state asked for.

showEvent(event) → None[source]

Lay the panes out from their remembered sizes, once.

Parameters:

event – the show event.

toggle_pane(name: str, *, by_user: bool = True) → bool[source]

Flip the pane name; returns whether it is now collapsed.

Parameters:
  • name – the pane’s name.

  • by_user – whether a person asked for it; see set_collapsed().

class spacr.qt.widgets.collapsible_splitter.FocusCollapse(parent=None)[source]

Bases: PySide6.QtCore.QObject

Collapse the surroundings while a focus widget is on screen.

Requested: “when a live preview opens, it takes the whole screen height: the console, the System container and the button section below System auto-collapse, and the settings collapse to the left”, and the same whenever figures appear.

FOCUS widgets are watched (watch()); while ANY of them is shown the TARGET panes (target()) are collapsed, and when the last is hidden the targets this collapsed are opened again. A target that was already collapsed – by the user, or remembered from last session – is left exactly as it is, both ways.

NEVER FIGHTS THE USER. A target the user opens by hand (clicking its heading or its handle, or dragging it open) is PINNED: nothing here collapses it again until the view ends. A target the user collapses by hand is theirs too, and is not re-opened when the focus goes.

A VIEW is one visit to one screen: it begins when the screen is shown (begin_view(), from its showEvent) and ends when it is hidden because the user went to another screen (end_view(), from its hideEvent). Minimising the window does not end it – the owner passes only non-spontaneous events. Pins last for the view: returning to the screen with its live preview still open collapses the surroundings again, which is what a fresh arrival at that layout would get.

Parameters:

parent – owner, normally the screen.

Watch nothing and collapse nothing until told what to.

begin_view() → None[source]

The screen came on: forget last view’s pins and apply the rule.

end_view() → None[source]

The user left the screen: their pins for this view are spent.

eventFilter(watched, event) → bool[source]

A focus widget was shown or hidden.

Parameters:
  • watched – the focus widget.

  • event – the event; only show and hide are read.

Returns:

False, so the event goes on as usual.

is_active() → bool[source]

Whether any focus widget is shown.

is_pinned(splitter, name) → bool[source]

Whether the user opened name by hand during this view.

Parameters:
  • splitter – the splitter holding the pane.

  • name – the pane’s name.

refresh(*, force: bool = False) → bool[source]

Collapse or restore the targets for what is shown now.

Acts on a CHANGE (or when force), so a figure arriving while a preview is already open does not re-collapse what the user opened.

Returns:

whether a focus widget is shown.

target(splitter: CollapsibleSplitter, name: str) → None[source]

Collapse the pane name of splitter while a focus is shown.

Parameters:
  • splitter – the splitter holding the pane.

  • name – the pane’s name.

watch(widget: PySide6.QtWidgets.QWidget) → None[source]

Treat widget being shown as a reason to collapse the targets.

Only its hidden flag is read – it is never shown, measured or built, so a lazily-built panel stays unbuilt until its owner shows it.

Parameters:

widget – the focus widget.

class spacr.qt.widgets.collapsible_splitter.FoldSection(body: PySide6.QtWidgets.QWidget, name: str, parent=None, *, persist_key: str = '', stretch: int = 1, actions=(), follow_body: bool = True, folded: bool = False)[source]

Bases: PySide6.QtWidgets.QWidget

A body under a clickable heading; folded, the heading sits at the bottom.

Requested: “any sections inside the figures or live preview containers should also be colapseable … evey GUI element be it a figure or a table”. This is the one way a section inside a screen gets a fold control, so every one of them looks and behaves alike: an arrow and a name above the body (the same Folder the console and System use), a click folds it, and a folded section is its heading locked to the BOTTOM of whatever room it is given (lock_folded_to_bottom()).

THE BODY’S OWN VISIBILITY STAYS ITS OWNER’S. The fold hides an inner holder, never the body, so unfolding cannot show a panel its owner hid or build a panel that is built on first show. The other way round, the section follows its body: while the owner keeps the body hidden the whole section – heading included – is hidden too, and it comes back when the owner shows the body. Only the body’s hidden flag is read for that, so nothing is shown, measured or built to find out.

Parameters:
  • body – the section’s content. Re-parented into the section.

  • name – stable English name; the heading shows it translated.

  • parent – parent widget.

  • persist_key – "<module>/<section>"; given, a fold the USER makes survives a restart (a fold made on their behalf never does).

  • stretch – the body’s stretch inside the section.

  • actions – widgets put at the right-hand end of the heading row (a Refresh button, a count); they stay visible while folded.

  • follow_body – False keeps the section shown whatever the body’s own flag says.

  • folded – start folded until the user opens it; with a persist_key the opening is remembered.

Variables:
  • heading – the heading label (object name FoldHeading).

  • folder – the section’s Folder; HELD, it owns the click filter.

  • body – the content widget.

Wrap body under a heading called name.

eventFilter(watched, event) → bool[source]

Hide the section with its body, and show it again with it.

Parameters:
  • watched – the body.

  • event – only ShowToParent and HideToParent are read.

Returns:

False, so the event goes on as usual.

set_folded(folded: bool, *, by_user: bool = True) → bool[source]

Fold or open the section; returns the new folded state.

Parameters:
  • folded – True to fold it.

  • by_user – False for a fold made on the user’s behalf, which is never remembered.

property shut: bool[source]

Whether the section is folded.

class spacr.qt.widgets.collapsible_splitter.Pane(widget, name, mode, folder, stretch, extent, minimum, focus, fold_key, hint='')[source]

One registered child of a CollapsibleSplitter.

Made by CollapsibleSplitter.add_pane(); each argument is kept as the attribute of the same name.

Parameters:
  • widget – the splitter’s direct child.

  • name – stable English name; the key its size is stored under.

  • mode – HEADER, EDGE or None.

  • folder – the heading’s Folder, for a HEADER pane.

  • stretch – 0 keeps the pane at its own size when the splitter grows.

  • extent – the size it opens at; updated by every drag.

  • minimum – its minimum while open, dropped while it is collapsed so it can shrink to its heading.

  • focus – showing it also opens it (a live preview the user folded opens again when it is switched back on).

  • fold_key – stores an EDGE pane’s collapse across restarts.

  • hint – what dragging this EDGE pane’s handle does, in English; it replaces the generic “or drag to resize it” in the handle’s tooltip.

Hold the pane’s description; see the class for each field.

is_collapsed() → bool[source]

Whether the pane is collapsed right now.

spacr.qt.widgets.collapsible_splitter.fold_card(card, name: str = '', *, persist_key: str = '')[source]

Make a titled Card fold by its title.

The card keeps its frame and its title; clicking the title folds the body away and the folded card’s title locks to the bottom of its room. A card whose body is built on first show stays unbuilt: the fold moves the card’s body widget only, never the card.

Parameters:
  • card – the card; one without a title is left alone.

  • name – English name; defaults to the card’s title text.

  • persist_key – "<module>/<card>" to remember a user’s fold.

Returns:

the card’s Folder, or None when it has no title.

spacr.qt.widgets.collapsible_splitter.get_pane_extents(persist_key: str) → dict[source]

The sizes the user dragged the panes of persist_key to.

Parameters:

persist_key – the splitter’s key, e.g. "mask::runtime".

Returns:

{pane name: pixels}; empty when nothing was ever dragged or the stored value is unreadable.

spacr.qt.widgets.collapsible_splitter.lock_folded_to_bottom(container: PySide6.QtWidgets.QWidget, folder, orientation=Qt.Vertical) → None[source]

Keep container’s heading at the bottom of its room while folded.

A folded panel given more room than its heading needs – the last pane in a splitter, a section stretched by its column – used to lay its heading out in the middle of that room, which is the reported bug (“when i collapse the console it collapses to the middle”). A stretch put ABOVE the heading while it is folded takes the spare room instead.

Parameters:
  • container – the widget whose layout holds the heading (first) and the body.

  • folder – its Folder.

  • orientation – Qt.Horizontal locks it to the left instead, for a panel that folds sideways.

Called twice for one container and folder – a FoldSection locks itself and CollapsibleSplitter.add_pane() locks every HEADER pane – the second call does nothing, so there is only ever one stretch and the first orientation asked for wins.

spacr.qt.widgets.collapsible_splitter.set_pane_extents(persist_key: str, extents: dict) → None[source]

Remember {pane name: pixels} for persist_key.

Parameters:
  • persist_key – the splitter’s key.

  • extents – {pane name: pixels}; zero and negative sizes are dropped.

spacr.qt.widgets.collapsible_splitter.splitter_of(widget) → CollapsibleSplitter | None[source]

The CollapsibleSplitter widget sits directly in, if any.

Parameters:

widget – any widget.

Nested helpers

lock_folded_to_bottom.apply(shut: bool, _by_user: bool = True) → None

Add or take away the stretch in front of the heading.

spacr/qt/widgets/collapsible_splitter.py:209