Square thumbnail selection border, Qt-targetable selection colors, indicator row swap, drop dead missing-indicator code

This commit is contained in:
pax 2026-04-07 15:19:16 -05:00
parent 0eab860088
commit 3824d382c3

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from PySide6.QtCore import Qt, Signal, QSize, QRect, QRectF, QMimeData, QUrl, QPoint, Property from PySide6.QtCore import Qt, Signal, QSize, QRect, QRectF, QMimeData, QUrl, QPoint, Property
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QKeyEvent, QWheelEvent, QDrag, QMouseEvent from PySide6.QtGui import QPixmap, QPainter, QPainterPath, QColor, QPen, QKeyEvent, QWheelEvent, QDrag, QMouseEvent
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QWidget,
QScrollArea, QScrollArea,
@ -31,7 +31,6 @@ class ThumbnailWidget(QWidget):
# QSS-controllable dot colors # QSS-controllable dot colors
_saved_color = QColor("#22cc22") _saved_color = QColor("#22cc22")
_bookmarked_color = QColor("#ffcc00") _bookmarked_color = QColor("#ffcc00")
_missing_color = QColor("#ff4444")
def _get_saved_color(self): return self._saved_color def _get_saved_color(self): return self._saved_color
def _set_saved_color(self, c): self._saved_color = QColor(c) if isinstance(c, str) else c def _set_saved_color(self, c): self._saved_color = QColor(c) if isinstance(c, str) else c
@ -41,9 +40,30 @@ class ThumbnailWidget(QWidget):
def _set_bookmarked_color(self, c): self._bookmarked_color = QColor(c) if isinstance(c, str) else c def _set_bookmarked_color(self, c): self._bookmarked_color = QColor(c) if isinstance(c, str) else c
bookmarkedColor = Property(QColor, _get_bookmarked_color, _set_bookmarked_color) bookmarkedColor = Property(QColor, _get_bookmarked_color, _set_bookmarked_color)
def _get_missing_color(self): return self._missing_color # QSS-controllable selection paint colors. Defaults are read from the
def _set_missing_color(self, c): self._missing_color = QColor(c) if isinstance(c, str) else c # palette in __init__ so non-themed environments still pick up the
missingColor = Property(QColor, _get_missing_color, _set_missing_color) # system Highlight color, but a custom.qss can override any of them
# via `ThumbnailWidget { qproperty-selectionColor: ${accent}; }`.
_selection_color = QColor("#3399ff")
_multi_select_color = QColor("#226699")
_hover_color = QColor("#66bbff")
_idle_color = QColor("#444444")
def _get_selection_color(self): return self._selection_color
def _set_selection_color(self, c): self._selection_color = QColor(c) if isinstance(c, str) else c
selectionColor = Property(QColor, _get_selection_color, _set_selection_color)
def _get_multi_select_color(self): return self._multi_select_color
def _set_multi_select_color(self, c): self._multi_select_color = QColor(c) if isinstance(c, str) else c
multiSelectColor = Property(QColor, _get_multi_select_color, _set_multi_select_color)
def _get_hover_color(self): return self._hover_color
def _set_hover_color(self, c): self._hover_color = QColor(c) if isinstance(c, str) else c
hoverColor = Property(QColor, _get_hover_color, _set_hover_color)
def _get_idle_color(self): return self._idle_color
def _set_idle_color(self, c): self._idle_color = QColor(c) if isinstance(c, str) else c
idleColor = Property(QColor, _get_idle_color, _set_idle_color)
def __init__(self, index: int, parent: QWidget | None = None) -> None: def __init__(self, index: int, parent: QWidget | None = None) -> None:
super().__init__(parent) super().__init__(parent)
@ -53,11 +73,20 @@ class ThumbnailWidget(QWidget):
self._multi_selected = False self._multi_selected = False
self._bookmarked = False self._bookmarked = False
self._saved_locally = False self._saved_locally = False
self._missing = False
self._hover = False self._hover = False
self._drag_start: QPoint | None = None self._drag_start: QPoint | None = None
self._cached_path: str | None = None self._cached_path: str | None = None
self._prefetch_progress: float = -1 # -1 = not prefetching, 0-1 = progress self._prefetch_progress: float = -1 # -1 = not prefetching, 0-1 = progress
# Seed selection colors from the palette so non-themed environments
# (no custom.qss) automatically use the system highlight color.
# The qproperty setters above override these later when the QSS is
# polished, so any theme can repaint via `qproperty-selectionColor`.
from PySide6.QtGui import QPalette
pal = self.palette()
self._selection_color = pal.color(QPalette.ColorRole.Highlight)
self._multi_select_color = self._selection_color.darker(150)
self._hover_color = self._selection_color.lighter(150)
self._idle_color = pal.color(QPalette.ColorRole.Mid)
self.setFixedSize(THUMB_SIZE, THUMB_SIZE) self.setFixedSize(THUMB_SIZE, THUMB_SIZE)
self.setCursor(Qt.CursorShape.PointingHandCursor) self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setMouseTracking(True) self.setMouseTracking(True)
@ -86,10 +115,6 @@ class ThumbnailWidget(QWidget):
self._saved_locally = saved self._saved_locally = saved
self.update() self.update()
def set_missing(self, missing: bool) -> None:
self._missing = missing
self.update()
def set_prefetch_progress(self, progress: float) -> None: def set_prefetch_progress(self, progress: float) -> None:
"""Set prefetch progress: -1 = hide, 0.0-1.0 = progress.""" """Set prefetch progress: -1 = hide, 0.0-1.0 = progress."""
self._prefetch_progress = progress self._prefetch_progress = progress
@ -101,9 +126,11 @@ class ThumbnailWidget(QWidget):
p = QPainter(self) p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing) p.setRenderHint(QPainter.RenderHint.Antialiasing)
pal = self.palette() pal = self.palette()
highlight = pal.color(pal.ColorRole.Highlight) # State colors come from Qt Properties so QSS can override them.
# Defaults were seeded from the palette in __init__.
highlight = self._selection_color
base = pal.color(pal.ColorRole.Base) base = pal.color(pal.ColorRole.Base)
mid = pal.color(pal.ColorRole.Mid) mid = self._idle_color
window = pal.color(pal.ColorRole.Window) window = pal.color(pal.ColorRole.Window)
# Fill entire cell with window color # Fill entire cell with window color
@ -121,7 +148,7 @@ class ThumbnailWidget(QWidget):
# Background (content area only) # Background (content area only)
if self._multi_selected: if self._multi_selected:
p.fillRect(content, highlight.darker(200)) p.fillRect(content, self._multi_select_color.darker(200))
elif self._hover: elif self._hover:
p.fillRect(content, window.lighter(130)) p.fillRect(content, window.lighter(130))
@ -129,58 +156,78 @@ class ThumbnailWidget(QWidget):
# centered on a QRect's geometric edge spills half a pixel out on # centered on a QRect's geometric edge spills half a pixel out on
# each side, which on AA-on rendering blends with the cell # each side, which on AA-on rendering blends with the cell
# background and makes the border read as thinner than the pen # background and makes the border read as thinner than the pen
# width — and uneven, since some sides land on integer pixels and # width. Inset by half the pen width into a QRectF so the full
# some don't. Inset by half the pen width into a QRectF so the # pen width sits cleanly inside the content rect.
# full pen width sits cleanly inside the content rect, and use # All four state colors are QSS-controllable Qt Properties on
# drawRoundedRect for smooth corners that match the rest of the # ThumbnailWidget — see selectionColor, multiSelectColor,
# Fusion-style theming. # hoverColor, idleColor at the top of this class.
if self._selected: if self._selected:
pen_width = 3 pen_width = 3
pen_color = highlight pen_color = self._selection_color
elif self._multi_selected: elif self._multi_selected:
pen_width = 3 pen_width = 3
pen_color = highlight.darker(150) pen_color = self._multi_select_color
elif self._hover: elif self._hover:
pen_width = 1 pen_width = 1
pen_color = highlight.lighter(150) pen_color = self._hover_color
else: else:
pen_width = 1 pen_width = 1
pen_color = mid pen_color = self._idle_color
pen = QPen(pen_color, pen_width)
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
p.setPen(pen)
p.setBrush(Qt.BrushStyle.NoBrush)
half = pen_width / 2.0 half = pen_width / 2.0
border_rect = QRectF(content).adjusted(half, half, -half, -half) border_rect = QRectF(content).adjusted(half, half, -half, -half)
# 2px radius is subtle enough that it doesn't visibly leave window
# color showing through the gap between the rounded curve and the
# square pixmap corners.
p.drawRoundedRect(border_rect, 2, 2)
# Thumbnail # Draw the thumbnail FIRST so the selection border z-orders on top.
# No clip path: the border is square and the pixmap is square, so
# there's nothing to round and nothing to mismatch.
if self._pixmap: if self._pixmap:
x = (self.width() - self._pixmap.width()) // 2 x = (self.width() - self._pixmap.width()) // 2
y = (self.height() - self._pixmap.height()) // 2 y = (self.height() - self._pixmap.height()) // 2
p.drawPixmap(x, y, self._pixmap) p.drawPixmap(x, y, self._pixmap)
# Indicators relative to content rect # Border drawn AFTER the pixmap. Plain rectangle (no rounding) so
indicator_x = content.right() - 2 # it lines up exactly with the pixmap's square edges — no corner
if self._bookmarked: # cut-off triangles where window color would peek through.
pen = QPen(pen_color, pen_width)
p.setPen(pen)
p.setBrush(Qt.BrushStyle.NoBrush)
p.drawRect(border_rect)
# Indicators (top-right of content rect): bookmark on the left,
# saved dot on the right. Both share a fixed-size box so
# they're vertically and horizontally aligned. The right anchor
# is fixed regardless of which indicators are visible, so the
# rightmost slot stays in the same place whether the cell has
# one indicator or two.
from PySide6.QtGui import QFont from PySide6.QtGui import QFont
p.setPen(self._bookmarked_color) slot_size = 9
p.setFont(QFont(p.font().family(), 8)) slot_gap = 2
indicator_x -= 11 slot_y = content.top() + 3
p.drawText(indicator_x, content.top() + 14, "\u2605") right_anchor = content.right() - 3
if self._missing:
# Build the row right-to-left so we can decrement x as we draw.
# Right slot (drawn first): the saved-locally dot.
# Left slot (drawn second): the bookmark star.
draw_order: list[tuple[str, QColor]] = []
if self._saved_locally:
draw_order.append(('dot', self._saved_color))
if self._bookmarked:
draw_order.append(('star', self._bookmarked_color))
x = right_anchor - slot_size
for kind, color in draw_order:
slot = QRect(x, slot_y, slot_size, slot_size)
if kind == 'dot':
p.setPen(Qt.PenStyle.NoPen) p.setPen(Qt.PenStyle.NoPen)
p.setBrush(self._missing_color) p.setBrush(color)
indicator_x -= 9 # 1px inset so the circle doesn't kiss the slot edge —
p.drawEllipse(indicator_x, content.top() + 4, 7, 7) # makes it look slightly less stamped-on at small sizes.
elif self._saved_locally: p.drawEllipse(slot.adjusted(1, 1, -1, -1))
p.setPen(Qt.PenStyle.NoPen) elif kind == 'star':
p.setBrush(self._saved_color) p.setPen(color)
indicator_x -= 9 p.setBrush(Qt.BrushStyle.NoBrush)
p.drawEllipse(indicator_x, content.top() + 4, 7, 7) p.setFont(QFont(p.font().family(), 9))
p.drawText(slot, int(Qt.AlignmentFlag.AlignCenter), "\u2605")
x -= (slot_size + slot_gap)
# Multi-select checkmark # Multi-select checkmark
if self._multi_selected: if self._multi_selected: