From d5cdddee33123d00711b40a8c38ca76db50a593f Mon Sep 17 00:00:00 2001 From: trav Date: Mon, 28 Sep 2026 22:51:05 -0700 Subject: [PATCH] v0.31.4: wider now-playing panel, tighter knolled top row The now-playing panel (and the seek row under it) caps at 700px, up from 560. The top row gets one equal gap before, between and after its controls, capped at 28px; past that the extra splits between the two ends, so the row sits centered instead of spreading out. Co-Authored-By: Claude Opus 5.5 --- lintunes/__init__.py | 2 +- lintunes/gui/transport.py | 51 ++++++++++++++++++++++++--------------- tests/test_round69.py | 19 +++++++++++++++ 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/lintunes/__init__.py b/lintunes/__init__.py index aec08eb..301675f 100644 --- a/lintunes/__init__.py +++ b/lintunes/__init__.py @@ -1,3 +1,3 @@ """LinTunes — iTunes-style music library manager and player for Linux.""" -__version__ = "0.31.3" +__version__ = "0.31.4" diff --git a/lintunes/gui/transport.py b/lintunes/gui/transport.py index 17532dd..d2f36a2 100644 --- a/lintunes/gui/transport.py +++ b/lintunes/gui/transport.py @@ -2,7 +2,7 @@ import time from PyQt6.QtWidgets import ( QWidget, QFrame, QHBoxLayout, QVBoxLayout, QLabel, QToolButton, QSlider, - QPushButton, QSizePolicy, QStyle, QLayout, + QPushButton, QSizePolicy, QStyle, QLayout, QSpacerItem, ) from PyQt6.QtCore import Qt, QTimer, QEvent, QPoint, QRect, pyqtSignal from PyQt6.QtGui import QColor, QFont @@ -20,16 +20,15 @@ ICON_GRAY = QColor("#4A4A4A") TOP_MARGIN = 9 # Between the row of boxes and the header row under it. ROW_GAP = 4 -# Side margin of the row of boxes — the Library button's own left margin, so -# the transport box and the Library button share a left edge. -SIDE_MARGIN = 8 - -# The now-playing panel grows with the window up to this width; past it the -# extra goes to the gaps between the controls, evenly. -NOW_PLAYING_MAX = 560 +# The now-playing panel grows with the window up to this width. +NOW_PLAYING_MAX = 700 NOW_PLAYING_MIN = 220 -# Floor for each of those gaps. +# The row of boxes is knolled: the space before the first control, between +# each pair and after the last is one equal gap, between these bounds. Once +# the gaps are as wide as they get, any extra splits between the two ends, +# so the row sits centered rather than spreading out. MIN_GAP = 12 +MAX_GAP = 28 # Shared height for the boxed side controls (transport, shuffle, bpm) and the # visualizer, so they line up. ~midway between their old heights: the stretchy @@ -355,16 +354,20 @@ class TransportBar(QWidget): # window could never shrink past the width it last had. outer.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint) layout = QHBoxLayout() - layout.setContentsMargins(SIDE_MARGIN, 0, SIDE_MARGIN, 0) + layout.setContentsMargins(MIN_GAP, 0, MIN_GAP, 0) layout.setSpacing(0) outer.addLayout(layout) # Knolled: every control keeps its size and the gaps between them - # are equal, growing together once the now-playing panel is as wide - # as it gets. + # are equal; _size_now_playing sizes them (and the two ends). + self._row = layout + self._gaps: list[QSpacerItem] = [] + def gap(): - layout.addSpacing(MIN_GAP) - layout.addStretch(1) + spacer = QSpacerItem(MIN_GAP, 0, QSizePolicy.Policy.Fixed, + QSizePolicy.Policy.Minimum) + self._gaps.append(spacer) + layout.addSpacerItem(spacer) # Rounded button boxes carry a user-tunable grayscale fill, decoupled # from the stripe color they used to share via palette(alternate-base). @@ -466,8 +469,7 @@ class TransportBar(QWidget): # the seek row hangs underneath the panel, in the header row. self._now_playing.setFixedHeight(CONTROL_HEIGHT) # Its width is set by _size_now_playing: all the room the controls - # and minimal gaps leave, up to NOW_PLAYING_MAX; the gaps share the - # rest. + # and minimal gaps leave, up to NOW_PLAYING_MAX. self._now_playing.setFixedWidth(NOW_PLAYING_MIN) layout.addWidget(self._now_playing, alignment=TOP) self._now_playing.installEventFilter(self) @@ -547,16 +549,27 @@ class TransportBar(QWidget): def _controls_width(self) -> int: """Everything in the row of boxes but the now-playing panel, with each gap at its floor.""" - return (2 * SIDE_MARGIN + return (2 * MIN_GAP # A fixed width shows in the minimum, not the size hint. + sum(max(w.sizeHint().width(), w.minimumWidth()) for w in self._fixed_controls) + len(self._fixed_controls) * MIN_GAP) def _size_now_playing(self): + """Panel first, up to its cap; then equal gaps up to MAX_GAP; then + whatever is still left, split between the two ends.""" room = self.width() - self._controls_width() - self._now_playing.setFixedWidth( - max(NOW_PLAYING_MIN, min(NOW_PLAYING_MAX, room))) + panel = max(NOW_PLAYING_MIN, min(NOW_PLAYING_MAX, room)) + self._now_playing.setFixedWidth(panel) + controls = self._controls_width() - (2 + len(self._gaps)) * MIN_GAP + free = max(0, self.width() - controls - panel) + gap = max(MIN_GAP, min(MAX_GAP, free // (len(self._gaps) + 2))) + end = max(MIN_GAP, (free - gap * len(self._gaps)) // 2) + for spacer in self._gaps: + spacer.changeSize(gap, 0, QSizePolicy.Policy.Fixed, + QSizePolicy.Policy.Minimum) + self._row.setContentsMargins(end, 0, end, 0) + self._row.invalidate() def minimumSizeHint(self): hint = super().minimumSizeHint() diff --git a/tests/test_round69.py b/tests/test_round69.py index bdf8b59..fe743e5 100644 --- a/tests/test_round69.py +++ b/tests/test_round69.py @@ -113,3 +113,22 @@ def test_long_name_shrinks_then_elides(qapp): assert label.font().pointSize() >= base - 2 assert label.text().endswith("…") assert label.toolTip() == label.full_text() + + +def test_row_is_knolled_with_equal_gaps_and_ends(qapp, tmp_path): + from lintunes.gui.transport import MAX_GAP, MIN_GAP + window = _window(qapp, tmp_path) + bar = window._transport + for width in (900, 1280, 1800): + window.resize(width, 500) + qapp.processEvents() + row = [bar._prev_btn.parentWidget(), bar._shuffle_btn.parentWidget(), + bar._visualizer, bar._volume_slider, bar._now_playing, + bar._bpm_button.parentWidget()] + edges = [(w.geometry().left(), w.geometry().right()) for w in row] + gaps = [b[0] - a[1] - 1 for a, b in zip(edges, edges[1:])] + assert max(gaps) - min(gaps) <= 1 + assert MIN_GAP <= gaps[0] <= MAX_GAP + left_end, right_end = edges[0][0], bar.width() - 1 - edges[-1][1] + assert abs(left_end - right_end) <= 1 + assert left_end >= gaps[0] - 1