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."""
__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,
QInputDialog, QMessageBox, QAbstractItemView, QLineEdit, QApplication,
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 lintunes.models import PlaylistType
@@ -76,9 +76,7 @@ class SidebarPanel(QWidget):
self.header = top
top.header_owner = self
# The playlists and the album art under them are one group inside
# one frame — the list's own frame, drawn around both, so the art
# sits within it rather than hanging below.
# The playlists in their frame; the album art panel sits below it.
group = QFrame()
self._lists = QStackedWidget()
self.tree = PlaylistTree(manager)
@@ -95,9 +93,9 @@ class SidebarPanel(QWidget):
inner.addWidget(self._lists, stretch=1)
self.art = SidebarArt(player)
layout.addWidget(group, stretch=1)
self._inner, self._outer = inner, layout
self.art.has_art_changed.connect(self._place_art)
self._place_art(False)
# Art or none, the same panel in the same place: a cover changes
# what is in the panel, never the frame around it.
layout.addWidget(self.art)
# What the header row lines the Library button up with.
self.list_area = group
@@ -109,14 +107,6 @@ class SidebarPanel(QWidget):
self.friend_list.playlist_selected.connect(self._on_friend_playlist)
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):
self.library_button.setChecked(True)
if self.friend_mode:
@@ -263,22 +253,18 @@ class FriendPlaylistList(QListWidget):
self.playlist_selected.emit(current.data(Qt.ItemDataRole.UserRole))
# The no-art panel: on the window under the list's frame, this far below
# it, with the visualizer's corners.
# The art panel: on the window under the list's frame, this far below 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):
"""Square album art pinned to the sidebar bottom, inside the playlist
list's frame. With no embedded art it is an empty panel instead, set
into the window below the frame like the visualizer (SidebarPanel moves
it on ``has_art_changed``). A click opens the big art window, or, on the
empty panel, asks for the track's art to be downloaded."""
"""Square album art pinned to the sidebar bottom: a panel set into the
window below the playlist list's frame, like the visualizer, with the
cover inside its border. With no embedded art the panel is empty. A
click opens the big art window, or, on the empty panel, asks for the
track's art to be downloaded."""
download_art_requested = pyqtSignal(int) # track id
has_art_changed = pyqtSignal(bool)
@@ -329,8 +315,8 @@ 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() + (ART_RULE if self.has_art() else NO_ART_GAP)
# above the panel)
height = self.width() + NO_ART_GAP
if self.height() != height:
self.setFixedHeight(height)
@@ -351,40 +337,30 @@ class SidebarArt(QWidget):
super().mouseReleaseEvent(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.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():
# 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
# 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)
# The cover fills the panel inside its border, so the border and its
# darker top edge stay where they are with or without art.
inside = rect.adjusted(1, 1, -1, -1)
clip = QPainterPath()
clip.addRoundedRect(inside, NO_ART_RADIUS - 1, NO_ART_RADIUS - 1)
painter.setClipPath(clip)
area = inside.toAlignedRect()
scaled = self._pixmap.scaled(
area.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding,
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
painter.setClipRect(area)
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()
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):
"""Shaded ``like`` a 62 px panel, a 400 px one matches it row for row
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 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
def _show_art(qapp, art, color):
pixmap = QPixmap(10, 10)
pixmap.fill(QColor("white"))
pixmap.fill(QColor(color))
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)
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()
middle = image.width() // 2
for y in range(ART_RULE):
assert image.pixelColor(middle, y) == frame
assert image.pixelColor(middle, ART_RULE) == QColor("white")
# the gap and the panel's darker top edge, unchanged
for y in range(NO_ART_GAP + 1):
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()