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.
Files changed (107) hide show
  1. fastuiwidgets/__init__.py +12 -0
  2. fastuiwidgets/_rc/__init__.py +0 -0
  3. fastuiwidgets/_rc/resource.py +98835 -0
  4. fastuiwidgets/common/__init__.py +12 -0
  5. fastuiwidgets/common/animation.py +530 -0
  6. fastuiwidgets/common/auto_wrap.py +164 -0
  7. fastuiwidgets/common/color.py +95 -0
  8. fastuiwidgets/common/config.py +423 -0
  9. fastuiwidgets/common/exception_handler.py +31 -0
  10. fastuiwidgets/common/font.py +38 -0
  11. fastuiwidgets/common/icon.py +703 -0
  12. fastuiwidgets/common/image_utils.py +198 -0
  13. fastuiwidgets/common/overload.py +47 -0
  14. fastuiwidgets/common/router.py +133 -0
  15. fastuiwidgets/common/screen.py +25 -0
  16. fastuiwidgets/common/smooth_scroll.py +141 -0
  17. fastuiwidgets/common/style_sheet.py +512 -0
  18. fastuiwidgets/common/theme_listener.py +27 -0
  19. fastuiwidgets/common/translator.py +14 -0
  20. fastuiwidgets/components/__init__.py +6 -0
  21. fastuiwidgets/components/date_time/__init__.py +4 -0
  22. fastuiwidgets/components/date_time/calendar_picker.py +121 -0
  23. fastuiwidgets/components/date_time/calendar_view.py +671 -0
  24. fastuiwidgets/components/date_time/date_picker.py +245 -0
  25. fastuiwidgets/components/date_time/fast_calendar_view.py +487 -0
  26. fastuiwidgets/components/date_time/picker_base.py +632 -0
  27. fastuiwidgets/components/date_time/time_picker.py +223 -0
  28. fastuiwidgets/components/dialog_box/__init__.py +6 -0
  29. fastuiwidgets/components/dialog_box/color_dialog.py +414 -0
  30. fastuiwidgets/components/dialog_box/dialog.py +167 -0
  31. fastuiwidgets/components/dialog_box/folder_list_dialog.py +307 -0
  32. fastuiwidgets/components/dialog_box/mask_dialog_base.py +120 -0
  33. fastuiwidgets/components/dialog_box/message_box_base.py +92 -0
  34. fastuiwidgets/components/dialog_box/message_dialog.py +65 -0
  35. fastuiwidgets/components/layout/__init__.py +3 -0
  36. fastuiwidgets/components/layout/expand_layout.py +96 -0
  37. fastuiwidgets/components/layout/flow_layout.py +236 -0
  38. fastuiwidgets/components/layout/v_box_layout.py +41 -0
  39. fastuiwidgets/components/material/__init__.py +6 -0
  40. fastuiwidgets/components/material/acrylic_combo_box.py +96 -0
  41. fastuiwidgets/components/material/acrylic_flyout.py +105 -0
  42. fastuiwidgets/components/material/acrylic_line_edit.py +27 -0
  43. fastuiwidgets/components/material/acrylic_menu.py +204 -0
  44. fastuiwidgets/components/material/acrylic_tool_tip.py +39 -0
  45. fastuiwidgets/components/material/acrylic_widget.py +42 -0
  46. fastuiwidgets/components/navigation/__init__.py +9 -0
  47. fastuiwidgets/components/navigation/breadcrumb.py +350 -0
  48. fastuiwidgets/components/navigation/navigation_bar.py +416 -0
  49. fastuiwidgets/components/navigation/navigation_interface.py +268 -0
  50. fastuiwidgets/components/navigation/navigation_panel.py +657 -0
  51. fastuiwidgets/components/navigation/navigation_widget.py +686 -0
  52. fastuiwidgets/components/navigation/pivot.py +272 -0
  53. fastuiwidgets/components/navigation/segmented_widget.py +174 -0
  54. fastuiwidgets/components/settings/__init__.py +8 -0
  55. fastuiwidgets/components/settings/custom_color_setting_card.py +139 -0
  56. fastuiwidgets/components/settings/expand_setting_card.py +390 -0
  57. fastuiwidgets/components/settings/folder_list_setting_card.py +134 -0
  58. fastuiwidgets/components/settings/options_setting_card.py +86 -0
  59. fastuiwidgets/components/settings/setting_card.py +449 -0
  60. fastuiwidgets/components/settings/setting_card_group.py +48 -0
  61. fastuiwidgets/components/widgets/__init__.py +41 -0
  62. fastuiwidgets/components/widgets/acrylic_label.py +261 -0
  63. fastuiwidgets/components/widgets/button.py +1059 -0
  64. fastuiwidgets/components/widgets/card_widget.py +369 -0
  65. fastuiwidgets/components/widgets/check_box.py +203 -0
  66. fastuiwidgets/components/widgets/combo_box.py +556 -0
  67. fastuiwidgets/components/widgets/command_bar.py +636 -0
  68. fastuiwidgets/components/widgets/cycle_list_widget.py +251 -0
  69. fastuiwidgets/components/widgets/flip_view.py +430 -0
  70. fastuiwidgets/components/widgets/flyout.py +521 -0
  71. fastuiwidgets/components/widgets/frameless_window.py +49 -0
  72. fastuiwidgets/components/widgets/icon_widget.py +53 -0
  73. fastuiwidgets/components/widgets/info_badge.py +483 -0
  74. fastuiwidgets/components/widgets/info_bar.py +596 -0
  75. fastuiwidgets/components/widgets/label.py +553 -0
  76. fastuiwidgets/components/widgets/line_edit.py +551 -0
  77. fastuiwidgets/components/widgets/list_view.py +158 -0
  78. fastuiwidgets/components/widgets/menu.py +1318 -0
  79. fastuiwidgets/components/widgets/pips_pager.py +331 -0
  80. fastuiwidgets/components/widgets/progress_bar.py +311 -0
  81. fastuiwidgets/components/widgets/progress_ring.py +212 -0
  82. fastuiwidgets/components/widgets/scroll_area.py +125 -0
  83. fastuiwidgets/components/widgets/scroll_bar.py +673 -0
  84. fastuiwidgets/components/widgets/separator.py +43 -0
  85. fastuiwidgets/components/widgets/slider.py +307 -0
  86. fastuiwidgets/components/widgets/spin_box.py +306 -0
  87. fastuiwidgets/components/widgets/stacked_widget.py +211 -0
  88. fastuiwidgets/components/widgets/state_tool_tip.py +188 -0
  89. fastuiwidgets/components/widgets/switch_button.py +312 -0
  90. fastuiwidgets/components/widgets/tab_view.py +804 -0
  91. fastuiwidgets/components/widgets/table_view.py +360 -0
  92. fastuiwidgets/components/widgets/teaching_tip.py +657 -0
  93. fastuiwidgets/components/widgets/tool_tip.py +460 -0
  94. fastuiwidgets/components/widgets/tree_view.py +216 -0
  95. fastuiwidgets/multimedia/__init__.py +3 -0
  96. fastuiwidgets/multimedia/media_play_bar.py +319 -0
  97. fastuiwidgets/multimedia/media_player.py +124 -0
  98. fastuiwidgets/multimedia/video_widget.py +93 -0
  99. fastuiwidgets/window/__init__.py +2 -0
  100. fastuiwidgets/window/fluent_window.py +413 -0
  101. fastuiwidgets/window/splash_screen.py +92 -0
  102. fastuiwidgets/window/stacked_widget.py +66 -0
  103. python_fastui_widgets-1.0.0.dist-info/METADATA +30 -0
  104. python_fastui_widgets-1.0.0.dist-info/RECORD +107 -0
  105. python_fastui_widgets-1.0.0.dist-info/WHEEL +5 -0
  106. python_fastui_widgets-1.0.0.dist-info/licenses/LICENSE +674 -0
  107. 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)