# coding:utf-8 from typing import List, Union from PySide6.QtCore import Qt, Signal, QModelIndex, QSize, Property, QRectF, QPropertyAnimation, QSizeF from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QWheelEvent, QPainterPath, QImageReader from PySide6.QtWidgets import QStyleOptionViewItem, QListWidget, QStyledItemDelegate, QListWidgetItem from ...common.overload import singledispatchmethod from ...common.style_sheet import isDarkTheme, FluentStyleSheet from ...common.icon import drawIcon, FluentIcon from .scroll_bar import SmoothScrollBar from .button import ToolButton class ScrollButton(ToolButton): """ Scroll button """ def _postInit(self): self._opacity = 0 self.opacityAni = QPropertyAnimation(self, b'opacity', self) self.opacityAni.setDuration(150) def getOpacity(self): return self._opacity def setOpacity(self, o: float): self._opacity = o self.update() def isTransparent(self): return self.opacity == 0 def fadeIn(self): self.opacityAni.setStartValue(self.opacity) self.opacityAni.setEndValue(1) self.opacityAni.start() def fadeOut(self): self.opacityAni.setStartValue(self.opacity) self.opacityAni.setEndValue(0) self.opacityAni.start() def paintEvent(self, e): painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing) painter.setPen(Qt.NoPen) painter.setOpacity(self.opacity) # draw background if not isDarkTheme(): painter.setBrush(QColor(252, 252, 252, 217)) else: painter.setBrush(QColor(44, 44, 44, 245)) painter.drawRoundedRect(self.rect(), 4, 4) # draw icon if isDarkTheme(): color = QColor(255, 255, 255) opacity = 0.773 if self.isHover or self.isPressed else 0.541 else: color = QColor(0, 0, 0) opacity = 0.616 if self.isHover or self.isPressed else 0.45 painter.setOpacity(self.opacity * opacity) s = 6 if self.isPressed else 8 w, h = self.width(), self.height() x, y = (w - s) / 2, (h - s) / 2 drawIcon(self._icon, painter, QRectF(x, y, s, s), fill=color.name()) opacity = Property(float, getOpacity, setOpacity) class FlipImageDelegate(QStyledItemDelegate): """ Flip view image delegate """ def __init__(self, parent=None): super().__init__(parent) self.borderRadius = 0 def itemSize(self, index: int): p = self.parent() # type: FlipView return p.item(index).sizeHint() def setBorderRadius(self, radius: int): self.borderRadius = radius self.parent().viewport().update() def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex): painter.save() painter.setRenderHints(QPainter.Antialiasing) painter.setPen(Qt.NoPen) size = self.itemSize(index.row()) # type: QSize p = self.parent() # type: FlipView # draw image r = p.devicePixelRatioF() image = index.data(Qt.UserRole) # type: QImage if image is None: return painter.restore() # lazy load image if image.isNull() and index.data(Qt.ItemDataRole.DisplayRole): image.load(index.data(Qt.ItemDataRole.DisplayRole)) index.model().setData(index, image, Qt.ItemDataRole.UserRole) x = option.rect.x() + int((option.rect.width() - size.width()) / 2) y = option.rect.y() + int((option.rect.height() - size.height()) / 2) rect = QRectF(x, y, size.width(), size.height()) # clipped path path = QPainterPath() path.addRoundedRect(rect, self.borderRadius, self.borderRadius) subPath = QPainterPath() subPath.addRoundedRect(QRectF(p.rect()), self.borderRadius, self.borderRadius) path = path.intersected(subPath) image = image.scaled(size * r, p.aspectRatioMode, Qt.SmoothTransformation) painter.setClipPath(path) # center crop image if p.aspectRatioMode == Qt.AspectRatioMode.KeepAspectRatioByExpanding: iw, ih = image.width(), image.height() size = QSizeF(size) * r x, y = (iw - size.width()) / 2, (ih - size.height()) / 2 image = image.copy(int(x), int(y), int(size.width()), int(size.height())) painter.drawImage(rect, image) painter.restore() class FlipView(QListWidget): """ Flip view Constructors ------------ * FlipView(`parent`: QWidget = None) * FlipView(`orient`: Qt.Orientation, `parent`: QWidget = None) """ currentIndexChanged = Signal(int) @singledispatchmethod def __init__(self, parent=None): super().__init__(parent=parent) self.orientation = Qt.Horizontal self._postInit() @__init__.register def _(self, orientation: Qt.Orientation, parent=None): super().__init__(parent=parent) self.orientation = orientation self._postInit() def _postInit(self): self.isHover = False self._currentIndex = -1 self._aspectRatioMode = Qt.AspectRatioMode.IgnoreAspectRatio self._itemSize = QSize(480, 270) # 16:9 self.delegate = FlipImageDelegate(self) self.scrollBar = SmoothScrollBar(self.orientation, self) self.scrollBar.setScrollAnimation(500) self.scrollBar.setForceHidden(True) # self.setUniformItemSizes(True) self.setMinimumSize(self.itemSize) self.setItemDelegate(self.delegate) self.setMovement(QListWidget.Static) self.setVerticalScrollMode(self.ScrollMode.ScrollPerPixel) self.setHorizontalScrollMode(self.ScrollMode.ScrollPerPixel) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) FluentStyleSheet.FLIP_VIEW.apply(self) if self.isHorizontal(): self.setFlow(QListWidget.LeftToRight) self.preButton = ScrollButton(FluentIcon.CARE_LEFT_SOLID, self) self.nextButton = ScrollButton(FluentIcon.CARE_RIGHT_SOLID, self) self.preButton.setFixedSize(16, 38) self.nextButton.setFixedSize(16, 38) else: self.preButton = ScrollButton(FluentIcon.CARE_UP_SOLID, self) self.nextButton = ScrollButton(FluentIcon.CARE_DOWN_SOLID, self) self.preButton.setFixedSize(38, 16) self.nextButton.setFixedSize(38, 16) # connect signal to slot self.preButton.clicked.connect(self.scrollPrevious) self.nextButton.clicked.connect(self.scrollNext) def isHorizontal(self): return self.orientation == Qt.Horizontal def setItemSize(self, size: QSize): """ set the size of item """ if size == self.itemSize: return self._itemSize = size for i in range(self.count()): self._adjustItemSize(self.item(i)) self.viewport().update() def getItemSize(self): """ get the size of item """ return self._itemSize def setBorderRadius(self, radius: int): """ set the border radius of item """ self.delegate.setBorderRadius(radius) def getBorderRadius(self): return self.delegate.borderRadius def scrollPrevious(self): """ scroll to previous item """ self.setCurrentIndex(self.currentIndex() - 1) def scrollNext(self): """ scroll to next item """ self.setCurrentIndex(self.currentIndex() + 1) def setCurrentIndex(self, index: int): """ set current index """ if not 0 <= index < self.count() or index == self.currentIndex(): return self.scrollToIndex(index) # update the visibility of scroll button if index == 0: self.preButton.fadeOut() elif self.preButton.isTransparent() and self.isHover: self.preButton.fadeIn() if index == self.count() - 1: self.nextButton.fadeOut() elif self.nextButton.isTransparent() and self.isHover: self.nextButton.fadeIn() # fire signal self.currentIndexChanged.emit(index) def scrollToIndex(self, index): if not 0 <= index < self.count(): return self._currentIndex = index if self.isHorizontal(): value = sum(self.item(i).sizeHint().width() for i in range(index)) else: value = sum(self.item(i).sizeHint().height() for i in range(index)) value += (2 * index + 1) * self.spacing() self.scrollBar.scrollTo(value) def currentIndex(self): return self._currentIndex def image(self, index: int): if not 0 <= index < self.count(): return QImage() return self.item(index).data(Qt.UserRole) def addImage(self, image: Union[QImage, QPixmap, str]): """ add image """ self.addImages([image]) def addImages(self, images: List[Union[QImage, QPixmap, str]], targetSize: QSize = None): """ add images """ if not images: return N = self.count() self.addItems([''] * len(images)) for i in range(N, self.count()): self.setItemImage(i, images[i - N], targetSize=targetSize) if self.currentIndex() < 0: self._currentIndex = 0 def setItemImage(self, index: int, image: Union[QImage, QPixmap, str], targetSize: QSize = None): """ set the image of specified item """ if not 0 <= index < self.count(): return item = self.item(index) # convert image to QImage if isinstance(image, QPixmap): image = image.toImage() # lazy load if isinstance(image, QImage): item.setData(Qt.ItemDataRole.UserRole, image) else: item.setData(Qt.ItemDataRole.UserRole, QImage()) item.setData(Qt.ItemDataRole.DisplayRole, image) self._adjustItemSize(item) def _adjustItemSize(self, item: QListWidgetItem): image = self.itemImage(self.row(item), load=False) if not image.isNull(): size = image.size() else: imagePath = item.data(Qt.ItemDataRole.DisplayRole) or "" size = QImageReader(imagePath).size().expandedTo(QSize(1, 1)) if self.aspectRatioMode == Qt.AspectRatioMode.KeepAspectRatio: if self.isHorizontal(): h = self.itemSize.height() w = int(size.width() * h / size.height()) else: w = self.itemSize.width() h = int(size.height() * w / size.width()) else: w, h = self.itemSize.width(), self.itemSize.height() item.setSizeHint(QSize(w, h)) def itemImage(self, index: int, load=True) -> QImage: """ get the image of specified item Parameters ---------- index: int the index of image load: bool whether to load image data """ if not 0 <= index < self.count(): return item = self.item(index) image = item.data(Qt.ItemDataRole.UserRole) # type: QImage if image is None: return QImage() imagePath = item.data(Qt.ItemDataRole.DisplayRole) if image.isNull() and imagePath and load: image.load(imagePath) return image def resizeEvent(self, e): w, h = self.width(), self.height() bw, bh = self.preButton.width(), self.preButton.height() if self.isHorizontal(): self.preButton.move(2, int(h / 2 - bh / 2)) self.nextButton.move(w - bw - 2, int(h / 2 - bh / 2)) else: self.preButton.move(int(w / 2 - bw / 2), 2) self.nextButton.move(int(w / 2 - bw / 2), h - bh - 2) def enterEvent(self, e): super().enterEvent(e) self.isHover = True if self.currentIndex() > 0: self.preButton.fadeIn() if self.currentIndex() < self.count() - 1: self.nextButton.fadeIn() def leaveEvent(self, e): super().leaveEvent(e) self.isHover = False self.preButton.fadeOut() self.nextButton.fadeOut() def showEvent(self, e): self.scrollBar.duration = 0 self.scrollToIndex(self.currentIndex()) self.scrollBar.duration = 500 def wheelEvent(self, e: QWheelEvent): e.setAccepted(True) if self.scrollBar.ani.state() == QPropertyAnimation.Running: return if e.angleDelta().y() < 0: self.scrollNext() else: self.scrollPrevious() def getAspectRatioMode(self): return self._aspectRatioMode def setAspectRatioMode(self, mode: Qt.AspectRatioMode): if mode == self.aspectRatioMode: return self._aspectRatioMode = mode for i in range(self.count()): self._adjustItemSize(self.item(i)) self.viewport().update() itemSize = Property(QSize, getItemSize, setItemSize) borderRadius = Property(int, getBorderRadius, setBorderRadius) aspectRatioMode = Property(Qt.AspectRatioMode, getAspectRatioMode, setAspectRatioMode) class HorizontalFlipView(FlipView): """ Horizontal flip view """ def __init__(self, parent=None): super().__init__(Qt.Horizontal, parent) class VerticalFlipView(FlipView): """ Vertical flip view """ def __init__(self, parent=None): super().__init__(Qt.Vertical, parent)