v0.47.0: no album art shows a debossed panel, not a flat square
The sidebar's art square, when a track has no art, is now the list's own background with a rounded panel set into it, debossed like the visualizer and the now-playing panel. paint_deboss takes its rims in pixels when asked, so a panel as tall as the sidebar is recessed as much as a short one rather than fading over 16 px.
This commit is contained in:
@@ -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.46.0"
|
__version__ = "0.47.0"
|
||||||
|
|||||||
+19
-6
@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QSizePolicy,
|
QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QSizePolicy,
|
||||||
QFrame,
|
QFrame,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QTimer, QPoint, 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
|
||||||
@@ -253,9 +253,14 @@ 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: inset from the list's edges, corners like the visualizer's.
|
||||||
|
NO_ART_MARGIN = 6
|
||||||
|
NO_ART_RADIUS = 8
|
||||||
|
|
||||||
|
|
||||||
class SidebarArt(QWidget):
|
class SidebarArt(QWidget):
|
||||||
"""Square album art pinned to the sidebar bottom; gray square when the
|
"""Square album art pinned to the sidebar bottom; an empty debossed panel
|
||||||
current track has no embedded art. A click opens the big art window, or,
|
when the current track has no embedded art. A click opens the big art window, or,
|
||||||
on the gray square, asks for the track's art to be downloaded."""
|
on the gray square, asks for the track's art to be downloaded."""
|
||||||
|
|
||||||
download_art_requested = pyqtSignal(int) # track id
|
download_art_requested = pyqtSignal(int) # track id
|
||||||
@@ -317,10 +322,18 @@ class SidebarArt(QWidget):
|
|||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
# A shade lighter than mid: the sidebar group's frame line is drawn
|
|
||||||
# in mid, and would vanish into a placeholder of the same gray.
|
|
||||||
painter.fillRect(self.rect(), self.palette().mid().color().lighter(112))
|
|
||||||
if self._pixmap is None or self._pixmap.isNull():
|
if self._pixmap is None or self._pixmap.isNull():
|
||||||
|
# No art: the list's own background, and in it a rounded panel
|
||||||
|
# debossed like the visualizer and the now-playing panel.
|
||||||
|
painter.fillRect(self.rect(), self.palette().base())
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
rect = QRectF(self.rect()).adjusted(
|
||||||
|
NO_ART_MARGIN + 0.5, NO_ART_MARGIN + 0.5,
|
||||||
|
-NO_ART_MARGIN - 0.5, -NO_ART_MARGIN - 0.5)
|
||||||
|
theme.paint_deboss(painter, rect,
|
||||||
|
self.palette().alternateBase().color(),
|
||||||
|
NO_ART_RADIUS,
|
||||||
|
rim=theme.button_rim_px(self.fontMetrics()))
|
||||||
return
|
return
|
||||||
scaled = self._pixmap.scaled(
|
scaled = self._pixmap.scaled(
|
||||||
self.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
self.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
||||||
|
|||||||
+28
-22
@@ -235,17 +235,14 @@ def deboss_qss(selector: str, color: QColor, *, radius: int = 8,
|
|||||||
f" background: {emboss_gradient(color, pressed=True)}; {extra} }}")
|
f" background: {emboss_gradient(color, pressed=True)}; {extra} }}")
|
||||||
|
|
||||||
|
|
||||||
def paint_deboss(painter, rect, color: QColor, radius: float):
|
def paint_deboss(painter, rect, color: QColor, radius: float,
|
||||||
|
rim: float | None = None):
|
||||||
"""``deboss_qss`` for a widget that paints itself: fills ``rect`` (a
|
"""``deboss_qss`` for a widget that paints itself: fills ``rect`` (a
|
||||||
QRectF) as a debossed rounded panel and returns its path, for clipping."""
|
QRectF) as a debossed rounded panel and returns its path, for clipping.
|
||||||
from PyQt6.QtCore import QPointF
|
``rim`` (pixels) fixes the rims' height for a panel much taller than a
|
||||||
from PyQt6.QtGui import QLinearGradient, QPainterPath, QPen
|
button; without it they're ``EMBOSS_RIM_WIDTH`` of the height."""
|
||||||
path = QPainterPath()
|
from PyQt6.QtGui import QPen
|
||||||
path.addRoundedRect(rect, radius, radius)
|
path = _fill_embossed(painter, rect, color, radius, rim, pressed=True)
|
||||||
gradient = QLinearGradient(QPointF(0, rect.top()), QPointF(0, rect.bottom()))
|
|
||||||
for pos, stop in emboss_stops(color, pressed=True):
|
|
||||||
gradient.setColorAt(pos, stop)
|
|
||||||
painter.fillPath(path, gradient)
|
|
||||||
painter.setPen(QPen(color.darker(135), 1))
|
painter.setPen(QPen(color.darker(135), 1))
|
||||||
painter.drawPath(path)
|
painter.drawPath(path)
|
||||||
# the darker top edge
|
# the darker top edge
|
||||||
@@ -257,6 +254,25 @@ def paint_deboss(painter, rect, color: QColor, radius: float):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_embossed(painter, rect, color: QColor, radius: float,
|
||||||
|
rim: float | None, pressed: bool):
|
||||||
|
"""Fill a rounded ``rect`` with ``emboss_stops``; ``rim`` in pixels, or
|
||||||
|
None for the stops' own fractions. Returns the path."""
|
||||||
|
from PyQt6.QtCore import QPointF
|
||||||
|
from PyQt6.QtGui import QLinearGradient, QPainterPath
|
||||||
|
path = QPainterPath()
|
||||||
|
path.addRoundedRect(rect, radius, radius)
|
||||||
|
stops = emboss_stops(color, pressed)
|
||||||
|
if rim is not None:
|
||||||
|
edge = min(rim / max(rect.height(), 1.0), 0.49)
|
||||||
|
stops = [(pos, c) for pos, (_, c) in zip([0, edge, 0.5, 1 - edge, 1], stops)]
|
||||||
|
gradient = QLinearGradient(QPointF(0, rect.top()), QPointF(0, rect.bottom()))
|
||||||
|
for pos, stop in stops:
|
||||||
|
gradient.setColorAt(pos, stop)
|
||||||
|
painter.fillPath(path, gradient)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def button_rim_px(metrics) -> float:
|
def button_rim_px(metrics) -> float:
|
||||||
"""How tall a button's rims are, in pixels, for a font with these
|
"""How tall a button's rims are, in pixels, for a font with these
|
||||||
QFontMetrics: ``EMBOSS_RIM_WIDTH`` of a button's height, which is the
|
QFontMetrics: ``EMBOSS_RIM_WIDTH`` of a button's height, which is the
|
||||||
@@ -269,18 +285,8 @@ def paint_emboss(painter, rect, color: QColor, radius: float, rim: float):
|
|||||||
"""A button's face for a widget that paints itself: ``emboss_stops`` lit
|
"""A button's face for a widget that paints itself: ``emboss_stops`` lit
|
||||||
from above, but with rims ``rim`` pixels tall whatever ``rect``'s height,
|
from above, but with rims ``rim`` pixels tall whatever ``rect``'s height,
|
||||||
so a long scrollbar thumb is embossed exactly as much as a button."""
|
so a long scrollbar thumb is embossed exactly as much as a button."""
|
||||||
from PyQt6.QtCore import QPointF
|
from PyQt6.QtGui import QPen
|
||||||
from PyQt6.QtGui import QLinearGradient, QPainterPath, QPen
|
path = _fill_embossed(painter, rect, color, radius, rim, pressed=False)
|
||||||
path = QPainterPath()
|
|
||||||
path.addRoundedRect(rect, radius, radius)
|
|
||||||
height = max(rect.height(), 1.0)
|
|
||||||
edge = min(rim / height, 0.49)
|
|
||||||
stops = emboss_stops(color)
|
|
||||||
positions = [0, edge, 0.5, 1 - edge, 1]
|
|
||||||
gradient = QLinearGradient(QPointF(0, rect.top()), QPointF(0, rect.bottom()))
|
|
||||||
for pos, (_, stop) in zip(positions, stops):
|
|
||||||
gradient.setColorAt(pos, stop)
|
|
||||||
painter.fillPath(path, gradient)
|
|
||||||
painter.setPen(QPen(color.darker(135), 1))
|
painter.setPen(QPen(color.darker(135), 1))
|
||||||
painter.drawPath(path)
|
painter.drawPath(path)
|
||||||
# the darker bottom edge (emboss_edges)
|
# the darker bottom edge (emboss_edges)
|
||||||
|
|||||||
@@ -98,3 +98,46 @@ def test_thumb_paints(qapp):
|
|||||||
trough = image.pixelColor(7, 5)
|
trough = image.pixelColor(7, 5)
|
||||||
middle = image.pixelColor(7, thumb.center().y())
|
middle = image.pixelColor(7, thumb.center().y())
|
||||||
assert middle != trough
|
assert middle != trough
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# no album art: a debossed panel, not a flat square
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_no_art_is_a_debossed_panel(qapp):
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
from lintunes.gui.sidebar import NO_ART_MARGIN, SidebarArt
|
||||||
|
|
||||||
|
class Player(QObject):
|
||||||
|
track_changed = pyqtSignal(object)
|
||||||
|
|
||||||
|
art = SidebarArt(Player())
|
||||||
|
art.resize(200, 200)
|
||||||
|
image = art.grab().toImage()
|
||||||
|
base = art.palette().base().color()
|
||||||
|
# the margin around the panel is the list's own background...
|
||||||
|
assert image.pixelColor(1, 100) == base
|
||||||
|
# ...the corner is rounded off, so it's background too...
|
||||||
|
assert image.pixelColor(NO_ART_MARGIN + 1, NO_ART_MARGIN + 1) == base
|
||||||
|
# ...and the panel is shaded darker at the top than in the middle
|
||||||
|
top = image.pixelColor(100, NO_ART_MARGIN + 2).lightness()
|
||||||
|
middle = image.pixelColor(100, 100).lightness()
|
||||||
|
assert top < middle
|
||||||
|
|
||||||
|
|
||||||
|
def test_deboss_rims_can_be_fixed_in_pixels(qapp):
|
||||||
|
"""On a 400 px panel the default rims are 32 px deep; 3 px rims leave
|
||||||
|
row 10 already on the face, a good deal lighter."""
|
||||||
|
from PyQt6.QtCore import QRectF
|
||||||
|
from PyQt6.QtGui import QImage, QPainter
|
||||||
|
color = QColor("#e0e0e0")
|
||||||
|
|
||||||
|
def row10(**kw):
|
||||||
|
image = QImage(100, 400, QImage.Format.Format_ARGB32)
|
||||||
|
image.fill(0)
|
||||||
|
painter = QPainter(image)
|
||||||
|
theme.paint_deboss(painter, QRectF(0.5, 0.5, 99, 399), color, 8, **kw)
|
||||||
|
painter.end()
|
||||||
|
return image.pixelColor(50, 10).lightness()
|
||||||
|
|
||||||
|
assert row10(rim=3) > row10() + 5
|
||||||
|
|||||||
Reference in New Issue
Block a user