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,804 @@
|
|
1
|
+
# coding:utf-8
|
2
|
+
from copy import deepcopy
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Dict, List, Union
|
5
|
+
from PySide6.QtCore import Qt, Signal, Property, QRectF, QSize, QPoint, QPropertyAnimation, QEasingCurve, QRect
|
6
|
+
from PySide6.QtGui import QPainter, QColor, QIcon, QPainterPath, QLinearGradient, QPen, QBrush, QMouseEvent
|
7
|
+
from PySide6.QtWidgets import QWidget, QGraphicsDropShadowEffect, QHBoxLayout, QSizePolicy, QApplication
|
8
|
+
|
9
|
+
from ...common.icon import FluentIcon, FluentIconBase, drawIcon
|
10
|
+
from ...common.style_sheet import isDarkTheme, FluentStyleSheet
|
11
|
+
from ...common.font import setFont
|
12
|
+
from ...common.router import qrouter
|
13
|
+
from .button import TransparentToolButton, PushButton
|
14
|
+
from .scroll_area import SingleDirectionScrollArea
|
15
|
+
from .tool_tip import ToolTipFilter
|
16
|
+
|
17
|
+
|
18
|
+
class TabCloseButtonDisplayMode(Enum):
|
19
|
+
""" Tab close button display mode """
|
20
|
+
ALWAYS = 0
|
21
|
+
ON_HOVER = 1
|
22
|
+
NEVER = 2
|
23
|
+
|
24
|
+
|
25
|
+
def checkIndex(*default):
|
26
|
+
""" decorator for index checking
|
27
|
+
|
28
|
+
Parameters
|
29
|
+
----------
|
30
|
+
*default:
|
31
|
+
the default value returned when an index overflow
|
32
|
+
"""
|
33
|
+
|
34
|
+
def outer(func):
|
35
|
+
|
36
|
+
def inner(tabBar, index: int, *args, **kwargs):
|
37
|
+
if 0 <= index < len(tabBar.items):
|
38
|
+
return func(tabBar, index, *args, **kwargs)
|
39
|
+
|
40
|
+
value = deepcopy(default)
|
41
|
+
if len(value) == 0:
|
42
|
+
return None
|
43
|
+
elif len(value) == 1:
|
44
|
+
return value[0]
|
45
|
+
|
46
|
+
return value
|
47
|
+
|
48
|
+
return inner
|
49
|
+
|
50
|
+
return outer
|
51
|
+
|
52
|
+
|
53
|
+
class TabToolButton(TransparentToolButton):
|
54
|
+
""" Tab tool button """
|
55
|
+
|
56
|
+
def _postInit(self):
|
57
|
+
self.setFixedSize(32, 24)
|
58
|
+
self.setIconSize(QSize(12, 12))
|
59
|
+
|
60
|
+
def _drawIcon(self, icon, painter: QPainter, rect: QRectF, state=QIcon.Off):
|
61
|
+
color = '#eaeaea' if isDarkTheme() else '#484848'
|
62
|
+
icon = icon.icon(color=color)
|
63
|
+
super()._drawIcon(icon, painter, rect, state)
|
64
|
+
|
65
|
+
|
66
|
+
class TabItem(PushButton):
|
67
|
+
""" Tab item """
|
68
|
+
|
69
|
+
closed = Signal()
|
70
|
+
|
71
|
+
def _postInit(self):
|
72
|
+
super()._postInit()
|
73
|
+
self.borderRadius = 5
|
74
|
+
self.isSelected = False
|
75
|
+
self.isShadowEnabled = True
|
76
|
+
self.closeButtonDisplayMode = TabCloseButtonDisplayMode.ALWAYS
|
77
|
+
|
78
|
+
self._routeKey = None
|
79
|
+
self.textColor = None
|
80
|
+
self.lightSelectedBackgroundColor = QColor(249, 249, 249)
|
81
|
+
self.darkSelectedBackgroundColor = QColor(40, 40, 40)
|
82
|
+
|
83
|
+
self.closeButton = TabToolButton(FluentIcon.CLOSE, self)
|
84
|
+
self.shadowEffect = QGraphicsDropShadowEffect(self)
|
85
|
+
|
86
|
+
self.slideAni = QPropertyAnimation(self, b'pos', self)
|
87
|
+
|
88
|
+
self.__initWidget()
|
89
|
+
|
90
|
+
def __initWidget(self):
|
91
|
+
setFont(self, 12)
|
92
|
+
self.setFixedHeight(36)
|
93
|
+
self.setMaximumWidth(240)
|
94
|
+
self.setMinimumWidth(64)
|
95
|
+
self.installEventFilter(ToolTipFilter(self, showDelay=1000))
|
96
|
+
self.setAttribute(Qt.WA_LayoutUsesWidgetRect)
|
97
|
+
|
98
|
+
self.closeButton.setIconSize(QSize(10, 10))
|
99
|
+
|
100
|
+
self.shadowEffect.setBlurRadius(5)
|
101
|
+
self.shadowEffect.setOffset(0, 1)
|
102
|
+
self.setGraphicsEffect(self.shadowEffect)
|
103
|
+
self.setSelected(False)
|
104
|
+
|
105
|
+
self.closeButton.clicked.connect(self.closed)
|
106
|
+
|
107
|
+
def slideTo(self, x: int, duration=250):
|
108
|
+
self.slideAni.setStartValue(self.pos())
|
109
|
+
self.slideAni.setEndValue(QPoint(x, self.y()))
|
110
|
+
self.slideAni.setDuration(duration)
|
111
|
+
self.slideAni.setEasingCurve(QEasingCurve.InOutQuad)
|
112
|
+
self.slideAni.start()
|
113
|
+
|
114
|
+
def setShadowEnabled(self, isEnabled: bool):
|
115
|
+
""" set whether the shadow is enabled """
|
116
|
+
if isEnabled == self.isShadowEnabled:
|
117
|
+
return
|
118
|
+
|
119
|
+
self.isShadowEnabled = isEnabled
|
120
|
+
self.shadowEffect.setColor(QColor(0, 0, 0, 50*self._canShowShadow()))
|
121
|
+
|
122
|
+
def _canShowShadow(self):
|
123
|
+
return self.isSelected and self.isShadowEnabled
|
124
|
+
|
125
|
+
def setRouteKey(self, key: str):
|
126
|
+
self._routeKey = key
|
127
|
+
|
128
|
+
def routeKey(self):
|
129
|
+
return self._routeKey
|
130
|
+
|
131
|
+
def setBorderRadius(self, radius: int):
|
132
|
+
self.borderRadius = radius
|
133
|
+
self.update()
|
134
|
+
|
135
|
+
def setSelected(self, isSelected: bool):
|
136
|
+
self.isSelected = isSelected
|
137
|
+
|
138
|
+
self.shadowEffect.setColor(QColor(0, 0, 0, 50*self._canShowShadow()))
|
139
|
+
self.update()
|
140
|
+
|
141
|
+
if isSelected:
|
142
|
+
self.raise_()
|
143
|
+
|
144
|
+
if self.closeButtonDisplayMode == TabCloseButtonDisplayMode.ON_HOVER:
|
145
|
+
self.closeButton.setVisible(isSelected)
|
146
|
+
|
147
|
+
def setCloseButtonDisplayMode(self, mode: TabCloseButtonDisplayMode):
|
148
|
+
""" set close button display mode """
|
149
|
+
if mode == self.closeButtonDisplayMode:
|
150
|
+
return
|
151
|
+
|
152
|
+
self.closeButtonDisplayMode = mode
|
153
|
+
|
154
|
+
if mode == TabCloseButtonDisplayMode.NEVER:
|
155
|
+
self.closeButton.hide()
|
156
|
+
elif mode == TabCloseButtonDisplayMode.ALWAYS:
|
157
|
+
self.closeButton.show()
|
158
|
+
else:
|
159
|
+
self.closeButton.setVisible(self.isHover or self.isSelected)
|
160
|
+
|
161
|
+
def setTextColor(self, color: QColor):
|
162
|
+
self.textColor = QColor(color)
|
163
|
+
self.update()
|
164
|
+
|
165
|
+
def setSelectedBackgroundColor(self, light: QColor, dark: QColor):
|
166
|
+
""" set background color in selected state """
|
167
|
+
self.lightSelectedBackgroundColor = QColor(light)
|
168
|
+
self.darkSelectedBackgroundColor = QColor(dark)
|
169
|
+
self.update()
|
170
|
+
|
171
|
+
def resizeEvent(self, e):
|
172
|
+
self.closeButton.move(
|
173
|
+
self.width()-6-self.closeButton.width(), int(self.height()/2-self.closeButton.height()/2))
|
174
|
+
|
175
|
+
def enterEvent(self, e):
|
176
|
+
super().enterEvent(e)
|
177
|
+
if self.closeButtonDisplayMode == TabCloseButtonDisplayMode.ON_HOVER:
|
178
|
+
self.closeButton.show()
|
179
|
+
|
180
|
+
def leaveEvent(self, e):
|
181
|
+
super().leaveEvent(e)
|
182
|
+
if self.closeButtonDisplayMode == TabCloseButtonDisplayMode.ON_HOVER and not self.isSelected:
|
183
|
+
self.closeButton.hide()
|
184
|
+
|
185
|
+
def mousePressEvent(self, e):
|
186
|
+
super().mousePressEvent(e)
|
187
|
+
self._forwardMouseEvent(e)
|
188
|
+
|
189
|
+
def mouseMoveEvent(self, e):
|
190
|
+
super().mouseMoveEvent(e)
|
191
|
+
self._forwardMouseEvent(e)
|
192
|
+
|
193
|
+
def mouseReleaseEvent(self, e):
|
194
|
+
super().mouseReleaseEvent(e)
|
195
|
+
self._forwardMouseEvent(e)
|
196
|
+
|
197
|
+
def _forwardMouseEvent(self, e: QMouseEvent):
|
198
|
+
pos = self.mapToParent(e.pos())
|
199
|
+
event = QMouseEvent(e.type(), pos, e.button(),
|
200
|
+
e.buttons(), e.modifiers())
|
201
|
+
QApplication.sendEvent(self.parent(), event)
|
202
|
+
|
203
|
+
def sizeHint(self):
|
204
|
+
return QSize(self.maximumWidth(), 36)
|
205
|
+
|
206
|
+
def paintEvent(self, e):
|
207
|
+
painter = QPainter(self)
|
208
|
+
painter.setRenderHints(QPainter.Antialiasing)
|
209
|
+
|
210
|
+
if self.isSelected:
|
211
|
+
self._drawSelectedBackground(painter)
|
212
|
+
else:
|
213
|
+
self._drawNotSelectedBackground(painter)
|
214
|
+
|
215
|
+
# draw icon
|
216
|
+
if not self.isSelected:
|
217
|
+
painter.setOpacity(0.79 if isDarkTheme() else 0.61)
|
218
|
+
|
219
|
+
drawIcon(self._icon, painter, QRectF(10, 10, 16, 16))
|
220
|
+
|
221
|
+
# draw text
|
222
|
+
self._drawText(painter)
|
223
|
+
|
224
|
+
def _drawSelectedBackground(self, painter: QPainter):
|
225
|
+
w, h = self.width(), self.height()
|
226
|
+
r = self.borderRadius
|
227
|
+
d = 2 * r
|
228
|
+
|
229
|
+
isDark = isDarkTheme()
|
230
|
+
|
231
|
+
# draw top border
|
232
|
+
path = QPainterPath()
|
233
|
+
path.arcMoveTo(1, h - d - 1, d, d, 225)
|
234
|
+
path.arcTo(1, h - d - 1, d, d, 225, -45)
|
235
|
+
path.lineTo(1, r)
|
236
|
+
path.arcTo(1, 1, d, d, -180, -90)
|
237
|
+
path.lineTo(w - r, 1)
|
238
|
+
path.arcTo(w - d - 1, 1, d, d, 90, -90)
|
239
|
+
path.lineTo(w - 1, h - r)
|
240
|
+
path.arcTo(w - d - 1, h - d - 1, d, d, 0, -45)
|
241
|
+
|
242
|
+
topBorderColor = QColor(0, 0, 0, 20)
|
243
|
+
if isDark:
|
244
|
+
if self.isPressed:
|
245
|
+
topBorderColor = QColor(255, 255, 255, 18)
|
246
|
+
elif self.isHover:
|
247
|
+
topBorderColor = QColor(255, 255, 255, 13)
|
248
|
+
else:
|
249
|
+
topBorderColor = QColor(0, 0, 0, 16)
|
250
|
+
|
251
|
+
painter.strokePath(path, topBorderColor)
|
252
|
+
|
253
|
+
# draw bottom border
|
254
|
+
path = QPainterPath()
|
255
|
+
path.arcMoveTo(1, h - d - 1, d, d, 225)
|
256
|
+
path.arcTo(1, h - d - 1, d, d, 225, 45)
|
257
|
+
path.lineTo(w - r - 1, h - 1)
|
258
|
+
path.arcTo(w - d - 1, h - d - 1, d, d, 270, 45)
|
259
|
+
|
260
|
+
bottomBorderColor = topBorderColor
|
261
|
+
if not isDark:
|
262
|
+
bottomBorderColor = QColor(0, 0, 0, 63)
|
263
|
+
|
264
|
+
painter.strokePath(path, bottomBorderColor)
|
265
|
+
|
266
|
+
# draw background
|
267
|
+
painter.setPen(Qt.NoPen)
|
268
|
+
rect = self.rect().adjusted(1, 1, -1, -1)
|
269
|
+
painter.setBrush(
|
270
|
+
self.darkSelectedBackgroundColor if isDark else self.lightSelectedBackgroundColor)
|
271
|
+
painter.drawRoundedRect(rect, r, r)
|
272
|
+
|
273
|
+
def _drawNotSelectedBackground(self, painter: QPainter):
|
274
|
+
if not (self.isPressed or self.isHover):
|
275
|
+
return
|
276
|
+
|
277
|
+
isDark = isDarkTheme()
|
278
|
+
|
279
|
+
if self.isPressed:
|
280
|
+
color = QColor(255, 255, 255, 12) if isDark else QColor(0, 0, 0, 7)
|
281
|
+
else:
|
282
|
+
color = QColor(255, 255, 255, 15) if isDark else QColor(
|
283
|
+
0, 0, 0, 10)
|
284
|
+
|
285
|
+
painter.setBrush(color)
|
286
|
+
painter.setPen(Qt.NoPen)
|
287
|
+
painter.drawRoundedRect(self.rect().adjusted(
|
288
|
+
1, 1, -1, -1), self.borderRadius, self.borderRadius)
|
289
|
+
|
290
|
+
def _drawText(self, painter: QPainter):
|
291
|
+
tw = self.fontMetrics().boundingRect(self.text()).width()
|
292
|
+
|
293
|
+
if self.icon().isNull():
|
294
|
+
dw = 47 if self.closeButton.isVisible() else 20
|
295
|
+
rect = QRectF(10, 0, self.width() - dw, self.height())
|
296
|
+
else:
|
297
|
+
dw = 70 if self.closeButton.isVisible() else 45
|
298
|
+
rect = QRectF(33, 0, self.width() - dw, self.height())
|
299
|
+
|
300
|
+
pen = QPen()
|
301
|
+
color = Qt.white if isDarkTheme() else Qt.black
|
302
|
+
color = self.textColor or color
|
303
|
+
rw = rect.width()
|
304
|
+
|
305
|
+
if tw > rw:
|
306
|
+
gradient = QLinearGradient(rect.x(), 0, tw+rect.x(), 0)
|
307
|
+
gradient.setColorAt(0, color)
|
308
|
+
gradient.setColorAt(max(0, (rw - 10) / tw), color)
|
309
|
+
gradient.setColorAt(max(0, rw / tw), Qt.transparent)
|
310
|
+
gradient.setColorAt(1, Qt.transparent)
|
311
|
+
pen.setBrush(QBrush(gradient))
|
312
|
+
else:
|
313
|
+
pen.setColor(color)
|
314
|
+
|
315
|
+
painter.setPen(pen)
|
316
|
+
painter.setFont(self.font())
|
317
|
+
painter.drawText(rect, Qt.AlignVCenter | Qt.AlignLeft, self.text())
|
318
|
+
|
319
|
+
|
320
|
+
class TabBar(SingleDirectionScrollArea):
|
321
|
+
""" Tab bar """
|
322
|
+
|
323
|
+
currentChanged = Signal(int)
|
324
|
+
tabBarClicked = Signal(int)
|
325
|
+
tabCloseRequested = Signal(int)
|
326
|
+
tabAddRequested = Signal()
|
327
|
+
|
328
|
+
def __init__(self, parent=None):
|
329
|
+
super().__init__(parent=parent, orient=Qt.Horizontal)
|
330
|
+
self.items = [] # type: List[TabItem]
|
331
|
+
self.itemMap = {} # type: Dict[str, TabItem]
|
332
|
+
|
333
|
+
self._currentIndex = -1
|
334
|
+
|
335
|
+
self._isMovable = False
|
336
|
+
self._isScrollable = False
|
337
|
+
self._isTabShadowEnabled = True
|
338
|
+
|
339
|
+
self._tabMaxWidth = 240
|
340
|
+
self._tabMinWidth = 64
|
341
|
+
|
342
|
+
self.dragPos = QPoint()
|
343
|
+
self.isDraging = False
|
344
|
+
|
345
|
+
self.lightSelectedBackgroundColor = QColor(249, 249, 249)
|
346
|
+
self.darkSelectedBackgroundColor = QColor(40, 40, 40)
|
347
|
+
self.closeButtonDisplayMode = TabCloseButtonDisplayMode.ALWAYS
|
348
|
+
|
349
|
+
self.view = QWidget(self)
|
350
|
+
self.hBoxLayout = QHBoxLayout(self.view)
|
351
|
+
self.itemLayout = QHBoxLayout()
|
352
|
+
self.widgetLayout = QHBoxLayout()
|
353
|
+
|
354
|
+
self.addButton = TabToolButton(FluentIcon.ADD, self)
|
355
|
+
|
356
|
+
self.__initWidget()
|
357
|
+
|
358
|
+
def __initWidget(self):
|
359
|
+
self.setFixedHeight(46)
|
360
|
+
self.setWidget(self.view)
|
361
|
+
self.setWidgetResizable(True)
|
362
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
363
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
364
|
+
|
365
|
+
self.hBoxLayout.setSizeConstraint(QHBoxLayout.SetMaximumSize)
|
366
|
+
|
367
|
+
self.addButton.clicked.connect(self.tabAddRequested)
|
368
|
+
|
369
|
+
self.view.setObjectName('view')
|
370
|
+
FluentStyleSheet.TAB_VIEW.apply(self)
|
371
|
+
FluentStyleSheet.TAB_VIEW.apply(self.view)
|
372
|
+
|
373
|
+
self.__initLayout()
|
374
|
+
|
375
|
+
def __initLayout(self):
|
376
|
+
self.hBoxLayout.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
|
377
|
+
self.itemLayout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
378
|
+
self.widgetLayout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
379
|
+
|
380
|
+
self.itemLayout.setContentsMargins(5, 5, 5, 5)
|
381
|
+
self.widgetLayout.setContentsMargins(0, 0, 0, 0)
|
382
|
+
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
|
383
|
+
|
384
|
+
self.itemLayout.setSizeConstraint(QHBoxLayout.SetMinAndMaxSize)
|
385
|
+
|
386
|
+
self.hBoxLayout.setSpacing(0)
|
387
|
+
self.itemLayout.setSpacing(0)
|
388
|
+
|
389
|
+
self.hBoxLayout.addLayout(self.itemLayout)
|
390
|
+
self.hBoxLayout.addSpacing(3)
|
391
|
+
|
392
|
+
self.widgetLayout.addWidget(self.addButton, 0, Qt.AlignLeft)
|
393
|
+
self.hBoxLayout.addLayout(self.widgetLayout)
|
394
|
+
self.hBoxLayout.addStretch(1)
|
395
|
+
|
396
|
+
def setAddButtonVisible(self, isVisible: bool):
|
397
|
+
self.addButton.setVisible(isVisible)
|
398
|
+
|
399
|
+
def addTab(self, routeKey: str, text: str, icon: Union[QIcon, str, FluentIconBase] = None, onClick=None):
|
400
|
+
""" add tab
|
401
|
+
|
402
|
+
Parameters
|
403
|
+
----------
|
404
|
+
routeKey: str
|
405
|
+
the unique name of tab item
|
406
|
+
|
407
|
+
text: str
|
408
|
+
the text of tab item
|
409
|
+
|
410
|
+
text: str
|
411
|
+
the icon of tab item
|
412
|
+
|
413
|
+
onClick: callable
|
414
|
+
the slot connected to item clicked signal
|
415
|
+
"""
|
416
|
+
return self.insertTab(-1, routeKey, text, icon, onClick)
|
417
|
+
|
418
|
+
def insertTab(self, index: int, routeKey: str, text: str, icon: Union[QIcon, str, FluentIconBase] = None,
|
419
|
+
onClick=None):
|
420
|
+
""" insert tab
|
421
|
+
|
422
|
+
Parameters
|
423
|
+
----------
|
424
|
+
index: int
|
425
|
+
the insert position of tab item
|
426
|
+
|
427
|
+
routeKey: str
|
428
|
+
the unique name of tab item
|
429
|
+
|
430
|
+
text: str
|
431
|
+
the text of tab item
|
432
|
+
|
433
|
+
text: str
|
434
|
+
the icon of tab item
|
435
|
+
|
436
|
+
onClick: callable
|
437
|
+
the slot connected to item clicked signal
|
438
|
+
"""
|
439
|
+
if routeKey in self.itemMap:
|
440
|
+
raise ValueError(f"The route key `{routeKey}` is duplicated.")
|
441
|
+
|
442
|
+
if index == -1:
|
443
|
+
index = len(self.items)
|
444
|
+
|
445
|
+
# adjust current index
|
446
|
+
if index <= self.currentIndex() and self.currentIndex() >= 0:
|
447
|
+
self._currentIndex += 1
|
448
|
+
|
449
|
+
item = TabItem(text, self.view, icon)
|
450
|
+
item.setRouteKey(routeKey)
|
451
|
+
|
452
|
+
# set the size of tab
|
453
|
+
w = self.tabMaximumWidth() if self.isScrollable() else self.tabMinimumWidth()
|
454
|
+
item.setMinimumWidth(w)
|
455
|
+
item.setMaximumWidth(self.tabMaximumWidth())
|
456
|
+
|
457
|
+
item.setShadowEnabled(self.isTabShadowEnabled())
|
458
|
+
item.setCloseButtonDisplayMode(self.closeButtonDisplayMode)
|
459
|
+
item.setSelectedBackgroundColor(
|
460
|
+
self.lightSelectedBackgroundColor, self.darkSelectedBackgroundColor)
|
461
|
+
|
462
|
+
item.pressed.connect(self._onItemPressed)
|
463
|
+
item.closed.connect(lambda: self.tabCloseRequested.emit(self.items.index(item)))
|
464
|
+
if onClick:
|
465
|
+
item.pressed.connect(onClick)
|
466
|
+
|
467
|
+
self.itemLayout.insertWidget(index, item, 1)
|
468
|
+
self.items.insert(index, item)
|
469
|
+
self.itemMap[routeKey] = item
|
470
|
+
|
471
|
+
if len(self.items) == 1:
|
472
|
+
self.setCurrentIndex(0)
|
473
|
+
|
474
|
+
return item
|
475
|
+
|
476
|
+
def removeTab(self, index: int):
|
477
|
+
if not 0 <= index < len(self.items):
|
478
|
+
return
|
479
|
+
|
480
|
+
# adjust current index
|
481
|
+
if index < self.currentIndex():
|
482
|
+
self._currentIndex -= 1
|
483
|
+
elif index == self.currentIndex():
|
484
|
+
if self.currentIndex() > 0:
|
485
|
+
self.setCurrentIndex(self.currentIndex() - 1)
|
486
|
+
self.currentChanged.emit(self.currentIndex())
|
487
|
+
elif len(self.items) == 1:
|
488
|
+
self._currentIndex = -1
|
489
|
+
else:
|
490
|
+
self.setCurrentIndex(1)
|
491
|
+
self._currentIndex = 0
|
492
|
+
self.currentChanged.emit(0)
|
493
|
+
|
494
|
+
# remove tab
|
495
|
+
item = self.items.pop(index)
|
496
|
+
self.itemMap.pop(item.routeKey())
|
497
|
+
self.hBoxLayout.removeWidget(item)
|
498
|
+
qrouter.remove(item.routeKey())
|
499
|
+
item.deleteLater()
|
500
|
+
|
501
|
+
# remove shadow
|
502
|
+
self.update()
|
503
|
+
|
504
|
+
def removeTabByKey(self, routeKey: str):
|
505
|
+
if routeKey not in self.itemMap:
|
506
|
+
return
|
507
|
+
|
508
|
+
self.removeTab(self.items.index(self.tab(routeKey)))
|
509
|
+
|
510
|
+
def setCurrentIndex(self, index: int):
|
511
|
+
""" set current index """
|
512
|
+
if index == self._currentIndex:
|
513
|
+
return
|
514
|
+
|
515
|
+
if self.currentIndex() >= 0:
|
516
|
+
self.items[self.currentIndex()].setSelected(False)
|
517
|
+
|
518
|
+
self._currentIndex = index
|
519
|
+
self.items[index].setSelected(True)
|
520
|
+
|
521
|
+
def setCurrentTab(self, routeKey: str):
|
522
|
+
if routeKey not in self.itemMap:
|
523
|
+
return
|
524
|
+
|
525
|
+
self.setCurrentIndex(self.items.index(self.tab(routeKey)))
|
526
|
+
|
527
|
+
def currentIndex(self):
|
528
|
+
return self._currentIndex
|
529
|
+
|
530
|
+
def currentTab(self):
|
531
|
+
return self.tabItem(self.currentIndex())
|
532
|
+
|
533
|
+
def _onItemPressed(self):
|
534
|
+
for item in self.items:
|
535
|
+
item.setSelected(item is self.sender())
|
536
|
+
|
537
|
+
index = self.items.index(self.sender())
|
538
|
+
self.tabBarClicked.emit(index)
|
539
|
+
|
540
|
+
if index != self.currentIndex():
|
541
|
+
self.setCurrentIndex(index)
|
542
|
+
self.currentChanged.emit(index)
|
543
|
+
|
544
|
+
def setCloseButtonDisplayMode(self, mode: TabCloseButtonDisplayMode):
|
545
|
+
""" set close button display mode """
|
546
|
+
if mode == self.closeButtonDisplayMode:
|
547
|
+
return
|
548
|
+
|
549
|
+
self.closeButtonDisplayMode = mode
|
550
|
+
for item in self.items:
|
551
|
+
item.setCloseButtonDisplayMode(mode)
|
552
|
+
|
553
|
+
@checkIndex()
|
554
|
+
def tabItem(self, index: int):
|
555
|
+
return self.items[index]
|
556
|
+
|
557
|
+
def tab(self, routeKey: str):
|
558
|
+
return self.itemMap.get(routeKey, None)
|
559
|
+
|
560
|
+
def tabRegion(self) -> QRect:
|
561
|
+
""" return the bounding rect of all tabs """
|
562
|
+
return self.itemLayout.geometry()
|
563
|
+
|
564
|
+
@checkIndex()
|
565
|
+
def tabRect(self, index: int):
|
566
|
+
""" return the visual rectangle of the tab at position index """
|
567
|
+
x = 0
|
568
|
+
for i in range(index):
|
569
|
+
x += self.tabItem(i).width()
|
570
|
+
|
571
|
+
rect = self.tabItem(index).geometry()
|
572
|
+
rect.moveLeft(x)
|
573
|
+
return rect
|
574
|
+
|
575
|
+
@checkIndex('')
|
576
|
+
def tabText(self, index: int):
|
577
|
+
return self.tabItem(index).text()
|
578
|
+
|
579
|
+
@checkIndex()
|
580
|
+
def tabIcon(self, index: int):
|
581
|
+
return self.tabItem(index).icon()
|
582
|
+
|
583
|
+
@checkIndex('')
|
584
|
+
def tabToolTip(self, index: int):
|
585
|
+
return self.tabItem(index).toolTip()
|
586
|
+
|
587
|
+
def setTabsClosable(self, isClosable: bool):
|
588
|
+
""" set whether the tab is closable """
|
589
|
+
if isClosable:
|
590
|
+
self.setCloseButtonDisplayMode(TabCloseButtonDisplayMode.ALWAYS)
|
591
|
+
else:
|
592
|
+
self.setCloseButtonDisplayMode(TabCloseButtonDisplayMode.NEVER)
|
593
|
+
|
594
|
+
def tabsClosable(self):
|
595
|
+
return self.closeButtonDisplayMode != TabCloseButtonDisplayMode.NEVER
|
596
|
+
|
597
|
+
@checkIndex()
|
598
|
+
def setTabIcon(self, index: int, icon: Union[QIcon, FluentIconBase, str]):
|
599
|
+
""" set tab icon """
|
600
|
+
self.tabItem(index).setIcon(icon)
|
601
|
+
|
602
|
+
@checkIndex()
|
603
|
+
def setTabText(self, index: int, text: str):
|
604
|
+
""" set tab text """
|
605
|
+
self.tabItem(index).setText(text)
|
606
|
+
|
607
|
+
@checkIndex()
|
608
|
+
def setTabVisible(self, index: int, isVisible: bool):
|
609
|
+
""" set the visibility of tab """
|
610
|
+
self.tabItem(index).setVisible(isVisible)
|
611
|
+
|
612
|
+
if isVisible and self.currentIndex() < 0:
|
613
|
+
self.setCurrentIndex(0)
|
614
|
+
elif not isVisible:
|
615
|
+
if self.currentIndex() > 0:
|
616
|
+
self.setCurrentIndex(self.currentIndex() - 1)
|
617
|
+
self.currentChanged.emit(self.currentIndex())
|
618
|
+
elif len(self.items) == 1:
|
619
|
+
self._currentIndex = -1
|
620
|
+
else:
|
621
|
+
self.setCurrentIndex(1)
|
622
|
+
self._currentIndex = 0
|
623
|
+
self.currentChanged.emit(0)
|
624
|
+
|
625
|
+
@checkIndex()
|
626
|
+
def setTabTextColor(self, index: int, color: QColor):
|
627
|
+
""" set the text color of tab item """
|
628
|
+
self.tabItem(index).setTextColor(color)
|
629
|
+
|
630
|
+
@checkIndex()
|
631
|
+
def setTabToolTip(self, index: int, toolTip: str):
|
632
|
+
""" set tool tip of tab """
|
633
|
+
self.tabItem(index).setToolTip(toolTip)
|
634
|
+
|
635
|
+
def setTabSelectedBackgroundColor(self, light: QColor, dark: QColor):
|
636
|
+
""" set the background in selected state """
|
637
|
+
self.lightSelectedBackgroundColor = QColor(light)
|
638
|
+
self.darkSelectedBackgroundColor = QColor(dark)
|
639
|
+
|
640
|
+
for item in self.items:
|
641
|
+
item.setSelectedBackgroundColor(light, dark)
|
642
|
+
|
643
|
+
def setTabShadowEnabled(self, isEnabled: bool):
|
644
|
+
""" set whether the shadow of tab is enabled """
|
645
|
+
if isEnabled == self.isTabShadowEnabled():
|
646
|
+
return
|
647
|
+
|
648
|
+
self._isTabShadowEnabled = isEnabled
|
649
|
+
for item in self.items:
|
650
|
+
item.setShadowEnabled(isEnabled)
|
651
|
+
|
652
|
+
def isTabShadowEnabled(self):
|
653
|
+
return self._isTabShadowEnabled
|
654
|
+
|
655
|
+
def paintEvent(self, e):
|
656
|
+
painter = QPainter(self.viewport())
|
657
|
+
painter.setRenderHints(QPainter.Antialiasing)
|
658
|
+
|
659
|
+
# draw separators
|
660
|
+
if isDarkTheme():
|
661
|
+
color = QColor(255, 255, 255, 21)
|
662
|
+
else:
|
663
|
+
color = QColor(0, 0, 0, 15)
|
664
|
+
|
665
|
+
painter.setPen(color)
|
666
|
+
|
667
|
+
for i, item in enumerate(self.items):
|
668
|
+
canDraw = not (item.isHover or item.isSelected)
|
669
|
+
if i < len(self.items) - 1:
|
670
|
+
nextItem = self.items[i + 1]
|
671
|
+
if nextItem.isHover or nextItem.isSelected:
|
672
|
+
canDraw = False
|
673
|
+
|
674
|
+
if canDraw:
|
675
|
+
x = item.geometry().right()
|
676
|
+
y = self.height() // 2 - 8
|
677
|
+
painter.drawLine(x, y, x, y + 16)
|
678
|
+
|
679
|
+
def setMovable(self, movable: bool):
|
680
|
+
self._isMovable = movable
|
681
|
+
|
682
|
+
def isMovable(self):
|
683
|
+
return self._isMovable
|
684
|
+
|
685
|
+
def setScrollable(self, scrollable: bool):
|
686
|
+
self._isScrollable = scrollable
|
687
|
+
w = self._tabMaxWidth if scrollable else self._tabMinWidth
|
688
|
+
for item in self.items:
|
689
|
+
item.setMinimumWidth(w)
|
690
|
+
|
691
|
+
def setTabMaximumWidth(self, width: int):
|
692
|
+
""" set the maximum width of tab """
|
693
|
+
if width == self._tabMaxWidth:
|
694
|
+
return
|
695
|
+
|
696
|
+
self._tabMaxWidth = width
|
697
|
+
for item in self.items:
|
698
|
+
item.setMaximumWidth(width)
|
699
|
+
|
700
|
+
def setTabMinimumWidth(self, width: int):
|
701
|
+
""" set the minimum width of tab """
|
702
|
+
if width == self._tabMinWidth:
|
703
|
+
return
|
704
|
+
|
705
|
+
self._tabMinWidth = width
|
706
|
+
|
707
|
+
if not self.isScrollable():
|
708
|
+
for item in self.items:
|
709
|
+
item.setMinimumWidth(width)
|
710
|
+
|
711
|
+
def tabMaximumWidth(self):
|
712
|
+
return self._tabMaxWidth
|
713
|
+
|
714
|
+
def tabMinimumWidth(self):
|
715
|
+
return self._tabMinWidth
|
716
|
+
|
717
|
+
def isScrollable(self):
|
718
|
+
return self._isScrollable
|
719
|
+
|
720
|
+
def count(self):
|
721
|
+
""" returns the number of tabs """
|
722
|
+
return len(self.items)
|
723
|
+
|
724
|
+
def mousePressEvent(self, e: QMouseEvent):
|
725
|
+
super().mousePressEvent(e)
|
726
|
+
if not self.isMovable() or e.button() != Qt.LeftButton or \
|
727
|
+
not self.itemLayout.geometry().contains(e.pos()):
|
728
|
+
return
|
729
|
+
|
730
|
+
self.dragPos = e.pos()
|
731
|
+
|
732
|
+
def mouseMoveEvent(self, e: QMouseEvent):
|
733
|
+
super().mouseMoveEvent(e)
|
734
|
+
|
735
|
+
if not self.isMovable() or self.count() <= 1 or not self.itemLayout.geometry().contains(e.pos()):
|
736
|
+
return
|
737
|
+
|
738
|
+
index = self.currentIndex()
|
739
|
+
item = self.tabItem(index)
|
740
|
+
dx = e.pos().x() - self.dragPos.x()
|
741
|
+
self.dragPos = e.pos()
|
742
|
+
|
743
|
+
# first tab can't move left
|
744
|
+
if index == 0 and dx < 0 and item.x() <= 0:
|
745
|
+
return
|
746
|
+
|
747
|
+
# last tab can't move right
|
748
|
+
if index == self.count() - 1 and dx > 0 and item.geometry().right() >= self.itemLayout.sizeHint().width():
|
749
|
+
return
|
750
|
+
|
751
|
+
item.move(item.x() + dx, item.y())
|
752
|
+
self.isDraging = True
|
753
|
+
|
754
|
+
# move the left sibling item to right
|
755
|
+
if dx < 0 and index > 0:
|
756
|
+
siblingIndex = index - 1
|
757
|
+
|
758
|
+
if item.x() < self.tabItem(siblingIndex).geometry().center().x():
|
759
|
+
self._swapItem(siblingIndex)
|
760
|
+
|
761
|
+
# move the right sibling item to left
|
762
|
+
elif dx > 0 and index < self.count() - 1:
|
763
|
+
siblingIndex = index + 1
|
764
|
+
|
765
|
+
if item.geometry().right() > self.tabItem(siblingIndex).geometry().center().x():
|
766
|
+
self._swapItem(siblingIndex)
|
767
|
+
|
768
|
+
def mouseReleaseEvent(self, e):
|
769
|
+
super().mouseReleaseEvent(e)
|
770
|
+
|
771
|
+
if not self.isMovable() or not self.isDraging:
|
772
|
+
return
|
773
|
+
|
774
|
+
self.isDraging = False
|
775
|
+
|
776
|
+
item = self.tabItem(self.currentIndex())
|
777
|
+
x = self.tabRect(self.currentIndex()).x()
|
778
|
+
duration = int(abs(item.x() - x) * 250 / item.width())
|
779
|
+
item.slideTo(x, duration)
|
780
|
+
item.slideAni.finished.connect(self._adjustLayout)
|
781
|
+
|
782
|
+
def _adjustLayout(self):
|
783
|
+
self.sender().finished.disconnect()
|
784
|
+
|
785
|
+
for item in self.items:
|
786
|
+
self.itemLayout.removeWidget(item)
|
787
|
+
|
788
|
+
for item in self.items:
|
789
|
+
self.itemLayout.addWidget(item)
|
790
|
+
|
791
|
+
def _swapItem(self, index: int):
|
792
|
+
items = self.items
|
793
|
+
swappedItem = self.tabItem(index)
|
794
|
+
x = self.tabRect(self.currentIndex()).x()
|
795
|
+
|
796
|
+
items[self.currentIndex()], items[index] = items[index], items[self.currentIndex()]
|
797
|
+
self._currentIndex = index
|
798
|
+
swappedItem.slideTo(x)
|
799
|
+
|
800
|
+
movable = Property(bool, isMovable, setMovable)
|
801
|
+
scrollable = Property(bool, isScrollable, setScrollable)
|
802
|
+
tabMaxWidth = Property(int, tabMaximumWidth, setTabMaximumWidth)
|
803
|
+
tabMinWidth = Property(int, tabMinimumWidth, setTabMinimumWidth)
|
804
|
+
tabShadowEnabled = Property(bool, isTabShadowEnabled, setTabShadowEnabled)
|