Source code for spacr.qt.widgets.usage_bar

"""UsageBar — labeled slim progress bar for RAM/GPU/CPU indicators."""
from __future__ import annotations

from PySide6.QtCore import QEvent, Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QProgressBar, QWidget

from ..theme import SPACING


[docs] class UsageBar(QWidget): """Labeled thin progress bar with a right-aligned percent readout. BOTH TEXT COLUMNS ARE FIXED-WIDTH ON PURPOSE, and that is what made them a bug at a large font scale. The caption column is wider than "RAM" needs so the four bars of the System card line up; the readout column is fixed so the bar does not jitter sideways as 9 % becomes 10 %. Neither reason survives being written as a constant: see :meth:`_size_the_columns`. :param label: text shown to the left of the bar (e.g. "RAM", "GPU"). :param parent: parent widget; ownership only. """ #: The 1.0 widths, kept as the ALIGNMENT FLOOR rather than as the answer. #: A column sized to its own text would put every row's bar at a #: different x; these keep the four rows of the System card in one line. LABEL_W = 48 PCT_W = 40 #: The widest readout :meth:`set_value` can ever produce. Sizing to the #: longest string a control can hold, rather than to the one it happens #: to be showing, is the rule -- and here it also makes #: the width deterministic, since the number on screen is live RAM. WIDEST_PCT = "100%" def __init__(self, label: str, parent=None): """Build one labelled resource meter. The row itself must paint nothing: the application sheet's blanket background rule would otherwise put an opaque rectangle behind the bar, whatever is behind the page. The theme sheet's ``UsageBarRow`` rule says so for every row at once, so a row carries no sheet of its own. :param label: what is being measured. :param parent: parent widget, or ``None``. """ super().__init__(parent) self.setObjectName("UsageBarRow") self.setAttribute(Qt.WA_StyledBackground, True) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(SPACING["sm"]) self._label = QLabel(label) self._label.setObjectName("Muted") layout.addWidget(self._label) self._bar = QProgressBar() self._bar.setObjectName("UsageBar") self._bar.setRange(0, 100) self._bar.setValue(0) self._bar.setTextVisible(False) layout.addWidget(self._bar, 1) self._pct = QLabel("0%") self._pct.setObjectName("Muted") self._pct.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(self._pct) self._size_the_columns() def _size_the_columns(self) -> None: """Set both fixed widths from the font actually in use. A FIXED SIZE ONLY HOLDS AT ONE FONT SCALE, caught by a sweep at the largest scale preferences offers. `setFixedWidth(48)` and `setFixedWidth(40)` were written at 100 %, and at 200 % the glyphs doubled while the boxes did not: 'VRAM' 48 px of column, 66 px of text 'RAM' 48 px of column, 52 px of text '100%' 40 px of column, 62 px of text (and 46 at 150 %) so every System card in the application cut its own labels in half. `scaled_px` exists for precisely this -- its docstring says "any control tuned to match a text width goes wrong at large font scales" -- and it is the floor here rather than the whole answer, because a scale is not the only way a glyph gets wider: a theme font, a translated caption or a longer bar name would each overflow a purely proportional box. The floor keeps the rows aligned; the metrics keep the text whole; whichever is larger wins. """ from ..preferences import scaled_px if getattr(self, "_pct", None) is None: return caption = self._label.fontMetrics().horizontalAdvance( self._label.text()) self._label.setFixedWidth(max(scaled_px(self.LABEL_W), caption)) readout = self._pct.fontMetrics().horizontalAdvance(self.WIDEST_PCT) self._pct.setFixedWidth(max(scaled_px(self.PCT_W), readout))
[docs] def changeEvent(self, event) -> None: """Re-measure when the font under this row changes. The stylesheet is what carries the font scale, and it is installed on the application AFTER these widgets are built -- a preferences save re-installs it on a live window. Both arrive as a font change on the widgets whose resolved font moved, so the columns are re-measured there rather than being right only for the sheet that happened to be current at construction. :param event: the change event, passed to the base class; a font, application-font or style change re-measures the columns. """ super().changeEvent(event) if event.type() in (QEvent.FontChange, QEvent.ApplicationFontChange, QEvent.StyleChange): self._size_the_columns()
[docs] def showEvent(self, event) -> None: """Re-measure on the way to being seen. A widget's QSS font is resolved when it is polished, which for a screen built into a stacked page happens on the way to the first show. Measuring here as well as on the font change is what keeps a row that was never sent a font change -- because its own font was inherited whole -- from being sized by the default metrics. :param event: the show event, passed to the base class after the columns are re-measured. """ self._size_the_columns() super().showEvent(event)
[docs] def set_value(self, pct: float) -> None: """Set the bar value, clamped to 0-100, and re-color at 75/90 %. :param pct: value in ``0.0``-``100.0``; out-of-range values clamp. """ pct = max(0, min(100, int(round(pct)))) self._bar.setValue(pct) self._pct.setText(f"{pct}%") if pct >= 90: self._bar.setObjectName("UsageBarError") elif pct >= 75: self._bar.setObjectName("UsageBarWarn") else: self._bar.setObjectName("UsageBar") self._bar.style().unpolish(self._bar) self._bar.style().polish(self._bar)