Square thumbnail selection border, Qt-targetable selection colors, indicator row swap, drop dead missing-indicator code
This commit is contained in:
parent
0eab860088
commit
3824d382c3
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user