From 720f5c10a2f5a056a29b94abc3f809dcb9ce7c94 Mon Sep 17 00:00:00 2001 From: trav Date: Fri, 25 Sep 2026 23:56:19 -0700 Subject: [PATCH] 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 --- lintunes/__init__.py | 2 +- lintunes/gui/transport.py | 43 +++++++++++++++++++++++---------------- tests/test_round29.py | 5 ++++- tests/test_round62.py | 26 +++++++++++++++++++++++ 4 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 tests/test_round62.py diff --git a/lintunes/__init__.py b/lintunes/__init__.py index 8fa5eb4..23f3978 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.24.0" +__version__ = "0.24.1" diff --git a/lintunes/gui/transport.py b/lintunes/gui/transport.py index 20e59fd..d01579f 100644 --- a/lintunes/gui/transport.py +++ b/lintunes/gui/transport.py @@ -16,12 +16,10 @@ from lintunes import theme ICON_GRAY = QColor("#4A4A4A") -# Height floor for the top control panel. It must exceed the natural stacked -# height of title + artist + timeline (~69px at medium scale) so the leftover -# becomes vertical slack that the equal stretches split into matching borders -# above the title and below the artist. Bigger = more breathing room (and a -# taller bar); the text block stays centered either way. -BAR_HEIGHT = 84 +# Height floor for the top control panel: the row of CONTROL_HEIGHT boxes and +# now-playing panel, the seek row hanging under the panel (~17px at medium +# scale), and the 3px margins. Any leftover sits below the seek row. +BAR_HEIGHT = 86 # Shared height for the boxed side controls (transport, shuffle, bpm) and the # visualizer, so they line up. ~midway between their old heights: the stretchy @@ -29,6 +27,10 @@ BAR_HEIGHT = 84 # at ~46px. Tune to taste. 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: return f""" QFrame#transportBox {{ @@ -263,7 +265,7 @@ class TransportBar(QWidget): bg=box_bg, margins=(3, 3, 3, 3), height=CONTROL_HEIGHT, split=True) self._boxes.append(transport_box) - layout.addWidget(transport_box) + layout.addWidget(transport_box, alignment=TOP) self._shuffle_btn = FlashButton("shuffle") 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), height=CONTROL_HEIGHT, split=True) self._boxes.append(shuffle_box) - layout.addWidget(shuffle_box) + layout.addWidget(shuffle_box, alignment=TOP) layout.addSpacing(10) self._visualizer = VisualizerWidget(player, prefs) self._visualizer.setFixedHeight(CONTROL_HEIGHT) - layout.addWidget(self._visualizer) + layout.addWidget(self._visualizer, alignment=TOP) layout.addSpacing(10) # 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.setRange(0, 100) 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.setToolTip("Volume") 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.timeout.connect(self._save_volume) self._volume_slider.valueChanged.connect(self._on_volume_changed) - layout.addWidget(self._volume_slider) + layout.addWidget(self._volume_slider, alignment=TOP) layout.addSpacing(10) center = QVBoxLayout() @@ -338,21 +343,23 @@ class TransportBar(QWidget): seek_row.addWidget(self._slider) seek_row.addWidget(self._remaining_label) - # The now-playing panel fills the zone above the seek row; the seek row - # stays pinned to the bottom so the timeline sits low. Its height is - # just the slider + time labels, so the panel takes the rest. - center.addWidget(self._now_playing, stretch=1) + # The now-playing panel is exactly as tall as the boxes and shares + # their top edge, so boxes, visualizer, panel and bpm read as one row; + # the seek row hangs underneath the panel and any slack goes below it. + self._now_playing.setFixedHeight(CONTROL_HEIGHT) + center.addWidget(self._now_playing) center.addLayout(seek_row) + center.addStretch(1) layout.addLayout(center, stretch=1) - # bpm tap button lives on the far right as its own box, vertically - # centered like the transport boxes — kept out of the seek row so it - # doesn't drive the timeline row's height. + # bpm tap button lives on the far right as its own box, top-aligned + # with the now-playing panel like the transport boxes — kept out of + # the seek row so it doesn't drive the timeline row's height. self._bpm_button = BpmButton(player, manager) bpm_box = _box(self._bpm_button, bg=box_bg, margins=(8, 8, 8, 8), hug=True, height=CONTROL_HEIGHT) self._boxes.append(bpm_box) - layout.addWidget(bpm_box) + layout.addWidget(bpm_box, alignment=TOP) # Wiring self._prev_btn.clicked.connect(player.previous) diff --git a/tests/test_round29.py b/tests/test_round29.py index 866ed8b..6f8b259 100644 --- a/tests/test_round29.py +++ b/tests/test_round29.py @@ -875,6 +875,7 @@ class TestTransportLayout: bar.resize(1200, BAR_HEIGHT) bar.show() idle_y = bar._volume_slider.pos().y() + idle_height = bar.sizeHint().height() # The visualizer panel doubles as the cast indicator, so connecting # adds no widget and nothing moves. @@ -882,7 +883,9 @@ class TestTransportLayout: bar.layout().activate() 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() def test_clicking_the_visualizer_while_casting_disconnects( diff --git a/tests/test_round62.py b/tests/test_round62.py new file mode 100644 index 0000000..d51a8f5 --- /dev/null +++ b/tests/test_round62.py @@ -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()