237 lines
7.1 KiB
Python
237 lines
7.1 KiB
Python
|
|
# coding:utf-8
|
||
|
|
from typing import List
|
||
|
|
|
||
|
|
from PySide6.QtCore import QSize, QPoint, Qt, QRect, QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, QEvent, QTimer, QObject
|
||
|
|
from PySide6.QtWidgets import QLayout, QWidgetItem, QLayoutItem
|
||
|
|
|
||
|
|
|
||
|
|
class FlowLayout(QLayout):
|
||
|
|
""" Flow layout """
|
||
|
|
|
||
|
|
def __init__(self, parent=None, needAni=False, isTight=False):
|
||
|
|
"""
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
parent:
|
||
|
|
parent window or layout
|
||
|
|
|
||
|
|
needAni: bool
|
||
|
|
whether to add moving animation
|
||
|
|
|
||
|
|
isTight: bool
|
||
|
|
whether to use the tight layout when widgets are hidden
|
||
|
|
"""
|
||
|
|
super().__init__(parent)
|
||
|
|
self._items = [] # type: List[QLayoutItem]
|
||
|
|
self._anis = [] # type: List[QPropertyAnimation]
|
||
|
|
self._aniGroup = QParallelAnimationGroup(self)
|
||
|
|
self._verticalSpacing = 10
|
||
|
|
self._horizontalSpacing = 10
|
||
|
|
self.duration = 300
|
||
|
|
self.ease = QEasingCurve.Linear
|
||
|
|
self.needAni = needAni
|
||
|
|
self.isTight = isTight
|
||
|
|
self._deBounceTimer = QTimer(self)
|
||
|
|
self._deBounceTimer.setSingleShot(True)
|
||
|
|
self._deBounceTimer.timeout.connect(lambda: self._doLayout(self.geometry(), True))
|
||
|
|
self._wParent = None
|
||
|
|
self._isInstalledEventFilter = False
|
||
|
|
|
||
|
|
def addItem(self, item):
|
||
|
|
self._items.append(item)
|
||
|
|
|
||
|
|
def insertItem(self, index, item):
|
||
|
|
self._items.insert(index, item)
|
||
|
|
|
||
|
|
def addWidget(self, w):
|
||
|
|
super().addWidget(w)
|
||
|
|
self._onWidgetAdded(w)
|
||
|
|
|
||
|
|
def insertWidget(self, index, w):
|
||
|
|
self.insertItem(index, QWidgetItem(w))
|
||
|
|
self.addChildWidget(w)
|
||
|
|
self._onWidgetAdded(w, index)
|
||
|
|
|
||
|
|
def _onWidgetAdded(self, w, index=-1):
|
||
|
|
if not self._isInstalledEventFilter:
|
||
|
|
if w.parent():
|
||
|
|
self._wParent = w.parent()
|
||
|
|
w.parent().installEventFilter(self)
|
||
|
|
else:
|
||
|
|
w.installEventFilter(self)
|
||
|
|
|
||
|
|
if not self.needAni:
|
||
|
|
return
|
||
|
|
|
||
|
|
ani = QPropertyAnimation(w, b'geometry')
|
||
|
|
ani.setEndValue(QRect(QPoint(0, 0), w.size()))
|
||
|
|
ani.setDuration(self.duration)
|
||
|
|
ani.setEasingCurve(self.ease)
|
||
|
|
w.setProperty('flowAni', ani)
|
||
|
|
self._aniGroup.addAnimation(ani)
|
||
|
|
|
||
|
|
if index == -1:
|
||
|
|
self._anis.append(ani)
|
||
|
|
else:
|
||
|
|
self._anis.insert(index, ani)
|
||
|
|
|
||
|
|
def setAnimation(self, duration, ease=QEasingCurve.Linear):
|
||
|
|
""" set the moving animation
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
duration: int
|
||
|
|
the duration of animation in milliseconds
|
||
|
|
|
||
|
|
ease: QEasingCurve
|
||
|
|
the easing curve of animation
|
||
|
|
"""
|
||
|
|
if not self.needAni:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.duration = duration
|
||
|
|
self.ease = ease
|
||
|
|
|
||
|
|
for ani in self._anis:
|
||
|
|
ani.setDuration(duration)
|
||
|
|
ani.setEasingCurve(ease)
|
||
|
|
|
||
|
|
def count(self):
|
||
|
|
return len(self._items)
|
||
|
|
|
||
|
|
def itemAt(self, index: int):
|
||
|
|
if 0 <= index < len(self._items):
|
||
|
|
return self._items[index]
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
def takeAt(self, index: int):
|
||
|
|
if 0 <= index < len(self._items):
|
||
|
|
item = self._items[index] # type: QLayoutItem
|
||
|
|
ani = item.widget().property('flowAni')
|
||
|
|
if ani:
|
||
|
|
self._anis.remove(ani)
|
||
|
|
self._aniGroup.removeAnimation(ani)
|
||
|
|
ani.deleteLater()
|
||
|
|
|
||
|
|
return self._items.pop(index).widget()
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
def removeWidget(self, widget):
|
||
|
|
for i, item in enumerate(self._items):
|
||
|
|
if item.widget() is widget:
|
||
|
|
return self.takeAt(i)
|
||
|
|
|
||
|
|
def removeAllWidgets(self):
|
||
|
|
""" remove all widgets from layout """
|
||
|
|
while self._items:
|
||
|
|
self.takeAt(0)
|
||
|
|
|
||
|
|
def takeAllWidgets(self):
|
||
|
|
""" remove all widgets from layout and delete them """
|
||
|
|
while self._items:
|
||
|
|
w = self.takeAt(0)
|
||
|
|
if w:
|
||
|
|
w.deleteLater()
|
||
|
|
|
||
|
|
def expandingDirections(self):
|
||
|
|
return Qt.Orientation(0)
|
||
|
|
|
||
|
|
def hasHeightForWidth(self):
|
||
|
|
return True
|
||
|
|
|
||
|
|
def heightForWidth(self, width: int):
|
||
|
|
""" get the minimal height according to width """
|
||
|
|
return self._doLayout(QRect(0, 0, width, 0), False)
|
||
|
|
|
||
|
|
def setGeometry(self, rect: QRect):
|
||
|
|
super().setGeometry(rect)
|
||
|
|
|
||
|
|
if self.needAni:
|
||
|
|
self._deBounceTimer.start(80)
|
||
|
|
else:
|
||
|
|
self._doLayout(rect, True)
|
||
|
|
|
||
|
|
def sizeHint(self):
|
||
|
|
return self.minimumSize()
|
||
|
|
|
||
|
|
def minimumSize(self):
|
||
|
|
size = QSize()
|
||
|
|
|
||
|
|
for item in self._items:
|
||
|
|
size = size.expandedTo(item.minimumSize())
|
||
|
|
|
||
|
|
m = self.contentsMargins()
|
||
|
|
size += QSize(m.left()+m.right(), m.top()+m.bottom())
|
||
|
|
|
||
|
|
return size
|
||
|
|
|
||
|
|
def setVerticalSpacing(self, spacing: int):
|
||
|
|
""" set vertical spacing between widgets """
|
||
|
|
self._verticalSpacing = spacing
|
||
|
|
|
||
|
|
def verticalSpacing(self):
|
||
|
|
""" get vertical spacing between widgets """
|
||
|
|
return self._verticalSpacing
|
||
|
|
|
||
|
|
def setHorizontalSpacing(self, spacing: int):
|
||
|
|
""" set horizontal spacing between widgets """
|
||
|
|
self._horizontalSpacing = spacing
|
||
|
|
|
||
|
|
def horizontalSpacing(self):
|
||
|
|
""" get horizontal spacing between widgets """
|
||
|
|
return self._horizontalSpacing
|
||
|
|
|
||
|
|
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||
|
|
if obj in [w.widget() for w in self._items] and event.type() == QEvent.Type.ParentChange:
|
||
|
|
self._wParent = obj.parent()
|
||
|
|
obj.parent().installEventFilter(self)
|
||
|
|
self._isInstalledEventFilter = True
|
||
|
|
|
||
|
|
if obj == self._wParent and event.type() == QEvent.Type.Show:
|
||
|
|
self._doLayout(self.geometry(), True)
|
||
|
|
self._isInstalledEventFilter = True
|
||
|
|
|
||
|
|
return super().eventFilter(obj, event)
|
||
|
|
|
||
|
|
def _doLayout(self, rect: QRect, move: bool):
|
||
|
|
""" adjust widgets position according to the window size """
|
||
|
|
aniRestart = False
|
||
|
|
margin = self.contentsMargins()
|
||
|
|
x = rect.x() + margin.left()
|
||
|
|
y = rect.y() + margin.top()
|
||
|
|
rowHeight = 0
|
||
|
|
spaceX = self.horizontalSpacing()
|
||
|
|
spaceY = self.verticalSpacing()
|
||
|
|
|
||
|
|
for i, item in enumerate(self._items):
|
||
|
|
if item.widget() and not item.widget().isVisible() and self.isTight:
|
||
|
|
continue
|
||
|
|
|
||
|
|
nextX = x + item.sizeHint().width() + spaceX
|
||
|
|
|
||
|
|
if nextX - spaceX > rect.right() - margin.right() and rowHeight > 0:
|
||
|
|
x = rect.x() + margin.left()
|
||
|
|
y = y + rowHeight + spaceY
|
||
|
|
nextX = x + item.sizeHint().width() + spaceX
|
||
|
|
rowHeight = 0
|
||
|
|
|
||
|
|
if move:
|
||
|
|
target = QRect(QPoint(x, y), item.sizeHint())
|
||
|
|
if not self.needAni:
|
||
|
|
item.setGeometry(target)
|
||
|
|
elif target != self._anis[i].endValue():
|
||
|
|
self._anis[i].stop()
|
||
|
|
self._anis[i].setEndValue(target)
|
||
|
|
aniRestart = True
|
||
|
|
|
||
|
|
x = nextX
|
||
|
|
rowHeight = max(rowHeight, item.sizeHint().height())
|
||
|
|
|
||
|
|
if self.needAni and aniRestart:
|
||
|
|
self._aniGroup.stop()
|
||
|
|
self._aniGroup.start()
|
||
|
|
|
||
|
|
return y + rowHeight + margin.bottom() - rect.y()
|