From f48165080b869a09eebb44277147db7b6d711142 Mon Sep 17 00:00:00 2001 From: trav Date: Tue, 13 Oct 2026 10:11:55 -0700 Subject: [PATCH] v0.51.1: album art sits under the list frame's closing line, like the empty panel --- lintunes/__init__.py | 2 +- lintunes/gui/sidebar.py | 34 ++++++++++++++++++++++++++++------ tests/test_round80.py | 3 ++- tests/test_round82.py | 27 +++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tests/test_round82.py diff --git a/lintunes/__init__.py b/lintunes/__init__.py index aa06f75..7cb678c 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.51.0" +__version__ = "0.51.1" diff --git a/lintunes/gui/sidebar.py b/lintunes/gui/sidebar.py index daa52f3..268d97f 100644 --- a/lintunes/gui/sidebar.py +++ b/lintunes/gui/sidebar.py @@ -2,9 +2,9 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QInputDialog, QMessageBox, QAbstractItemView, QLineEdit, QApplication, QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QSizePolicy, - QFrame, + QFrame, QStyle, QStyleOptionFrame, ) -from PyQt6.QtCore import Qt, QTimer, QPoint, QRectF, pyqtSignal +from PyQt6.QtCore import Qt, QTimer, QPoint, QRect, QRectF, pyqtSignal from PyQt6.QtGui import QBrush, QPainter, QPalette, QPixmap from lintunes.models import PlaylistType @@ -267,6 +267,10 @@ class FriendPlaylistList(QListWidget): # it, with the visualizer's corners. NO_ART_GAP = 6 NO_ART_RADIUS = 8 +# Art sits inside the list's frame, so the frame never closes between them; +# the art draws that closing line itself, this tall, so a cover with a white +# border doesn't run straight on from the list. +ART_RULE = 1 class SidebarArt(QWidget): @@ -326,7 +330,7 @@ class SidebarArt(QWidget): def _fit_height(self): # Keep it square: height follows the sidebar's width (plus the gap # above the empty panel) - height = self.width() + (0 if self.has_art() else NO_ART_GAP) + height = self.width() + (ART_RULE if self.has_art() else NO_ART_GAP) if self.height() != height: self.setFixedHeight(height) @@ -358,11 +362,29 @@ class SidebarArt(QWidget): self.palette().alternateBase().color(), NO_ART_RADIUS, like=CONTROL_HEIGHT) return + # The list frame's own bottom edge, drawn by the style: a frame + # whose bottom row is this widget's top row, its sides pushed out of + # view, so the line is exactly the one under the list with no art. + option = QStyleOptionFrame() + option.initFrom(self) + option.frameShape = QFrame.Shape.StyledPanel + option.state |= QStyle.StateFlag.State_Sunken + option.lineWidth = option.midLineWidth = 0 + frame = self.style().pixelMetric(QStyle.PixelMetric.PM_DefaultFrameWidth) + option.rect = QRect(-frame - 4, ART_RULE - 4 * frame - 8, + self.width() + 2 * frame + 8, 4 * frame + 8) + painter.save() + painter.setClipRect(0, 0, self.width(), ART_RULE) + self.style().drawPrimitive(QStyle.PrimitiveElement.PE_Frame, + option, painter, self) + painter.restore() + area = self.rect().adjusted(0, ART_RULE, 0, 0) scaled = self._pixmap.scaled( - self.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding, + area.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation) - x = (self.width() - scaled.width()) // 2 - y = (self.height() - scaled.height()) // 2 + x = (area.width() - scaled.width()) // 2 + y = area.top() + (area.height() - scaled.height()) // 2 + painter.setClipRect(area) painter.drawPixmap(x, y, scaled) diff --git a/tests/test_round80.py b/tests/test_round80.py index 5eaec12..7eb2c80 100644 --- a/tests/test_round80.py +++ b/tests/test_round80.py @@ -138,6 +138,7 @@ def test_no_art_panel_sits_below_the_frame(qapp, tmp_path): def test_real_art_goes_inside_the_frame(qapp, tmp_path): from PyQt6.QtGui import QPixmap + from lintunes.gui.sidebar import ART_RULE window, sidebar = _sidebar(qapp, tmp_path) art = sidebar.art pixmap = QPixmap(10, 10) @@ -146,7 +147,7 @@ def test_real_art_goes_inside_the_frame(qapp, tmp_path): art.has_art_changed.emit(True) art._fit_height() assert art.parentWidget() is sidebar.list_area - assert art.height() == art.width() + assert art.height() == art.width() + ART_RULE art._pixmap = None art.set_track(None) assert art.parentWidget() is sidebar diff --git a/tests/test_round82.py b/tests/test_round82.py new file mode 100644 index 0000000..a88f062 --- /dev/null +++ b/tests/test_round82.py @@ -0,0 +1,27 @@ +"""Round 82: the list frame closes above album art too.""" +from PyQt6.QtGui import QColor, QPixmap + +from tests.test_round80 import _sidebar + + +def test_art_draws_the_frames_closing_line(qapp, tmp_path): + """A white cover inside the list's frame starts under a line in the + frame's own color, not straight on from the list.""" + from lintunes.gui.sidebar import ART_RULE + window, sidebar = _sidebar(qapp, tmp_path) + art = sidebar.art + pixmap = QPixmap(10, 10) + pixmap.fill(QColor("white")) + art._pixmap = pixmap + art.has_art_changed.emit(True) + art._fit_height() + qapp.processEvents() + group = sidebar.list_area.grab().toImage() + # the frame's own left edge, halfway down the list + frame = group.pixelColor(0, group.height() // 4) + image = art.grab().toImage() + middle = image.width() // 2 + for y in range(ART_RULE): + assert image.pixelColor(middle, y) == frame + assert image.pixelColor(middle, ART_RULE) == QColor("white") + window.close()