v0.24.1: bpm box in line with the now-playing panel

The boxed controls were centered on the whole bar while the now-playing
panel hung from its top, so the bpm box sat ~11px below the panel beside
it. The panel is now CONTROL_HEIGHT tall and every control shares its top
edge; the seek row hangs under the panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 23:56:19 -07:00
co-authored by Claude Opus 5.5
parent 372946c79f
commit 720f5c10a2
4 changed files with 56 additions and 20 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux.""" """LinTunes — iTunes-style music library manager and player for Linux."""
__version__ = "0.24.0" __version__ = "0.24.1"
+25 -18
View File
@@ -16,12 +16,10 @@ from lintunes import theme
ICON_GRAY = QColor("#4A4A4A") ICON_GRAY = QColor("#4A4A4A")
# Height floor for the top control panel. It must exceed the natural stacked # Height floor for the top control panel: the row of CONTROL_HEIGHT boxes and
# height of title + artist + timeline (~69px at medium scale) so the leftover # now-playing panel, the seek row hanging under the panel (~17px at medium
# becomes vertical slack that the equal stretches split into matching borders # scale), and the 3px margins. Any leftover sits below the seek row.
# above the title and below the artist. Bigger = more breathing room (and a BAR_HEIGHT = 86
# taller bar); the text block stays centered either way.
BAR_HEIGHT = 84
# Shared height for the boxed side controls (transport, shuffle, bpm) and the # Shared height for the boxed side controls (transport, shuffle, bpm) and the
# visualizer, so they line up. ~midway between their old heights: the stretchy # visualizer, so they line up. ~midway between their old heights: the stretchy
@@ -29,6 +27,10 @@ BAR_HEIGHT = 84
# at ~46px. Tune to taste. # at ~46px. Tune to taste.
CONTROL_HEIGHT = 62 CONTROL_HEIGHT = 62
# Every control in the bar hangs from the same top edge as the now-playing
# panel (the seek row sits below the panel, not beside the boxes).
TOP = Qt.AlignmentFlag.AlignTop
def _box_style(bg: QColor) -> str: def _box_style(bg: QColor) -> str:
return f""" return f"""
QFrame#transportBox {{ QFrame#transportBox {{
@@ -263,7 +265,7 @@ class TransportBar(QWidget):
bg=box_bg, margins=(3, 3, 3, 3), bg=box_bg, margins=(3, 3, 3, 3),
height=CONTROL_HEIGHT, split=True) height=CONTROL_HEIGHT, split=True)
self._boxes.append(transport_box) self._boxes.append(transport_box)
layout.addWidget(transport_box) layout.addWidget(transport_box, alignment=TOP)
self._shuffle_btn = FlashButton("shuffle") self._shuffle_btn = FlashButton("shuffle")
self._shuffle_btn.setMinimumWidth(50) self._shuffle_btn.setMinimumWidth(50)
@@ -272,12 +274,12 @@ class TransportBar(QWidget):
shuffle_box = _box(self._shuffle_btn, bg=box_bg, margins=(3, 3, 3, 3), shuffle_box = _box(self._shuffle_btn, bg=box_bg, margins=(3, 3, 3, 3),
height=CONTROL_HEIGHT, split=True) height=CONTROL_HEIGHT, split=True)
self._boxes.append(shuffle_box) self._boxes.append(shuffle_box)
layout.addWidget(shuffle_box) layout.addWidget(shuffle_box, alignment=TOP)
layout.addSpacing(10) layout.addSpacing(10)
self._visualizer = VisualizerWidget(player, prefs) self._visualizer = VisualizerWidget(player, prefs)
self._visualizer.setFixedHeight(CONTROL_HEIGHT) self._visualizer.setFixedHeight(CONTROL_HEIGHT)
layout.addWidget(self._visualizer) layout.addWidget(self._visualizer, alignment=TOP)
layout.addSpacing(10) layout.addSpacing(10)
# Master output volume: a slim horizontal slider in its own slot # Master output volume: a slim horizontal slider in its own slot
@@ -286,6 +288,9 @@ class TransportBar(QWidget):
self._volume_slider = ClickJumpSlider() self._volume_slider = ClickJumpSlider()
self._volume_slider.setRange(0, 100) self._volume_slider.setRange(0, 100)
self._volume_slider.setFixedWidth(96) self._volume_slider.setFixedWidth(96)
# As tall as the boxes, so its groove (drawn centered) sits on the
# same midline as everything else in the row.
self._volume_slider.setFixedHeight(CONTROL_HEIGHT)
self._volume_slider.setFocusPolicy(Qt.FocusPolicy.NoFocus) self._volume_slider.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self._volume_slider.setToolTip("Volume") self._volume_slider.setToolTip("Volume")
initial_volume = _clamp_volume(self._prefs.get("volume", 0.8)) initial_volume = _clamp_volume(self._prefs.get("volume", 0.8))
@@ -298,7 +303,7 @@ class TransportBar(QWidget):
self._volume_save_timer.setInterval(400) self._volume_save_timer.setInterval(400)
self._volume_save_timer.timeout.connect(self._save_volume) self._volume_save_timer.timeout.connect(self._save_volume)
self._volume_slider.valueChanged.connect(self._on_volume_changed) self._volume_slider.valueChanged.connect(self._on_volume_changed)
layout.addWidget(self._volume_slider) layout.addWidget(self._volume_slider, alignment=TOP)
layout.addSpacing(10) layout.addSpacing(10)
center = QVBoxLayout() center = QVBoxLayout()
@@ -338,21 +343,23 @@ class TransportBar(QWidget):
seek_row.addWidget(self._slider) seek_row.addWidget(self._slider)
seek_row.addWidget(self._remaining_label) seek_row.addWidget(self._remaining_label)
# The now-playing panel fills the zone above the seek row; the seek row # The now-playing panel is exactly as tall as the boxes and shares
# stays pinned to the bottom so the timeline sits low. Its height is # their top edge, so boxes, visualizer, panel and bpm read as one row;
# just the slider + time labels, so the panel takes the rest. # the seek row hangs underneath the panel and any slack goes below it.
center.addWidget(self._now_playing, stretch=1) self._now_playing.setFixedHeight(CONTROL_HEIGHT)
center.addWidget(self._now_playing)
center.addLayout(seek_row) center.addLayout(seek_row)
center.addStretch(1)
layout.addLayout(center, stretch=1) layout.addLayout(center, stretch=1)
# bpm tap button lives on the far right as its own box, vertically # bpm tap button lives on the far right as its own box, top-aligned
# centered like the transport boxes — kept out of the seek row so it # with the now-playing panel like the transport boxes — kept out of
# doesn't drive the timeline row's height. # the seek row so it doesn't drive the timeline row's height.
self._bpm_button = BpmButton(player, manager) self._bpm_button = BpmButton(player, manager)
bpm_box = _box(self._bpm_button, bg=box_bg, margins=(8, 8, 8, 8), bpm_box = _box(self._bpm_button, bg=box_bg, margins=(8, 8, 8, 8),
hug=True, height=CONTROL_HEIGHT) hug=True, height=CONTROL_HEIGHT)
self._boxes.append(bpm_box) self._boxes.append(bpm_box)
layout.addWidget(bpm_box) layout.addWidget(bpm_box, alignment=TOP)
# Wiring # Wiring
self._prev_btn.clicked.connect(player.previous) self._prev_btn.clicked.connect(player.previous)
+4 -1
View File
@@ -875,6 +875,7 @@ class TestTransportLayout:
bar.resize(1200, BAR_HEIGHT) bar.resize(1200, BAR_HEIGHT)
bar.show() bar.show()
idle_y = bar._volume_slider.pos().y() idle_y = bar._volume_slider.pos().y()
idle_height = bar.sizeHint().height()
# The visualizer panel doubles as the cast indicator, so connecting # The visualizer panel doubles as the cast indicator, so connecting
# adds no widget and nothing moves. # adds no widget and nothing moves.
@@ -882,7 +883,9 @@ class TestTransportLayout:
bar.layout().activate() bar.layout().activate()
assert bar._volume_slider.pos().y() == idle_y assert bar._volume_slider.pos().y() == idle_y
assert bar.sizeHint().height() <= BAR_HEIGHT # Compared with idle rather than BAR_HEIGHT: the seek row's time
# labels follow the app font, which other tests may have scaled up.
assert bar.sizeHint().height() == idle_height
bar.hide() bar.hide()
def test_clicking_the_visualizer_while_casting_disconnects( def test_clicking_the_visualizer_while_casting_disconnects(
+26
View File
@@ -0,0 +1,26 @@
"""Round 62: the bpm box lines up with the rest of the transport bar.
The boxed controls (transport, shuffle, visualizer, bpm) were centered on the
whole bar while the now-playing panel hung from its top, so the bpm box sat
~11px lower than the panel right beside it. Now the panel is CONTROL_HEIGHT
tall and every control shares its top edge; the seek row sits under the panel.
"""
from tests.test_round21 import _build_bar
from lintunes.gui.transport import CONTROL_HEIGHT
def test_every_control_shares_the_now_playing_row(qapp, tmp_path):
bar = _build_bar(qapp, tmp_path)
panel = bar._now_playing.geometry()
assert panel.height() == CONTROL_HEIGHT
for widget in (bar._prev_btn.parentWidget(), bar._shuffle_btn.parentWidget(),
bar._visualizer, bar._volume_slider,
bar._bpm_button.parentWidget()):
g = widget.geometry()
assert (g.top(), g.bottom()) == (panel.top(), panel.bottom())
def test_seek_row_hangs_under_the_panel(qapp, tmp_path):
bar = _build_bar(qapp, tmp_path)
assert bar._slider.geometry().top() > bar._now_playing.geometry().bottom()