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 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 22:51:05 -07:00
co-authored by Claude Opus 5.5
parent 43d7b91aa6
commit d5cdddee33
3 changed files with 52 additions and 20 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux."""
__version__ = "0.31.3"
__version__ = "0.31.4"
+32 -19
View File
@@ -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()
+19
View File
@@ -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