From 370ea498ccfcf301baeb65a3d5f78cd9f97e6c44 Mon Sep 17 00:00:00 2001 From: trav Date: Fri, 11 Sep 2026 21:08:32 -0500 Subject: [PATCH] v0.20.2: the cancel button stops flying around MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status bar's transfer widgets are permanent widgets: Qt lays them out left to right but justifies the group right, so whichever is added last owns the corner and never moves, and everything before it slides whenever the group's total width changes. The label carries the song title of the moment, so it changed width on nearly every track — dragging the ✕ across the bar, which is the one widget you're aiming at. So the ✕ is added last instead of first. It sits in the corner, the label absorbs the movement, and the progress bar stops jittering too. Hovering still expands it to "cancel transfer" and pushes the row left, which is the one shift trav asked to keep. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SKXUgsBBwe3qaHEjeV8ubP --- lintunes/__init__.py | 2 +- lintunes/gui/main_window.py | 17 ++++--- tests/test_round53.py | 90 +++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 tests/test_round53.py diff --git a/lintunes/__init__.py b/lintunes/__init__.py index 0d87d85..dfe003b 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.20.1" +__version__ = "0.20.2" diff --git a/lintunes/gui/main_window.py b/lintunes/gui/main_window.py index e92aeda..7e647db 100644 --- a/lintunes/gui/main_window.py +++ b/lintunes/gui/main_window.py @@ -50,8 +50,9 @@ def album_tracks(library, artist: str, album: str) -> list: class _CancelSyncButton(QPushButton): - """Compact "✕" beside the sync progress bar that expands to say what it - does ("cancel transfer") while hovered, then shrinks back.""" + """Compact "✕" in the status bar's right corner that expands to say what + it does ("cancel transfer") while hovered, then shrinks back. Expanding + pushes the progress bar and label left; the button itself stays put.""" def __init__(self, parent=None): super().__init__(parent) @@ -149,10 +150,10 @@ class MainWindow(QMainWindow): # status bar's right and survive showMessage. No stretch (see above) # and hidden while idle so it costs no space. Text lives in its own # label — QProgressBar's overlay can't fit a sentence at this width. - self._sync_cancel = _CancelSyncButton() - self._sync_cancel.hide() - self._sync_cancel.clicked.connect(self._confirm_cancel_sync) - self.statusBar().addPermanentWidget(self._sync_cancel) + # The group is laid out left to right but justified right, so whatever + # is added *last* owns the corner and never moves. The cancel button + # goes there deliberately: it is the one widget the user aims at, and + # the label beside it is as wide as the song title of the moment. self._sync_label = QLabel() self._sync_label.hide() self.statusBar().addPermanentWidget(self._sync_label) @@ -161,6 +162,10 @@ class MainWindow(QMainWindow): self._sync_progress.setTextVisible(False) self._sync_progress.hide() self.statusBar().addPermanentWidget(self._sync_progress) + self._sync_cancel = _CancelSyncButton() + self._sync_cancel.hide() + self._sync_cancel.clicked.connect(self._confirm_cancel_sync) + self.statusBar().addPermanentWidget(self._sync_cancel) self._andtunes_worker = None self._install_worker = None self._sync_device_name = "" diff --git a/tests/test_round53.py b/tests/test_round53.py new file mode 100644 index 0000000..9546938 --- /dev/null +++ b/tests/test_round53.py @@ -0,0 +1,90 @@ +"""Round 53 — the cancel button stops flying around. + +The status bar's transfer widgets are permanent widgets, so Qt lays them out +left to right but justifies the group right: whichever one is added *last* +owns the corner and never moves, and everything before it slides whenever the +group's total width changes. The label carries the song title of the moment, +so it changes width on nearly every track — which used to drag the ✕ across +the bar, since it was added first. + +So the ✕ is added last. It sits in the corner; the label absorbs the movement. +Hovering it is the one thing that may still shift the row, because the button +expands to say "cancel transfer" — trav asked for that to stay. +""" +import pytest +from PyQt6.QtCore import QEvent, QPointF +from PyQt6.QtGui import QEnterEvent + +from lintunes.library_manager import LibraryManager +from lintunes.models import Library, Track +from lintunes.preferences import Preferences + + +def _library(tmp_path): + media = tmp_path / "media" + library = Library(music_folder=str(media)) + path = media / "Music" / "Artist" / "Album" / "T1.mp3" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"audio") + library.tracks[1] = Track(track_id=1, name="T1", artist="Artist", + location=str(path)) + return library + + +@pytest.fixture +def window(qapp, tmp_path, monkeypatch): + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config")) + from lintunes.gui.main_window import MainWindow + manager = LibraryManager(_library(tmp_path), tmp_path / "data") + win = MainWindow(manager, Preferences(tmp_path / "data")) + win.resize(1280, 800) + win.show() + for widget in (win._sync_label, win._sync_progress, win._sync_cancel): + widget.show() + qapp.processEvents() + yield win + win.close() + + +def _right_edge(widget): + return widget.x() + widget.width() + + +class TestCancelButtonStaysInTheCorner: + + def test_a_longer_song_title_does_not_move_the_button(self, window, qapp): + window._sync_label.setText("Rabbit R1 · 3/40 04 Song") + qapp.processEvents() + before = window._sync_cancel.x() + window._sync_label.setText( + "Rabbit R1 · 12/40 07 A Considerably Longer Song Title Indeed") + qapp.processEvents() + assert window._sync_label.width() > 0 + assert window._sync_cancel.x() == before + + def test_the_button_is_the_rightmost_of_the_three(self, window, qapp): + window._sync_label.setText("Rabbit R1 · 3/40 04 Song") + qapp.processEvents() + assert (_right_edge(window._sync_label) + <= window._sync_progress.x()) + assert (_right_edge(window._sync_progress) + <= window._sync_cancel.x()) + + def test_hovering_expands_leftwards(self, window, qapp): + window._sync_label.setText("Rabbit R1 · 3/40 04 Song") + qapp.processEvents() + edge = _right_edge(window._sync_cancel) + progress_x = window._sync_progress.x() + + narrow = window._sync_cancel.width() + here = QPointF(1, 1) + window._sync_cancel.enterEvent(QEnterEvent(here, here, here)) + qapp.processEvents() + assert window._sync_cancel.width() > narrow + assert _right_edge(window._sync_cancel) == edge + assert window._sync_progress.x() < progress_x + + window._sync_cancel.leaveEvent(QEvent(QEvent.Type.Leave)) + qapp.processEvents() + assert _right_edge(window._sync_cancel) == edge + assert window._sync_progress.x() == progress_x