initial fluent-widgets ui
This commit is contained in:
430
qfluentwidgets/components/widgets/flip_view.py
Normal file
430
qfluentwidgets/components/widgets/flip_view.py
Normal file
@ -0,0 +1,430 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user