Source code for spacr.qt.widgets.card

"""
Card — QFrame with rounded border, optional title bar, and a body widget.

Consumers add content to `card.body_layout` (a QVBoxLayout).
"""
from __future__ import annotations

from PySide6.QtWidgets import QFrame, QVBoxLayout, QLabel, QWidget

from ..theme import SPACING


[docs] class Card(QFrame): """Rounded-border container with optional title/subtitle and a body area. Consumers add content to :attr:`body_layout`. :param title: optional heading rendered above the body. :param subtitle: optional muted subheading rendered under the title. :ivar body: inner QWidget that holds the body layout. :ivar body_layout: QVBoxLayout consumers add widgets to. """ def __init__(self, title: str = "", subtitle: str = "", parent=None, *, foldable: bool = False, fold_key: str = ""): """Initialize the card and optional persistent folding behavior. :param foldable: allow the title to hide or restore the body. Disabled by default because folding is useful only when adjacent content can occupy the released space. :param fold_key: ``"<module>/<panel>"``; given, the fold survives a restart. :param parent: parent widget; ownership only. """ super().__init__(parent) self.setObjectName("Card") #: The :class:`~spacr.qt.widgets.foldable.Folder`, or ``None``. HELD, #: because it owns the event filter and one nobody keeps stops #: working silently. self.folder = None outer = QVBoxLayout(self) outer.setContentsMargins(SPACING["md"], SPACING["md"], SPACING["md"], SPACING["md"]) outer.setSpacing(SPACING["sm"]) title_label = None if title: title_label = QLabel(title) title_label.setObjectName("CardTitle") outer.addWidget(title_label) self.title_label = title_label if subtitle: sub_label = QLabel(subtitle) sub_label.setObjectName("CardSubtitle") sub_label.setWordWrap(True) outer.addWidget(sub_label) if title or subtitle: divider = QFrame() divider.setObjectName("Divider") divider.setFrameShape(QFrame.HLine) outer.addWidget(divider) self.body = QWidget(self) self.body.setObjectName("CardBody") self.body_layout = QVBoxLayout(self.body) self.body_layout.setContentsMargins(0, 0, 0, 0) self.body_layout.setSpacing(SPACING["sm"]) outer.addWidget(self.body, 1) if foldable and title_label is not None: from .foldable import make_foldable self.folder = make_foldable(title_label, self.body, name=title, persist_key=fold_key) self._outer = outer self._title_row = None if self.folder is not None: self.follow_fold(self.folder)
[docs] def follow_fold(self, folder) -> None: """Let the card shrink to its heading while ``folder`` is shut. The body is laid out with a stretch, and a layout counts that stretch even while the body is hidden, so a folded card still asked for room and shared it evenly with the stretch that locks a folded heading to the bottom of its pane: the folded Console came out half the pane tall. Folded, the body gives its stretch up; opened, it takes it back. :param folder: the :class:`~spacr.qt.widgets.foldable.Folder` that folds :attr:`body`. """ def apply(shut: bool, _by_user: bool = True) -> None: """Match the body's stretch to the fold.""" self._outer.setStretchFactor(self.body, 0 if shut else 1) folder.add_listener(apply) apply(folder.shut)
[docs] def add_title_action(self, widget: QWidget) -> None: """Put ``widget`` at the right-hand end of the title row. The title row is built on first use, so a card nobody adds an action to keeps its original layout exactly. :param widget: the control to add, e.g. a Refresh button. """ if self._title_row is None: from PySide6.QtWidgets import QHBoxLayout row = QHBoxLayout() row.setContentsMargins(0, 0, 0, 0) row.setSpacing(SPACING["sm"]) if self.title_label is not None: index = self._outer.indexOf(self.title_label) self._outer.removeWidget(self.title_label) row.addWidget(self.title_label) row.addStretch(1) self._outer.insertLayout(max(index, 0), row) else: row.addStretch(1) self._outer.insertLayout(0, row) self._title_row = row self._title_row.addWidget(widget)
class _CardBuiltWhenShown(Card): """A :class:`Card` whose body can wait until the card is first shown. A module screen is polished widget by widget against the whole stylesheet when it opens, so every widget on it costs time whether or not anybody can see it. A card that starts hidden -- a results panel before there are results, a search panel behind its switch -- is the largest share of that on the heavy screens, and nobody sees its body until the card is shown. A plain ``Card`` in every respect otherwise, ``Card`` rules in the stylesheet included. """ def __init__(self, *args, **kwargs): """Build the card with nothing waiting to fill it.""" super().__init__(*args, **kwargs) self._build_body = None def build_body_when_first_shown(self, build) -> None: """Defer filling this card's body until the card is first shown. ``build`` runs ONCE, before the card becomes visible, from :meth:`setVisible`; :meth:`showEvent` is the belt to that brace for a card that is revealed by its parent rather than shown itself. The caller may also run it earlier through :meth:`ensure_body_built`, which is what a screen does when code asks for the panel inside. :param build: a callable with no arguments that fills :attr:`body_layout`. """ self._build_body = build def body_is_built(self) -> bool: """Whether no deferred body is still waiting to be built.""" return self._build_body is None def ensure_body_built(self) -> bool: """Build the deferred body now, if one is waiting. :returns: ``True`` when this call built it. """ build, self._build_body = self._build_body, None if build is None: return False build() return True def setVisible(self, visible: bool) -> None: # noqa: N802 """Build a deferred body before the card can be seen. Before, not after: children added to a widget that is already visible are only shown on a later turn of the event loop, so a body built from the show would leave one frame of an empty card. """ if visible and self._build_body is not None: self.ensure_body_built() super().setVisible(visible) def showEvent(self, event) -> None: # noqa: N802 """Build a deferred body that a parent's show revealed.""" if self._build_body is not None: self.ensure_body_built() super().showEvent(event)