Python-FastUI-Widgets 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastuiwidgets/__init__.py +12 -0
- fastuiwidgets/_rc/__init__.py +0 -0
- fastuiwidgets/_rc/resource.py +98835 -0
- fastuiwidgets/common/__init__.py +12 -0
- fastuiwidgets/common/animation.py +530 -0
- fastuiwidgets/common/auto_wrap.py +164 -0
- fastuiwidgets/common/color.py +95 -0
- fastuiwidgets/common/config.py +423 -0
- fastuiwidgets/common/exception_handler.py +31 -0
- fastuiwidgets/common/font.py +38 -0
- fastuiwidgets/common/icon.py +703 -0
- fastuiwidgets/common/image_utils.py +198 -0
- fastuiwidgets/common/overload.py +47 -0
- fastuiwidgets/common/router.py +133 -0
- fastuiwidgets/common/screen.py +25 -0
- fastuiwidgets/common/smooth_scroll.py +141 -0
- fastuiwidgets/common/style_sheet.py +512 -0
- fastuiwidgets/common/theme_listener.py +27 -0
- fastuiwidgets/common/translator.py +14 -0
- fastuiwidgets/components/__init__.py +6 -0
- fastuiwidgets/components/date_time/__init__.py +4 -0
- fastuiwidgets/components/date_time/calendar_picker.py +121 -0
- fastuiwidgets/components/date_time/calendar_view.py +671 -0
- fastuiwidgets/components/date_time/date_picker.py +245 -0
- fastuiwidgets/components/date_time/fast_calendar_view.py +487 -0
- fastuiwidgets/components/date_time/picker_base.py +632 -0
- fastuiwidgets/components/date_time/time_picker.py +223 -0
- fastuiwidgets/components/dialog_box/__init__.py +6 -0
- fastuiwidgets/components/dialog_box/color_dialog.py +414 -0
- fastuiwidgets/components/dialog_box/dialog.py +167 -0
- fastuiwidgets/components/dialog_box/folder_list_dialog.py +307 -0
- fastuiwidgets/components/dialog_box/mask_dialog_base.py +120 -0
- fastuiwidgets/components/dialog_box/message_box_base.py +92 -0
- fastuiwidgets/components/dialog_box/message_dialog.py +65 -0
- fastuiwidgets/components/layout/__init__.py +3 -0
- fastuiwidgets/components/layout/expand_layout.py +96 -0
- fastuiwidgets/components/layout/flow_layout.py +236 -0
- fastuiwidgets/components/layout/v_box_layout.py +41 -0
- fastuiwidgets/components/material/__init__.py +6 -0
- fastuiwidgets/components/material/acrylic_combo_box.py +96 -0
- fastuiwidgets/components/material/acrylic_flyout.py +105 -0
- fastuiwidgets/components/material/acrylic_line_edit.py +27 -0
- fastuiwidgets/components/material/acrylic_menu.py +204 -0
- fastuiwidgets/components/material/acrylic_tool_tip.py +39 -0
- fastuiwidgets/components/material/acrylic_widget.py +42 -0
- fastuiwidgets/components/navigation/__init__.py +9 -0
- fastuiwidgets/components/navigation/breadcrumb.py +350 -0
- fastuiwidgets/components/navigation/navigation_bar.py +416 -0
- fastuiwidgets/components/navigation/navigation_interface.py +268 -0
- fastuiwidgets/components/navigation/navigation_panel.py +657 -0
- fastuiwidgets/components/navigation/navigation_widget.py +686 -0
- fastuiwidgets/components/navigation/pivot.py +272 -0
- fastuiwidgets/components/navigation/segmented_widget.py +174 -0
- fastuiwidgets/components/settings/__init__.py +8 -0
- fastuiwidgets/components/settings/custom_color_setting_card.py +139 -0
- fastuiwidgets/components/settings/expand_setting_card.py +390 -0
- fastuiwidgets/components/settings/folder_list_setting_card.py +134 -0
- fastuiwidgets/components/settings/options_setting_card.py +86 -0
- fastuiwidgets/components/settings/setting_card.py +449 -0
- fastuiwidgets/components/settings/setting_card_group.py +48 -0
- fastuiwidgets/components/widgets/__init__.py +41 -0
- fastuiwidgets/components/widgets/acrylic_label.py +261 -0
- fastuiwidgets/components/widgets/button.py +1059 -0
- fastuiwidgets/components/widgets/card_widget.py +369 -0
- fastuiwidgets/components/widgets/check_box.py +203 -0
- fastuiwidgets/components/widgets/combo_box.py +556 -0
- fastuiwidgets/components/widgets/command_bar.py +636 -0
- fastuiwidgets/components/widgets/cycle_list_widget.py +251 -0
- fastuiwidgets/components/widgets/flip_view.py +430 -0
- fastuiwidgets/components/widgets/flyout.py +521 -0
- fastuiwidgets/components/widgets/frameless_window.py +49 -0
- fastuiwidgets/components/widgets/icon_widget.py +53 -0
- fastuiwidgets/components/widgets/info_badge.py +483 -0
- fastuiwidgets/components/widgets/info_bar.py +596 -0
- fastuiwidgets/components/widgets/label.py +553 -0
- fastuiwidgets/components/widgets/line_edit.py +551 -0
- fastuiwidgets/components/widgets/list_view.py +158 -0
- fastuiwidgets/components/widgets/menu.py +1318 -0
- fastuiwidgets/components/widgets/pips_pager.py +331 -0
- fastuiwidgets/components/widgets/progress_bar.py +311 -0
- fastuiwidgets/components/widgets/progress_ring.py +212 -0
- fastuiwidgets/components/widgets/scroll_area.py +125 -0
- fastuiwidgets/components/widgets/scroll_bar.py +673 -0
- fastuiwidgets/components/widgets/separator.py +43 -0
- fastuiwidgets/components/widgets/slider.py +307 -0
- fastuiwidgets/components/widgets/spin_box.py +306 -0
- fastuiwidgets/components/widgets/stacked_widget.py +211 -0
- fastuiwidgets/components/widgets/state_tool_tip.py +188 -0
- fastuiwidgets/components/widgets/switch_button.py +312 -0
- fastuiwidgets/components/widgets/tab_view.py +804 -0
- fastuiwidgets/components/widgets/table_view.py +360 -0
- fastuiwidgets/components/widgets/teaching_tip.py +657 -0
- fastuiwidgets/components/widgets/tool_tip.py +460 -0
- fastuiwidgets/components/widgets/tree_view.py +216 -0
- fastuiwidgets/multimedia/__init__.py +3 -0
- fastuiwidgets/multimedia/media_play_bar.py +319 -0
- fastuiwidgets/multimedia/media_player.py +124 -0
- fastuiwidgets/multimedia/video_widget.py +93 -0
- fastuiwidgets/window/__init__.py +2 -0
- fastuiwidgets/window/fluent_window.py +413 -0
- fastuiwidgets/window/splash_screen.py +92 -0
- fastuiwidgets/window/stacked_widget.py +66 -0
- python_fastui_widgets-1.0.0.dist-info/METADATA +30 -0
- python_fastui_widgets-1.0.0.dist-info/RECORD +107 -0
- python_fastui_widgets-1.0.0.dist-info/WHEEL +5 -0
- python_fastui_widgets-1.0.0.dist-info/licenses/LICENSE +674 -0
- python_fastui_widgets-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,251 @@
|
|
1
|
+
# coding:utf-8
|
2
|
+
from typing import Iterable
|
3
|
+
|
4
|
+
from PySide6.QtCore import Qt, Signal, QSize, QEvent, QRectF
|
5
|
+
from PySide6.QtGui import QPainter
|
6
|
+
from PySide6.QtWidgets import QListWidget, QListWidgetItem, QToolButton
|
7
|
+
|
8
|
+
from .scroll_area import SmoothScrollBar
|
9
|
+
from ...common.icon import FluentIcon, isDarkTheme
|
10
|
+
|
11
|
+
|
12
|
+
class ScrollButton(QToolButton):
|
13
|
+
""" Scroll button """
|
14
|
+
|
15
|
+
def __init__(self, icon: FluentIcon, parent=None):
|
16
|
+
super().__init__(parent=parent)
|
17
|
+
self._icon = icon
|
18
|
+
self.isPressed = False
|
19
|
+
self.installEventFilter(self)
|
20
|
+
|
21
|
+
def eventFilter(self, obj, e: QEvent):
|
22
|
+
if obj is self:
|
23
|
+
if e.type() == QEvent.MouseButtonPress:
|
24
|
+
self.isPressed = True
|
25
|
+
self.update()
|
26
|
+
elif e.type() == QEvent.MouseButtonRelease:
|
27
|
+
self.isPressed = False
|
28
|
+
self.update()
|
29
|
+
|
30
|
+
return super().eventFilter(obj, e)
|
31
|
+
|
32
|
+
def paintEvent(self, e):
|
33
|
+
super().paintEvent(e)
|
34
|
+
painter = QPainter(self)
|
35
|
+
painter.setRenderHints(QPainter.Antialiasing)
|
36
|
+
|
37
|
+
if not self.isPressed:
|
38
|
+
w, h = 10, 10
|
39
|
+
else:
|
40
|
+
w, h = 8, 8
|
41
|
+
|
42
|
+
x = (self.width() - w) / 2
|
43
|
+
y = (self.height() - h) / 2
|
44
|
+
|
45
|
+
if not isDarkTheme():
|
46
|
+
self._icon.render(painter, QRectF(x, y, w, h), fill="#5e5e5e")
|
47
|
+
else:
|
48
|
+
self._icon.render(painter, QRectF(x, y, w, h))
|
49
|
+
|
50
|
+
|
51
|
+
class CycleListWidget(QListWidget):
|
52
|
+
""" Cycle list widget """
|
53
|
+
|
54
|
+
currentItemChanged = Signal(QListWidgetItem)
|
55
|
+
|
56
|
+
def __init__(self, items: Iterable, itemSize: QSize, align=Qt.AlignCenter, parent=None):
|
57
|
+
"""
|
58
|
+
Parameters
|
59
|
+
----------
|
60
|
+
items: Iterable[Any]
|
61
|
+
the items to be added
|
62
|
+
|
63
|
+
itemSize: QSize
|
64
|
+
the size of item
|
65
|
+
|
66
|
+
align: Qt.AlignmentFlag
|
67
|
+
the text alignment of item
|
68
|
+
|
69
|
+
parent: QWidget
|
70
|
+
parent widget
|
71
|
+
"""
|
72
|
+
super().__init__(parent=parent)
|
73
|
+
self.itemSize = itemSize
|
74
|
+
self.align = align
|
75
|
+
|
76
|
+
self.upButton = ScrollButton(FluentIcon.CARE_UP_SOLID, self)
|
77
|
+
self.downButton = ScrollButton(FluentIcon.CARE_DOWN_SOLID, self)
|
78
|
+
self.scrollDuration = 250
|
79
|
+
self.originItems = list(items)
|
80
|
+
|
81
|
+
self.vScrollBar = SmoothScrollBar(Qt.Vertical, self)
|
82
|
+
self.visibleNumber = 9
|
83
|
+
|
84
|
+
# repeat adding items to achieve circular scrolling
|
85
|
+
self.setItems(items)
|
86
|
+
|
87
|
+
self.setVerticalScrollMode(self.ScrollMode.ScrollPerPixel)
|
88
|
+
self.vScrollBar.setScrollAnimation(self.scrollDuration)
|
89
|
+
self.vScrollBar.setForceHidden(True)
|
90
|
+
|
91
|
+
self.setViewportMargins(0, 0, 0, 0)
|
92
|
+
self.setFixedSize(itemSize.width()+8,
|
93
|
+
itemSize.height()*self.visibleNumber)
|
94
|
+
|
95
|
+
# hide scroll bar
|
96
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
97
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
98
|
+
self.upButton.hide()
|
99
|
+
self.downButton.hide()
|
100
|
+
|
101
|
+
self.upButton.clicked.connect(self.scrollUp)
|
102
|
+
self.downButton.clicked.connect(self.scrollDown)
|
103
|
+
self.itemClicked.connect(self._onItemClicked)
|
104
|
+
|
105
|
+
self.installEventFilter(self)
|
106
|
+
|
107
|
+
def setItems(self, items: list):
|
108
|
+
""" set items in the list
|
109
|
+
|
110
|
+
Parameters
|
111
|
+
----------
|
112
|
+
items: Iterable[Any]
|
113
|
+
the items to be added
|
114
|
+
|
115
|
+
itemSize: QSize
|
116
|
+
the size of item
|
117
|
+
|
118
|
+
align: Qt.AlignmentFlag
|
119
|
+
the text alignment of item
|
120
|
+
"""
|
121
|
+
self.clear()
|
122
|
+
self._createItems(items)
|
123
|
+
|
124
|
+
def _createItems(self, items: list):
|
125
|
+
N = len(items)
|
126
|
+
self.isCycle = N > self.visibleNumber
|
127
|
+
|
128
|
+
if self.isCycle:
|
129
|
+
for _ in range(2):
|
130
|
+
self._addColumnItems(items)
|
131
|
+
|
132
|
+
self._currentIndex = len(items)
|
133
|
+
super().scrollToItem(
|
134
|
+
self.item(self.currentIndex()-self.visibleNumber//2), QListWidget.PositionAtTop)
|
135
|
+
else:
|
136
|
+
n = self.visibleNumber // 2 # add empty items to enable scrolling
|
137
|
+
|
138
|
+
self._addColumnItems(['']*n, True)
|
139
|
+
self._addColumnItems(items)
|
140
|
+
self._addColumnItems(['']*n, True)
|
141
|
+
|
142
|
+
self._currentIndex = n
|
143
|
+
|
144
|
+
def _addColumnItems(self, items, disabled=False):
|
145
|
+
for i in items:
|
146
|
+
item = QListWidgetItem(str(i), self)
|
147
|
+
item.setSizeHint(self.itemSize)
|
148
|
+
item.setTextAlignment(self.align | Qt.AlignVCenter)
|
149
|
+
if disabled:
|
150
|
+
item.setFlags(Qt.NoItemFlags)
|
151
|
+
|
152
|
+
self.addItem(item)
|
153
|
+
|
154
|
+
def _onItemClicked(self, item):
|
155
|
+
self.setCurrentIndex(self.row(item))
|
156
|
+
self.scrollToItem(self.currentItem())
|
157
|
+
|
158
|
+
def setSelectedItem(self, text: str):
|
159
|
+
""" set the selected item """
|
160
|
+
if text is None:
|
161
|
+
return
|
162
|
+
|
163
|
+
items = self.findItems(str(text), Qt.MatchExactly)
|
164
|
+
if not items:
|
165
|
+
return
|
166
|
+
|
167
|
+
if len(items) >= 2:
|
168
|
+
self.setCurrentIndex(self.row(items[1]))
|
169
|
+
else:
|
170
|
+
self.setCurrentIndex(self.row(items[0]))
|
171
|
+
|
172
|
+
super().scrollToItem(self.currentItem(), QListWidget.ScrollHint.PositionAtCenter)
|
173
|
+
|
174
|
+
def scrollToItem(self, item: QListWidgetItem, hint=QListWidget.ScrollHint.PositionAtCenter):
|
175
|
+
""" scroll to item """
|
176
|
+
# scroll to center position
|
177
|
+
index = self.row(item)
|
178
|
+
y = item.sizeHint().height() * (index - self.visibleNumber // 2)
|
179
|
+
self.vScrollBar.scrollTo(y)
|
180
|
+
|
181
|
+
# clear selection
|
182
|
+
self.clearSelection()
|
183
|
+
item.setSelected(False)
|
184
|
+
|
185
|
+
self.currentItemChanged.emit(item)
|
186
|
+
|
187
|
+
def wheelEvent(self, e):
|
188
|
+
if e.angleDelta().y() < 0:
|
189
|
+
self.scrollDown()
|
190
|
+
else:
|
191
|
+
self.scrollUp()
|
192
|
+
|
193
|
+
def scrollDown(self):
|
194
|
+
""" scroll down an item """
|
195
|
+
self.setCurrentIndex(self.currentIndex() + 1)
|
196
|
+
self.scrollToItem(self.currentItem())
|
197
|
+
|
198
|
+
def scrollUp(self):
|
199
|
+
""" scroll up an item """
|
200
|
+
self.setCurrentIndex(self.currentIndex() - 1)
|
201
|
+
self.scrollToItem(self.currentItem())
|
202
|
+
|
203
|
+
def enterEvent(self, e):
|
204
|
+
self.upButton.show()
|
205
|
+
self.downButton.show()
|
206
|
+
|
207
|
+
def leaveEvent(self, e):
|
208
|
+
self.upButton.hide()
|
209
|
+
self.downButton.hide()
|
210
|
+
|
211
|
+
def resizeEvent(self, e):
|
212
|
+
self.upButton.resize(self.width(), 34)
|
213
|
+
self.downButton.resize(self.width(), 34)
|
214
|
+
self.downButton.move(0, self.height() - 34)
|
215
|
+
|
216
|
+
def eventFilter(self, obj, e: QEvent):
|
217
|
+
if obj is not self or e.type() != QEvent.KeyPress:
|
218
|
+
return super().eventFilter(obj, e)
|
219
|
+
|
220
|
+
if e.key() == Qt.Key_Down:
|
221
|
+
self.scrollDown()
|
222
|
+
return True
|
223
|
+
elif e.key() == Qt.Key_Up:
|
224
|
+
self.scrollUp()
|
225
|
+
return True
|
226
|
+
|
227
|
+
return super().eventFilter(obj, e)
|
228
|
+
|
229
|
+
def currentItem(self):
|
230
|
+
return self.item(self.currentIndex())
|
231
|
+
|
232
|
+
def currentIndex(self):
|
233
|
+
return self._currentIndex
|
234
|
+
|
235
|
+
def setCurrentIndex(self, index: int):
|
236
|
+
if not self.isCycle:
|
237
|
+
n = self.visibleNumber // 2
|
238
|
+
self._currentIndex = max(
|
239
|
+
n, min(n + len(self.originItems) - 1, index))
|
240
|
+
else:
|
241
|
+
N = self.count() // 2
|
242
|
+
m = (self.visibleNumber + 1) // 2
|
243
|
+
self._currentIndex = index
|
244
|
+
|
245
|
+
# scroll to center to achieve circular scrolling
|
246
|
+
if index >= self.count() - m:
|
247
|
+
self._currentIndex = N + index - self.count()
|
248
|
+
super().scrollToItem(self.item(self.currentIndex() - 1), self.ScrollHint.PositionAtCenter)
|
249
|
+
elif index <= m - 1:
|
250
|
+
self._currentIndex = N + index
|
251
|
+
super().scrollToItem(self.item(N + index + 1), self.ScrollHint.PositionAtCenter)
|
@@ -0,0 +1,430 @@
|
|
1
|
+
# coding:utf-8
|
2
|
+
from typing import List, Union
|
3
|
+
|
4
|
+
from PySide6.QtCore import Qt, Signal, QModelIndex, QSize, Property, QRectF, QPropertyAnimation, QSizeF
|
5
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QWheelEvent, QPainterPath, QImageReader
|
6
|
+
from PySide6.QtWidgets import QStyleOptionViewItem, QListWidget, QStyledItemDelegate, QListWidgetItem
|
7
|
+
|
8
|
+
from ...common.overload import singledispatchmethod
|
9
|
+
from ...common.style_sheet import isDarkTheme, FluentStyleSheet
|
10
|
+
from ...common.icon import drawIcon, FluentIcon
|
11
|
+
from .scroll_bar import SmoothScrollBar
|
12
|
+
from .button import ToolButton
|
13
|
+
|
14
|
+
|
15
|
+
class ScrollButton(ToolButton):
|
16
|
+
""" Scroll button """
|
17
|
+
|
18
|
+
def _postInit(self):
|
19
|
+
self._opacity = 0
|
20
|
+
self.opacityAni = QPropertyAnimation(self, b'opacity', self)
|
21
|
+
self.opacityAni.setDuration(150)
|
22
|
+
|
23
|
+
def getOpacity(self):
|
24
|
+
return self._opacity
|
25
|
+
|
26
|
+
def setOpacity(self, o: float):
|
27
|
+
self._opacity = o
|
28
|
+
self.update()
|
29
|
+
|
30
|
+
def isTransparent(self):
|
31
|
+
return self.opacity == 0
|
32
|
+
|
33
|
+
def fadeIn(self):
|
34
|
+
self.opacityAni.setStartValue(self.opacity)
|
35
|
+
self.opacityAni.setEndValue(1)
|
36
|
+
self.opacityAni.start()
|
37
|
+
|
38
|
+
def fadeOut(self):
|
39
|
+
self.opacityAni.setStartValue(self.opacity)
|
40
|
+
self.opacityAni.setEndValue(0)
|
41
|
+
self.opacityAni.start()
|
42
|
+
|
43
|
+
def paintEvent(self, e):
|
44
|
+
painter = QPainter(self)
|
45
|
+
painter.setRenderHints(QPainter.Antialiasing)
|
46
|
+
painter.setPen(Qt.NoPen)
|
47
|
+
painter.setOpacity(self.opacity)
|
48
|
+
|
49
|
+
# draw background
|
50
|
+
if not isDarkTheme():
|
51
|
+
painter.setBrush(QColor(252, 252, 252, 217))
|
52
|
+
else:
|
53
|
+
painter.setBrush(QColor(44, 44, 44, 245))
|
54
|
+
|
55
|
+
painter.drawRoundedRect(self.rect(), 4, 4)
|
56
|
+
|
57
|
+
# draw icon
|
58
|
+
if isDarkTheme():
|
59
|
+
color = QColor(255, 255, 255)
|
60
|
+
opacity = 0.773 if self.isHover or self.isPressed else 0.541
|
61
|
+
else:
|
62
|
+
color = QColor(0, 0, 0)
|
63
|
+
opacity = 0.616 if self.isHover or self.isPressed else 0.45
|
64
|
+
|
65
|
+
painter.setOpacity(self.opacity * opacity)
|
66
|
+
|
67
|
+
s = 6 if self.isPressed else 8
|
68
|
+
w, h = self.width(), self.height()
|
69
|
+
x, y = (w - s) / 2, (h - s) / 2
|
70
|
+
drawIcon(self._icon, painter, QRectF(x, y, s, s), fill=color.name())
|
71
|
+
|
72
|
+
opacity = Property(float, getOpacity, setOpacity)
|
73
|
+
|
74
|
+
|
75
|
+
class FlipImageDelegate(QStyledItemDelegate):
|
76
|
+
""" Flip view image delegate """
|
77
|
+
|
78
|
+
def __init__(self, parent=None):
|
79
|
+
super().__init__(parent)
|
80
|
+
self.borderRadius = 0
|
81
|
+
|
82
|
+
def itemSize(self, index: int):
|
83
|
+
p = self.parent() # type: FlipView
|
84
|
+
return p.item(index).sizeHint()
|
85
|
+
|
86
|
+
def setBorderRadius(self, radius: int):
|
87
|
+
self.borderRadius = radius
|
88
|
+
self.parent().viewport().update()
|
89
|
+
|
90
|
+
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
|
91
|
+
painter.save()
|
92
|
+
painter.setRenderHints(QPainter.Antialiasing)
|
93
|
+
painter.setPen(Qt.NoPen)
|
94
|
+
|
95
|
+
size = self.itemSize(index.row()) # type: QSize
|
96
|
+
p = self.parent() # type: FlipView
|
97
|
+
|
98
|
+
# draw image
|
99
|
+
r = p.devicePixelRatioF()
|
100
|
+
image = index.data(Qt.UserRole) # type: QImage
|
101
|
+
if image is None:
|
102
|
+
return painter.restore()
|
103
|
+
|
104
|
+
# lazy load image
|
105
|
+
if image.isNull() and index.data(Qt.ItemDataRole.DisplayRole):
|
106
|
+
image.load(index.data(Qt.ItemDataRole.DisplayRole))
|
107
|
+
index.model().setData(index, image, Qt.ItemDataRole.UserRole)
|
108
|
+
|
109
|
+
x = option.rect.x() + int((option.rect.width() - size.width()) / 2)
|
110
|
+
y = option.rect.y() + int((option.rect.height() - size.height()) / 2)
|
111
|
+
rect = QRectF(x, y, size.width(), size.height())
|
112
|
+
|
113
|
+
# clipped path
|
114
|
+
path = QPainterPath()
|
115
|
+
path.addRoundedRect(rect, self.borderRadius, self.borderRadius)
|
116
|
+
subPath = QPainterPath()
|
117
|
+
subPath.addRoundedRect(QRectF(p.rect()), self.borderRadius, self.borderRadius)
|
118
|
+
path = path.intersected(subPath)
|
119
|
+
|
120
|
+
image = image.scaled(size * r, p.aspectRatioMode, Qt.SmoothTransformation)
|
121
|
+
painter.setClipPath(path)
|
122
|
+
|
123
|
+
# center crop image
|
124
|
+
if p.aspectRatioMode == Qt.AspectRatioMode.KeepAspectRatioByExpanding:
|
125
|
+
iw, ih = image.width(), image.height()
|
126
|
+
size = QSizeF(size) * r
|
127
|
+
x, y = (iw - size.width()) / 2, (ih - size.height()) / 2
|
128
|
+
image = image.copy(int(x), int(y), int(size.width()), int(size.height()))
|
129
|
+
|
130
|
+
painter.drawImage(rect, image)
|
131
|
+
painter.restore()
|
132
|
+
|
133
|
+
|
134
|
+
class FlipView(QListWidget):
|
135
|
+
""" Flip view
|
136
|
+
|
137
|
+
Constructors
|
138
|
+
------------
|
139
|
+
* FlipView(`parent`: QWidget = None)
|
140
|
+
* FlipView(`orient`: Qt.Orientation, `parent`: QWidget = None)
|
141
|
+
"""
|
142
|
+
|
143
|
+
currentIndexChanged = Signal(int)
|
144
|
+
|
145
|
+
@singledispatchmethod
|
146
|
+
def __init__(self, parent=None):
|
147
|
+
super().__init__(parent=parent)
|
148
|
+
self.orientation = Qt.Horizontal
|
149
|
+
self._postInit()
|
150
|
+
|
151
|
+
@__init__.register
|
152
|
+
def _(self, orientation: Qt.Orientation, parent=None):
|
153
|
+
super().__init__(parent=parent)
|
154
|
+
self.orientation = orientation
|
155
|
+
self._postInit()
|
156
|
+
|
157
|
+
def _postInit(self):
|
158
|
+
self.isHover = False
|
159
|
+
self._currentIndex = -1
|
160
|
+
self._aspectRatioMode = Qt.AspectRatioMode.IgnoreAspectRatio
|
161
|
+
self._itemSize = QSize(480, 270) # 16:9
|
162
|
+
|
163
|
+
self.delegate = FlipImageDelegate(self)
|
164
|
+
self.scrollBar = SmoothScrollBar(self.orientation, self)
|
165
|
+
|
166
|
+
self.scrollBar.setScrollAnimation(500)
|
167
|
+
self.scrollBar.setForceHidden(True)
|
168
|
+
|
169
|
+
# self.setUniformItemSizes(True)
|
170
|
+
self.setMinimumSize(self.itemSize)
|
171
|
+
self.setItemDelegate(self.delegate)
|
172
|
+
self.setMovement(QListWidget.Static)
|
173
|
+
self.setVerticalScrollMode(self.ScrollMode.ScrollPerPixel)
|
174
|
+
self.setHorizontalScrollMode(self.ScrollMode.ScrollPerPixel)
|
175
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
176
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
177
|
+
FluentStyleSheet.FLIP_VIEW.apply(self)
|
178
|
+
|
179
|
+
if self.isHorizontal():
|
180
|
+
self.setFlow(QListWidget.LeftToRight)
|
181
|
+
self.preButton = ScrollButton(FluentIcon.CARE_LEFT_SOLID, self)
|
182
|
+
self.nextButton = ScrollButton(FluentIcon.CARE_RIGHT_SOLID, self)
|
183
|
+
self.preButton.setFixedSize(16, 38)
|
184
|
+
self.nextButton.setFixedSize(16, 38)
|
185
|
+
else:
|
186
|
+
self.preButton = ScrollButton(FluentIcon.CARE_UP_SOLID, self)
|
187
|
+
self.nextButton = ScrollButton(FluentIcon.CARE_DOWN_SOLID, self)
|
188
|
+
self.preButton.setFixedSize(38, 16)
|
189
|
+
self.nextButton.setFixedSize(38, 16)
|
190
|
+
|
191
|
+
# connect signal to slot
|
192
|
+
self.preButton.clicked.connect(self.scrollPrevious)
|
193
|
+
self.nextButton.clicked.connect(self.scrollNext)
|
194
|
+
|
195
|
+
def isHorizontal(self):
|
196
|
+
return self.orientation == Qt.Horizontal
|
197
|
+
|
198
|
+
def setItemSize(self, size: QSize):
|
199
|
+
""" set the size of item """
|
200
|
+
if size == self.itemSize:
|
201
|
+
return
|
202
|
+
|
203
|
+
self._itemSize = size
|
204
|
+
|
205
|
+
for i in range(self.count()):
|
206
|
+
self._adjustItemSize(self.item(i))
|
207
|
+
|
208
|
+
self.viewport().update()
|
209
|
+
|
210
|
+
def getItemSize(self):
|
211
|
+
""" get the size of item """
|
212
|
+
return self._itemSize
|
213
|
+
|
214
|
+
def setBorderRadius(self, radius: int):
|
215
|
+
""" set the border radius of item """
|
216
|
+
self.delegate.setBorderRadius(radius)
|
217
|
+
|
218
|
+
def getBorderRadius(self):
|
219
|
+
return self.delegate.borderRadius
|
220
|
+
|
221
|
+
def scrollPrevious(self):
|
222
|
+
""" scroll to previous item """
|
223
|
+
self.setCurrentIndex(self.currentIndex() - 1)
|
224
|
+
|
225
|
+
def scrollNext(self):
|
226
|
+
""" scroll to next item """
|
227
|
+
self.setCurrentIndex(self.currentIndex() + 1)
|
228
|
+
|
229
|
+
def setCurrentIndex(self, index: int):
|
230
|
+
""" set current index """
|
231
|
+
if not 0 <= index < self.count() or index == self.currentIndex():
|
232
|
+
return
|
233
|
+
|
234
|
+
self.scrollToIndex(index)
|
235
|
+
|
236
|
+
# update the visibility of scroll button
|
237
|
+
if index == 0:
|
238
|
+
self.preButton.fadeOut()
|
239
|
+
elif self.preButton.isTransparent() and self.isHover:
|
240
|
+
self.preButton.fadeIn()
|
241
|
+
|
242
|
+
if index == self.count() - 1:
|
243
|
+
self.nextButton.fadeOut()
|
244
|
+
elif self.nextButton.isTransparent() and self.isHover:
|
245
|
+
self.nextButton.fadeIn()
|
246
|
+
|
247
|
+
# fire signal
|
248
|
+
self.currentIndexChanged.emit(index)
|
249
|
+
|
250
|
+
def scrollToIndex(self, index):
|
251
|
+
if not 0 <= index < self.count():
|
252
|
+
return
|
253
|
+
|
254
|
+
self._currentIndex = index
|
255
|
+
|
256
|
+
if self.isHorizontal():
|
257
|
+
value = sum(self.item(i).sizeHint().width() for i in range(index))
|
258
|
+
else:
|
259
|
+
value = sum(self.item(i).sizeHint().height() for i in range(index))
|
260
|
+
|
261
|
+
value += (2 * index + 1) * self.spacing()
|
262
|
+
self.scrollBar.scrollTo(value)
|
263
|
+
|
264
|
+
def currentIndex(self):
|
265
|
+
return self._currentIndex
|
266
|
+
|
267
|
+
def image(self, index: int):
|
268
|
+
if not 0 <= index < self.count():
|
269
|
+
return QImage()
|
270
|
+
|
271
|
+
return self.item(index).data(Qt.UserRole)
|
272
|
+
|
273
|
+
def addImage(self, image: Union[QImage, QPixmap, str]):
|
274
|
+
""" add image """
|
275
|
+
self.addImages([image])
|
276
|
+
|
277
|
+
def addImages(self, images: List[Union[QImage, QPixmap, str]], targetSize: QSize = None):
|
278
|
+
""" add images """
|
279
|
+
if not images:
|
280
|
+
return
|
281
|
+
|
282
|
+
N = self.count()
|
283
|
+
self.addItems([''] * len(images))
|
284
|
+
|
285
|
+
for i in range(N, self.count()):
|
286
|
+
self.setItemImage(i, images[i - N], targetSize=targetSize)
|
287
|
+
|
288
|
+
if self.currentIndex() < 0:
|
289
|
+
self._currentIndex = 0
|
290
|
+
|
291
|
+
def setItemImage(self, index: int, image: Union[QImage, QPixmap, str], targetSize: QSize = None):
|
292
|
+
""" set the image of specified item """
|
293
|
+
if not 0 <= index < self.count():
|
294
|
+
return
|
295
|
+
|
296
|
+
item = self.item(index)
|
297
|
+
|
298
|
+
# convert image to QImage
|
299
|
+
if isinstance(image, QPixmap):
|
300
|
+
image = image.toImage()
|
301
|
+
|
302
|
+
# lazy load
|
303
|
+
if isinstance(image, QImage):
|
304
|
+
item.setData(Qt.ItemDataRole.UserRole, image)
|
305
|
+
else:
|
306
|
+
item.setData(Qt.ItemDataRole.UserRole, QImage())
|
307
|
+
item.setData(Qt.ItemDataRole.DisplayRole, image)
|
308
|
+
|
309
|
+
self._adjustItemSize(item)
|
310
|
+
|
311
|
+
def _adjustItemSize(self, item: QListWidgetItem):
|
312
|
+
image = self.itemImage(self.row(item), load=False)
|
313
|
+
|
314
|
+
if not image.isNull():
|
315
|
+
size = image.size()
|
316
|
+
else:
|
317
|
+
imagePath = item.data(Qt.ItemDataRole.DisplayRole) or ""
|
318
|
+
size = QImageReader(imagePath).size().expandedTo(QSize(1, 1))
|
319
|
+
|
320
|
+
if self.aspectRatioMode == Qt.AspectRatioMode.KeepAspectRatio:
|
321
|
+
if self.isHorizontal():
|
322
|
+
h = self.itemSize.height()
|
323
|
+
w = int(size.width() * h / size.height())
|
324
|
+
else:
|
325
|
+
w = self.itemSize.width()
|
326
|
+
h = int(size.height() * w / size.width())
|
327
|
+
else:
|
328
|
+
w, h = self.itemSize.width(), self.itemSize.height()
|
329
|
+
|
330
|
+
item.setSizeHint(QSize(w, h))
|
331
|
+
|
332
|
+
def itemImage(self, index: int, load=True) -> QImage:
|
333
|
+
""" get the image of specified item
|
334
|
+
|
335
|
+
Parameters
|
336
|
+
----------
|
337
|
+
index: int
|
338
|
+
the index of image
|
339
|
+
|
340
|
+
load: bool
|
341
|
+
whether to load image data
|
342
|
+
"""
|
343
|
+
if not 0 <= index < self.count():
|
344
|
+
return
|
345
|
+
|
346
|
+
item = self.item(index)
|
347
|
+
image = item.data(Qt.ItemDataRole.UserRole) # type: QImage
|
348
|
+
|
349
|
+
if image is None:
|
350
|
+
return QImage()
|
351
|
+
|
352
|
+
imagePath = item.data(Qt.ItemDataRole.DisplayRole)
|
353
|
+
if image.isNull() and imagePath and load:
|
354
|
+
image.load(imagePath)
|
355
|
+
|
356
|
+
return image
|
357
|
+
|
358
|
+
def resizeEvent(self, e):
|
359
|
+
w, h = self.width(), self.height()
|
360
|
+
bw, bh = self.preButton.width(), self.preButton.height()
|
361
|
+
|
362
|
+
if self.isHorizontal():
|
363
|
+
self.preButton.move(2, int(h / 2 - bh / 2))
|
364
|
+
self.nextButton.move(w - bw - 2, int(h / 2 - bh / 2))
|
365
|
+
else:
|
366
|
+
self.preButton.move(int(w / 2 - bw / 2), 2)
|
367
|
+
self.nextButton.move(int(w / 2 - bw / 2), h - bh - 2)
|
368
|
+
|
369
|
+
def enterEvent(self, e):
|
370
|
+
super().enterEvent(e)
|
371
|
+
self.isHover = True
|
372
|
+
|
373
|
+
if self.currentIndex() > 0:
|
374
|
+
self.preButton.fadeIn()
|
375
|
+
|
376
|
+
if self.currentIndex() < self.count() - 1:
|
377
|
+
self.nextButton.fadeIn()
|
378
|
+
|
379
|
+
def leaveEvent(self, e):
|
380
|
+
super().leaveEvent(e)
|
381
|
+
self.isHover = False
|
382
|
+
self.preButton.fadeOut()
|
383
|
+
self.nextButton.fadeOut()
|
384
|
+
|
385
|
+
def showEvent(self, e):
|
386
|
+
self.scrollBar.duration = 0
|
387
|
+
self.scrollToIndex(self.currentIndex())
|
388
|
+
self.scrollBar.duration = 500
|
389
|
+
|
390
|
+
def wheelEvent(self, e: QWheelEvent):
|
391
|
+
e.setAccepted(True)
|
392
|
+
if self.scrollBar.ani.state() == QPropertyAnimation.Running:
|
393
|
+
return
|
394
|
+
|
395
|
+
if e.angleDelta().y() < 0:
|
396
|
+
self.scrollNext()
|
397
|
+
else:
|
398
|
+
self.scrollPrevious()
|
399
|
+
|
400
|
+
def getAspectRatioMode(self):
|
401
|
+
return self._aspectRatioMode
|
402
|
+
|
403
|
+
def setAspectRatioMode(self, mode: Qt.AspectRatioMode):
|
404
|
+
if mode == self.aspectRatioMode:
|
405
|
+
return
|
406
|
+
|
407
|
+
self._aspectRatioMode = mode
|
408
|
+
|
409
|
+
for i in range(self.count()):
|
410
|
+
self._adjustItemSize(self.item(i))
|
411
|
+
|
412
|
+
self.viewport().update()
|
413
|
+
|
414
|
+
itemSize = Property(QSize, getItemSize, setItemSize)
|
415
|
+
borderRadius = Property(int, getBorderRadius, setBorderRadius)
|
416
|
+
aspectRatioMode = Property(Qt.AspectRatioMode, getAspectRatioMode, setAspectRatioMode)
|
417
|
+
|
418
|
+
|
419
|
+
class HorizontalFlipView(FlipView):
|
420
|
+
""" Horizontal flip view """
|
421
|
+
|
422
|
+
def __init__(self, parent=None):
|
423
|
+
super().__init__(Qt.Horizontal, parent)
|
424
|
+
|
425
|
+
|
426
|
+
class VerticalFlipView(FlipView):
|
427
|
+
""" Vertical flip view """
|
428
|
+
|
429
|
+
def __init__(self, parent=None):
|
430
|
+
super().__init__(Qt.Vertical, parent)
|