From 8ffec1be827d69fd607e0817ab87a25f90f16317 Mon Sep 17 00:00:00 2001 From: trav Date: Mon, 28 Sep 2026 23:36:48 -0700 Subject: [PATCH] v0.31.6: album art inside the playlists' frame; text and playhead lifted 2px The playlist list and the album art under it share one frame (the list's own style, drawn around both), and the no-art placeholder is a shade lighter than that frame line so the line still reads around it. The playlist name, times and playhead sit 2px up (HeaderRow.LIFT), on the Library button's bottom line by eye. Co-Authored-By: Claude Opus 5.5 --- lintunes/__init__.py | 2 +- lintunes/gui/sidebar.py | 26 ++++++++++++++++++++------ lintunes/gui/transport.py | 8 ++++++-- tests/test_round69.py | 7 +++++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/lintunes/__init__.py b/lintunes/__init__.py index a0de2ea..8156cc1 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.31.5" +__version__ = "0.31.6" diff --git a/lintunes/gui/sidebar.py b/lintunes/gui/sidebar.py index 05bbddf..0decca9 100644 --- a/lintunes/gui/sidebar.py +++ b/lintunes/gui/sidebar.py @@ -2,6 +2,7 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QInputDialog, QMessageBox, QAbstractItemView, QLineEdit, QApplication, QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QSizePolicy, + QFrame, ) from PyQt6.QtCore import Qt, QTimer, QPoint, pyqtSignal from PyQt6.QtGui import QBrush, QPainter, QPalette, QPixmap @@ -75,17 +76,28 @@ 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. + group = QFrame() self._lists = QStackedWidget() self.tree = PlaylistTree(manager) self._lists.addWidget(self.tree) self.friend_list = FriendPlaylistList() self._lists.addWidget(self.friend_list) - layout.addWidget(self._lists, stretch=1) - # What the header row lines the Library button up with. - self.list_area = self._lists - + group.setFrameShape(self.tree.frameShape()) + group.setFrameShadow(self.tree.frameShadow()) + for view in (self.tree, self.friend_list): + view.setFrameShape(QFrame.Shape.NoFrame) + inner = QVBoxLayout(group) + inner.setContentsMargins(0, 0, 0, 0) + inner.setSpacing(0) + inner.addWidget(self._lists, stretch=1) self.art = SidebarArt(player) - layout.addWidget(self.art) + inner.addWidget(self.art) + layout.addWidget(group, stretch=1) + # What the header row lines the Library button up with. + self.list_area = group self.library_button.clicked.connect(self._on_library_clicked) self.tree.playlist_selected.connect(self._on_playlist_selected) @@ -305,7 +317,9 @@ class SidebarArt(QWidget): def paintEvent(self, event): painter = QPainter(self) - painter.fillRect(self.rect(), self.palette().mid().color()) + # 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(): return scaled = self._pixmap.scaled( diff --git a/lintunes/gui/transport.py b/lintunes/gui/transport.py index f51bbce..410f03e 100644 --- a/lintunes/gui/transport.py +++ b/lintunes/gui/transport.py @@ -276,6 +276,10 @@ class HeaderRow(QWidget): # lists: every item in the row stands on one floor, BOTTOM px up. TOP_PAD = 2 BOTTOM = 5 + # Text and the playhead have no hard bottom edge, so on the floor itself + # they read as sitting lower than the Library button beside them; this + # lifts them onto the button's bottom line by eye. + LIFT = 2 def __init__(self, seek: QWidget, parent=None): super().__init__(parent) @@ -336,7 +340,7 @@ class HeaderRow(QWidget): self.seek.layout().invalidate() self.seek.setGeometry(seek_left, self.TOP_PAD, max(0, panel.right() + 1 - seek_left), - box_h + drop) + box_h + drop - self.LIFT) header = self.view_header if header is not None: width = max(0, seek_left - self.GAP - content_left) @@ -344,7 +348,7 @@ class HeaderRow(QWidget): header.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom) header.setGeometry(content_left, self.TOP_PAD, width, - box_h + self._drop(header)) + box_h + self._drop(header) - self.LIFT) else: header.setGeometry(content_left, self.TOP_PAD, width, box_h) diff --git a/tests/test_round69.py b/tests/test_round69.py index 3a9dde3..f84ad52 100644 --- a/tests/test_round69.py +++ b/tests/test_round69.py @@ -52,6 +52,8 @@ def test_library_button_spans_the_playlist_list(qapp, tmp_path): window = _window(qapp, tmp_path) header = window.sidebar.header lists = window.sidebar.list_area + # The frame around the playlists takes in the album art too. + assert window.sidebar.art.parentWidget() is lists assert header.parentWidget() is not window.sidebar assert _gx(header) == _gx(lists) > _gx(window) assert header.width() == lists.width() @@ -71,12 +73,13 @@ def test_header_row_items_share_one_floor(qapp, tmp_path): assert bottom(window.sidebar.library_button) == floor assert bottom(window._library_view._search) == floor - assert bottom(window._transport._slider) == floor + # Text and the playhead sit LIFT px up, on the button's bottom line by eye. + assert bottom(window._transport._slider) == floor - HeaderRow.LIFT window._show_playlist("abcd1234") qapp.processEvents() label = window._playlist_view.header baseline = bottom(label) - label.fontMetrics().descent() - assert abs(baseline - floor) <= 1 + assert abs(baseline - (floor - HeaderRow.LIFT)) <= 1 def test_search_then_title_sit_where_the_view_starts(qapp, tmp_path):