v0.51.2: album art sits in the empty art panel's own frame, below the list, instead of inside the list's frame

This commit is contained in:
2026-10-13 10:20:26 -07:00
parent f48165080b
commit af92ff9b73
4 changed files with 66 additions and 90 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.51.1" __version__ = "0.51.2"
+32 -56
View File
@@ -2,9 +2,9 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QWidget, QVBoxLayout, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu,
QInputDialog, QMessageBox, QAbstractItemView, QLineEdit, QApplication, QInputDialog, QMessageBox, QAbstractItemView, QLineEdit, QApplication,
QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QSizePolicy, QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QSizePolicy,
QFrame, QStyle, QStyleOptionFrame, QFrame,
) )
from PyQt6.QtCore import Qt, QTimer, QPoint, QRect, QRectF, pyqtSignal from PyQt6.QtCore import Qt, QTimer, QPoint, QRectF, pyqtSignal
from PyQt6.QtGui import QBrush, QPainter, QPalette, QPixmap from PyQt6.QtGui import QBrush, QPainter, QPalette, QPixmap
from lintunes.models import PlaylistType from lintunes.models import PlaylistType
@@ -76,9 +76,7 @@ class SidebarPanel(QWidget):
self.header = top self.header = top
top.header_owner = self top.header_owner = self
# The playlists and the album art under them are one group inside # The playlists in their frame; the album art panel sits below it.
# one frame — the list's own frame, drawn around both, so the art
# sits within it rather than hanging below.
group = QFrame() group = QFrame()
self._lists = QStackedWidget() self._lists = QStackedWidget()
self.tree = PlaylistTree(manager) self.tree = PlaylistTree(manager)
@@ -95,9 +93,9 @@ class SidebarPanel(QWidget):
inner.addWidget(self._lists, stretch=1) inner.addWidget(self._lists, stretch=1)
self.art = SidebarArt(player) self.art = SidebarArt(player)
layout.addWidget(group, stretch=1) layout.addWidget(group, stretch=1)
self._inner, self._outer = inner, layout # Art or none, the same panel in the same place: a cover changes
self.art.has_art_changed.connect(self._place_art) # what is in the panel, never the frame around it.
self._place_art(False) layout.addWidget(self.art)
# What the header row lines the Library button up with. # What the header row lines the Library button up with.
self.list_area = group self.list_area = group
@@ -109,14 +107,6 @@ class SidebarPanel(QWidget):
self.friend_list.playlist_selected.connect(self._on_friend_playlist) self.friend_list.playlist_selected.connect(self._on_friend_playlist)
self.friend_list.follow_toggled.connect(self.follow_toggled) self.friend_list.follow_toggled.connect(self.follow_toggled)
def _place_art(self, has_art: bool):
"""Art goes inside the list's frame; the empty panel below it, on
the window, where it reads as set into the window like the
visualizer rather than as a box inside a white one."""
for lay in (self._inner, self._outer):
lay.removeWidget(self.art)
(self._inner if has_art else self._outer).addWidget(self.art)
def _on_library_clicked(self): def _on_library_clicked(self):
self.library_button.setChecked(True) self.library_button.setChecked(True)
if self.friend_mode: if self.friend_mode:
@@ -263,22 +253,18 @@ class FriendPlaylistList(QListWidget):
self.playlist_selected.emit(current.data(Qt.ItemDataRole.UserRole)) self.playlist_selected.emit(current.data(Qt.ItemDataRole.UserRole))
# The no-art panel: on the window under the list's frame, this far below # The art panel: on the window under the list's frame, this far below it,
# it, with the visualizer's corners. # with the visualizer's corners.
NO_ART_GAP = 6 NO_ART_GAP = 6
NO_ART_RADIUS = 8 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): class SidebarArt(QWidget):
"""Square album art pinned to the sidebar bottom, inside the playlist """Square album art pinned to the sidebar bottom: a panel set into the
list's frame. With no embedded art it is an empty panel instead, set window below the playlist list's frame, like the visualizer, with the
into the window below the frame like the visualizer (SidebarPanel moves cover inside its border. With no embedded art the panel is empty. A
it on ``has_art_changed``). A click opens the big art window, or, on the click opens the big art window, or, on the empty panel, asks for the
empty panel, asks for the track's art to be downloaded.""" track's art to be downloaded."""
download_art_requested = pyqtSignal(int) # track id download_art_requested = pyqtSignal(int) # track id
has_art_changed = pyqtSignal(bool) has_art_changed = pyqtSignal(bool)
@@ -329,8 +315,8 @@ class SidebarArt(QWidget):
def _fit_height(self): def _fit_height(self):
# Keep it square: height follows the sidebar's width (plus the gap # Keep it square: height follows the sidebar's width (plus the gap
# above the empty panel) # above the panel)
height = self.width() + (ART_RULE if self.has_art() else NO_ART_GAP) height = self.width() + NO_ART_GAP
if self.height() != height: if self.height() != height:
self.setFixedHeight(height) self.setFixedHeight(height)
@@ -351,40 +337,30 @@ class SidebarArt(QWidget):
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
def paintEvent(self, event): def paintEvent(self, event):
# Straight onto the window, a panel debossed in the visualizer's
# color and shaded at its edges as the visualizer is.
from lintunes.gui.transport import CONTROL_HEIGHT
from PyQt6.QtGui import QPainterPath
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
rect = QRectF(self.rect()).adjusted(0.5, NO_ART_GAP + 0.5, -0.5, -0.5)
theme.paint_deboss(painter, rect,
self.palette().alternateBase().color(),
NO_ART_RADIUS, like=CONTROL_HEIGHT)
if self._pixmap is None or self._pixmap.isNull(): if self._pixmap is None or self._pixmap.isNull():
# No art: straight onto the window, a panel debossed in the
# visualizer's color and shaded at its edges as the visualizer is.
from lintunes.gui.transport import CONTROL_HEIGHT
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
rect = QRectF(self.rect()).adjusted(0.5, NO_ART_GAP + 0.5, -0.5, -0.5)
theme.paint_deboss(painter, rect,
self.palette().alternateBase().color(),
NO_ART_RADIUS, like=CONTROL_HEIGHT)
return return
# The list frame's own bottom edge, drawn by the style: a frame # The cover fills the panel inside its border, so the border and its
# whose bottom row is this widget's top row, its sides pushed out of # darker top edge stay where they are with or without art.
# view, so the line is exactly the one under the list with no art. inside = rect.adjusted(1, 1, -1, -1)
option = QStyleOptionFrame() clip = QPainterPath()
option.initFrom(self) clip.addRoundedRect(inside, NO_ART_RADIUS - 1, NO_ART_RADIUS - 1)
option.frameShape = QFrame.Shape.StyledPanel painter.setClipPath(clip)
option.state |= QStyle.StateFlag.State_Sunken area = inside.toAlignedRect()
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( scaled = self._pixmap.scaled(
area.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding, area.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation) Qt.TransformationMode.SmoothTransformation)
x = (area.width() - scaled.width()) // 2 x = area.left() + (area.width() - scaled.width()) // 2
y = area.top() + (area.height() - scaled.height()) // 2 y = area.top() + (area.height() - scaled.height()) // 2
painter.setClipRect(area)
painter.drawPixmap(x, y, scaled) painter.drawPixmap(x, y, scaled)
-18
View File
@@ -136,24 +136,6 @@ def test_no_art_panel_sits_below_the_frame(qapp, tmp_path):
window.close() window.close()
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)
pixmap.fill(QColor("red"))
art._pixmap = pixmap
art.has_art_changed.emit(True)
art._fit_height()
assert art.parentWidget() is sidebar.list_area
assert art.height() == art.width() + ART_RULE
art._pixmap = None
art.set_track(None)
assert art.parentWidget() is sidebar
window.close()
def test_tall_deboss_copies_a_short_panels_edges(qapp): def test_tall_deboss_copies_a_short_panels_edges(qapp):
"""Shaded ``like`` a 62 px panel, a 400 px one matches it row for row """Shaded ``like`` a 62 px panel, a 400 px one matches it row for row
along the top, and is flat in the middle.""" along the top, and is flat in the middle."""
+33 -15
View File
@@ -1,27 +1,45 @@
"""Round 82: the list frame closes above album art too.""" """Round 82: album art sits in the same panel, in the same place, as the
empty art panel: the cover changes what is inside the frame, never the
frame."""
from PyQt6.QtGui import QColor, QPixmap from PyQt6.QtGui import QColor, QPixmap
from tests.test_round80 import _sidebar from tests.test_round80 import _sidebar
def test_art_draws_the_frames_closing_line(qapp, tmp_path): def _show_art(qapp, art, color):
"""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 = QPixmap(10, 10)
pixmap.fill(QColor("white")) pixmap.fill(QColor(color))
art._pixmap = pixmap art._pixmap = pixmap
art.has_art_changed.emit(True)
art._fit_height() art._fit_height()
qapp.processEvents() 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) def test_art_keeps_the_empty_panels_place_and_border(qapp, tmp_path):
from lintunes.gui.sidebar import NO_ART_GAP
window, sidebar = _sidebar(qapp, tmp_path)
art = sidebar.art
empty_geometry = art.geometry()
empty = art.grab().toImage()
_show_art(qapp, art, "white")
assert art.parentWidget() is sidebar
assert art.geometry() == empty_geometry
assert art.height() == art.width() + NO_ART_GAP
image = art.grab().toImage() image = art.grab().toImage()
middle = image.width() // 2 middle = image.width() // 2
for y in range(ART_RULE): # the gap and the panel's darker top edge, unchanged
assert image.pixelColor(middle, y) == frame for y in range(NO_ART_GAP + 1):
assert image.pixelColor(middle, ART_RULE) == QColor("white") assert image.pixelColor(middle, y) == empty.pixelColor(middle, y)
# ...and the cover just inside it
assert image.pixelColor(middle, NO_ART_GAP + 2) == QColor("white")
assert image.pixelColor(middle, image.height() // 2) == QColor("white")
window.close()
def test_art_changes_no_layout(qapp, tmp_path):
window, sidebar = _sidebar(qapp, tmp_path)
list_geometry = sidebar.list_area.geometry()
_show_art(qapp, sidebar.art, "red")
sidebar.art.has_art_changed.emit(True)
qapp.processEvents()
assert sidebar.list_area.geometry() == list_geometry
window.close() window.close()