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,1318 @@
1
+ # coding:utf-8
2
+ from enum import Enum
3
+ from typing import List, Union
4
+
5
+ from fframelesswindow import WindowEffect
6
+ from PySide6.QtCore import (QEasingCurve, QEvent, QPropertyAnimation, QObject, QModelIndex,
7
+ Qt, QSize, QRectF, Signal, QPoint, QTimer, QObject, QParallelAnimationGroup, QRect)
8
+ from PySide6.QtGui import (QAction, QIcon, QColor, QPainter, QPen, QPixmap, QRegion, QCursor, QTextCursor, QHoverEvent,
9
+ QFontMetrics, QKeySequence)
10
+ from PySide6.QtWidgets import (QApplication, QMenu, QProxyStyle, QStyle, QStyleFactory,
11
+ QGraphicsDropShadowEffect, QListWidget, QWidget, QHBoxLayout,
12
+ QListWidgetItem, QLineEdit, QTextEdit, QStyledItemDelegate, QStyleOptionViewItem, QLabel)
13
+
14
+ from ...common.icon import FluentIcon as FIF
15
+ from ...common.icon import FluentIconEngine, Action, FluentIconBase, Icon
16
+ from ...common.style_sheet import FluentStyleSheet, themeColor
17
+ from ...common.screen import getCurrentScreenGeometry
18
+ from ...common.font import getFont
19
+ from ...common.config import isDarkTheme
20
+ from .scroll_bar import SmoothScrollDelegate
21
+ from .tool_tip import ItemViewToolTipDelegate, ItemViewToolTipType
22
+
23
+
24
+ class CustomMenuStyle(QProxyStyle):
25
+ """ Custom menu style """
26
+
27
+ def __init__(self, iconSize=14):
28
+ """
29
+ Parameters
30
+ ----------
31
+ iconSizeL int
32
+ the size of icon
33
+ """
34
+ super().__init__()
35
+ self.iconSize = iconSize
36
+
37
+ def pixelMetric(self, metric, option, widget):
38
+ if metric == QStyle.PM_SmallIconSize:
39
+ return self.iconSize
40
+
41
+ return super().pixelMetric(metric, option, widget)
42
+
43
+
44
+ class DWMMenu(QMenu):
45
+ """ A menu with DWM shadow """
46
+
47
+ def __init__(self, title="", parent=None):
48
+ super().__init__(title, parent)
49
+ self.windowEffect = WindowEffect(self)
50
+ self.setWindowFlags(
51
+ Qt.FramelessWindowHint | Qt.Popup | Qt.NoDropShadowWindowHint)
52
+ self.setAttribute(Qt.WA_StyledBackground)
53
+ self.setStyle(CustomMenuStyle())
54
+ FluentStyleSheet.MENU.apply(self)
55
+
56
+ def event(self, e: QEvent):
57
+ if e.type() == QEvent.WinIdChange:
58
+ self.windowEffect.addMenuShadowEffect(self.winId())
59
+ return QMenu.event(self, e)
60
+
61
+
62
+ class MenuAnimationType(Enum):
63
+ """ Menu animation type """
64
+
65
+ NONE = 0
66
+ DROP_DOWN = 1
67
+ PULL_UP = 2
68
+ FADE_IN_DROP_DOWN = 3
69
+ FADE_IN_PULL_UP = 4
70
+
71
+
72
+
73
+ class SubMenuItemWidget(QWidget):
74
+ """ Sub menu item """
75
+
76
+ showMenuSig = Signal(QListWidgetItem)
77
+
78
+ def __init__(self, menu, item, parent=None):
79
+ """
80
+ Parameters
81
+ ----------
82
+ menu: QMenu | RoundMenu
83
+ sub menu
84
+
85
+ item: QListWidgetItem
86
+ menu item
87
+
88
+ parent: QWidget
89
+ parent widget
90
+ """
91
+ super().__init__(parent)
92
+ self.menu = menu
93
+ self.item = item
94
+
95
+ def enterEvent(self, e):
96
+ super().enterEvent(e)
97
+ self.showMenuSig.emit(self.item)
98
+
99
+ def paintEvent(self, e):
100
+ painter = QPainter(self)
101
+ painter.setRenderHints(QPainter.Antialiasing)
102
+
103
+ # draw right arrow
104
+ FIF.CHEVRON_RIGHT.render(painter, QRectF(
105
+ self.width()-10, self.height()/2-9/2, 9, 9))
106
+
107
+
108
+ class MenuItemDelegate(QStyledItemDelegate):
109
+ """ Menu item delegate """
110
+
111
+ def __init__(self, parent=None):
112
+ super().__init__(parent)
113
+ self.tooltipDelegate = None
114
+
115
+ def _isSeparator(self, index: QModelIndex):
116
+ return index.model().data(index, Qt.DecorationRole) == "seperator"
117
+
118
+ def paint(self, painter, option, index):
119
+ if not self._isSeparator(index):
120
+ return super().paint(painter, option, index)
121
+
122
+ # draw seperator
123
+ painter.save()
124
+
125
+ c = 0 if not isDarkTheme() else 255
126
+ pen = QPen(QColor(c, c, c, 25), 1)
127
+ pen.setCosmetic(True)
128
+ painter.setPen(pen)
129
+ rect = option.rect
130
+ painter.drawLine(0, rect.y() + 4, rect.width() + 12, rect.y() + 4)
131
+
132
+ painter.restore()
133
+
134
+ def helpEvent(self, event, view, option, index):
135
+ if not self.tooltipDelegate:
136
+ self.tooltipDelegate = ItemViewToolTipDelegate(view, 100, ItemViewToolTipType.LIST)
137
+
138
+ return self.tooltipDelegate.helpEvent(event, view, option, index)
139
+
140
+
141
+ class ShortcutMenuItemDelegate(MenuItemDelegate):
142
+ """ Shortcut key menu item delegate """
143
+
144
+ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
145
+ super().paint(painter, option, index)
146
+ if self._isSeparator(index):
147
+ return
148
+
149
+ # draw shortcut key
150
+ action = index.data(Qt.UserRole) # type: QAction
151
+ if not isinstance(action, QAction) or action.shortcut().isEmpty():
152
+ return
153
+
154
+ painter.save()
155
+
156
+ if not option.state & QStyle.State_Enabled:
157
+ painter.setOpacity(0.5 if isDarkTheme() else 0.6)
158
+
159
+ font = getFont(12)
160
+ painter.setFont(font)
161
+ painter.setPen(QColor(255, 255, 255, 200) if isDarkTheme() else QColor(0, 0, 0, 153))
162
+
163
+ fm = QFontMetrics(font)
164
+ shortcut = action.shortcut().toString(QKeySequence.NativeText)
165
+
166
+ sw = fm.boundingRect(shortcut).width()
167
+ painter.translate(option.rect.width()-sw-20, 0)
168
+
169
+ rect = QRectF(0, option.rect.y(), sw, option.rect.height())
170
+ painter.drawText(rect, Qt.AlignLeft | Qt.AlignVCenter, shortcut)
171
+
172
+ painter.restore()
173
+
174
+
175
+ class MenuActionListWidget(QListWidget):
176
+ """ Menu action list widget """
177
+
178
+ def __init__(self, parent=None):
179
+ super().__init__(parent)
180
+ self._itemHeight = 28
181
+ self._maxVisibleItems = -1 # adjust visible items according to the size of screen
182
+
183
+ self.setViewportMargins(0, 6, 0, 6)
184
+ self.setTextElideMode(Qt.ElideNone)
185
+ self.setDragEnabled(False)
186
+ self.setMouseTracking(True)
187
+ self.setVerticalScrollMode(self.ScrollMode.ScrollPerPixel)
188
+ self.setIconSize(QSize(14, 14))
189
+ self.setItemDelegate(ShortcutMenuItemDelegate(self))
190
+
191
+ self.scrollDelegate = SmoothScrollDelegate(self)
192
+ self.setStyleSheet(
193
+ 'MenuActionListWidget{font: 14px "Segoe UI", "Microsoft YaHei", "PingFang SC"}')
194
+
195
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
196
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
197
+
198
+ def insertItem(self, row, item):
199
+ """ inserts menu item at the position in the list given by row """
200
+ super().insertItem(row, item)
201
+ self.adjustSize()
202
+
203
+ def addItem(self, item):
204
+ """ add menu item at the end """
205
+ super().addItem(item)
206
+ self.adjustSize()
207
+
208
+ def takeItem(self, row):
209
+ """ delete item from list """
210
+ item = super().takeItem(row)
211
+ self.adjustSize()
212
+ return item
213
+
214
+ def adjustSize(self, pos=None, aniType=MenuAnimationType.NONE):
215
+ size = QSize()
216
+ for i in range(self.count()):
217
+ s = self.item(i).sizeHint()
218
+ size.setWidth(max(s.width(), size.width(), 1))
219
+ size.setHeight(max(1, size.height() + s.height()))
220
+
221
+ # adjust the height of viewport
222
+ w, h = MenuAnimationManager.make(self, aniType).availableViewSize(pos)
223
+
224
+ # fixes https://github.com/NumBNN/Python-FastUI-Widgets/issues/844
225
+ # self.viewport().adjustSize()
226
+
227
+ # adjust the height of list widget
228
+ m = self.viewportMargins()
229
+ size += QSize(m.left()+m.right()+2, m.top()+m.bottom())
230
+ size.setHeight(min(h, size.height()+3))
231
+ size.setWidth(max(min(w, size.width()), self.minimumWidth()))
232
+
233
+ if self.maxVisibleItems() > 0:
234
+ size.setHeight(min(
235
+ size.height(), self.maxVisibleItems() * self._itemHeight + m.top()+m.bottom() + 3))
236
+
237
+ self.setFixedSize(size)
238
+
239
+ def setItemHeight(self, height: int):
240
+ """ set the height of item """
241
+ if height == self._itemHeight:
242
+ return
243
+
244
+ for i in range(self.count()):
245
+ item = self.item(i)
246
+ if not self.itemWidget(item):
247
+ item.setSizeHint(QSize(item.sizeHint().width(), height))
248
+
249
+ self._itemHeight = height
250
+ self.adjustSize()
251
+
252
+ def setMaxVisibleItems(self, num: int):
253
+ """ set the maximum visible items """
254
+ self._maxVisibleItems = num
255
+ self.adjustSize()
256
+
257
+ def maxVisibleItems(self):
258
+ return self._maxVisibleItems
259
+
260
+ def heightForAnimation(self, pos: QPoint, aniType: MenuAnimationType):
261
+ """ height for animation """
262
+ ih = self.itemsHeight()
263
+ _, sh = MenuAnimationManager.make(self, aniType).availableViewSize(pos)
264
+ return min(ih, sh)
265
+
266
+ def itemsHeight(self):
267
+ """ Return the height of all items """
268
+ N = self.count() if self.maxVisibleItems() < 0 else min(self.maxVisibleItems(), self.count())
269
+ h = sum(self.item(i).sizeHint().height() for i in range(N))
270
+ m = self.viewportMargins()
271
+ return h + m.top() + m.bottom()
272
+
273
+
274
+ class RoundMenu(QMenu):
275
+ """ Round corner menu """
276
+
277
+ closedSignal = Signal()
278
+
279
+ def __init__(self, title="", parent=None):
280
+ super().__init__(parent=parent)
281
+ self.setTitle(title)
282
+ self._icon = QIcon()
283
+ self._actions = [] # type: List[QAction]
284
+ self._subMenus = []
285
+
286
+ self.isSubMenu = False
287
+ self.parentMenu = None
288
+ self.menuItem = None
289
+ self.lastHoverItem = None
290
+ self.lastHoverSubMenuItem = None
291
+ self.isHideBySystem = True
292
+ self.itemHeight = 28
293
+
294
+ self.hBoxLayout = QHBoxLayout(self)
295
+ self.view = MenuActionListWidget(self)
296
+
297
+ self.aniManager = None
298
+ self.timer = QTimer(self)
299
+
300
+ self.__initWidgets()
301
+
302
+ def __initWidgets(self):
303
+ self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint |
304
+ Qt.NoDropShadowWindowHint)
305
+ self.setAttribute(Qt.WA_TranslucentBackground)
306
+ self.setMouseTracking(True)
307
+
308
+ # fixes https://github.com/NumBNN/Python-FastUI-Widgets/issues/848
309
+ self.setStyle(QStyleFactory.create("fusion"))
310
+
311
+ self.timer.setSingleShot(True)
312
+ self.timer.setInterval(400)
313
+ self.timer.timeout.connect(self._onShowMenuTimeOut)
314
+
315
+ self.setShadowEffect()
316
+ self.hBoxLayout.addWidget(self.view, 1, Qt.AlignCenter)
317
+
318
+ self.hBoxLayout.setContentsMargins(12, 8, 12, 20)
319
+ FluentStyleSheet.MENU.apply(self)
320
+
321
+ self.view.itemClicked.connect(self._onItemClicked)
322
+ self.view.itemEntered.connect(self._onItemEntered)
323
+
324
+ def setMaxVisibleItems(self, num: int):
325
+ """ set the maximum visible items """
326
+ self.view.setMaxVisibleItems(num)
327
+ self.adjustSize()
328
+
329
+ def setItemHeight(self, height):
330
+ """ set the height of menu item """
331
+ if height == self.itemHeight:
332
+ return
333
+
334
+ self.itemHeight = height
335
+ self.view.setItemHeight(height)
336
+
337
+ def setShadowEffect(self, blurRadius=30, offset=(0, 8), color=QColor(0, 0, 0, 30)):
338
+ """ add shadow to dialog """
339
+ self.shadowEffect = QGraphicsDropShadowEffect(self.view)
340
+ self.shadowEffect.setBlurRadius(blurRadius)
341
+ self.shadowEffect.setOffset(*offset)
342
+ self.shadowEffect.setColor(color)
343
+ self.view.setGraphicsEffect(None)
344
+ self.view.setGraphicsEffect(self.shadowEffect)
345
+
346
+ def _setParentMenu(self, parent, item):
347
+ self.parentMenu = parent
348
+ self.menuItem = item
349
+ self.isSubMenu = True if parent else False
350
+
351
+ def adjustSize(self):
352
+ m = self.layout().contentsMargins()
353
+ w = self.view.width() + m.left() + m.right()
354
+ h = self.view.height() + m.top() + m.bottom()
355
+ self.setFixedSize(w, h)
356
+
357
+ def icon(self):
358
+ return self._icon
359
+
360
+ def title(self):
361
+ return self._title
362
+
363
+ def clear(self):
364
+ """ clear all actions """
365
+ while self._actions:
366
+ self.removeAction(self._actions[-1])
367
+
368
+ while self._subMenus:
369
+ self.removeMenu(self._subMenus[-1])
370
+
371
+ def setIcon(self, icon: Union[QIcon, FluentIconBase]):
372
+ """ set the icon of menu """
373
+ if isinstance(icon, FluentIconBase):
374
+ icon = Icon(icon)
375
+
376
+ self._icon = icon
377
+
378
+ def setTitle(self, title: str):
379
+ self._title = title
380
+ super().setTitle(title)
381
+
382
+ def addAction(self, action: Union[QAction, Action]):
383
+ """ add action to menu
384
+
385
+ Parameters
386
+ ----------
387
+ action: QAction
388
+ menu action
389
+ """
390
+ item = self._createActionItem(action)
391
+ self.view.addItem(item)
392
+ self.adjustSize()
393
+
394
+ def addWidget(self, widget: QWidget, selectable=True, onClick=None):
395
+ """ add custom widget
396
+
397
+ Parameters
398
+ ----------
399
+ widget: QWidget
400
+ custom widget
401
+
402
+ selectable: bool
403
+ whether the menu item is selectable
404
+
405
+ onClick: callable
406
+ the slot connected to item clicked signal
407
+ """
408
+ action = QAction()
409
+ action.setProperty('selectable', selectable)
410
+
411
+ item = self._createActionItem(action)
412
+ item.setSizeHint(widget.size())
413
+
414
+ self.view.addItem(item)
415
+ self.view.setItemWidget(item, widget)
416
+
417
+ if not selectable:
418
+ item.setFlags(Qt.NoItemFlags)
419
+
420
+ if onClick:
421
+ action.triggered.connect(onClick)
422
+
423
+ self.adjustSize()
424
+
425
+ def _createActionItem(self, action: QAction, before=None):
426
+ """ create menu action item """
427
+ if not before:
428
+ self._actions.append(action)
429
+ super().addAction(action)
430
+ elif before in self._actions:
431
+ index = self._actions.index(before)
432
+ self._actions.insert(index, action)
433
+ super().insertAction(before, action)
434
+ else:
435
+ raise ValueError('`before` is not in the action list')
436
+
437
+ item = QListWidgetItem(self._createItemIcon(action), action.text())
438
+ self._adjustItemText(item, action)
439
+
440
+ # disable item if the action is not enabled
441
+ if not action.isEnabled():
442
+ item.setFlags(Qt.NoItemFlags)
443
+ if action.text() != action.toolTip():
444
+ item.setToolTip(action.toolTip())
445
+
446
+ item.setData(Qt.UserRole, action)
447
+ action.setProperty('item', item)
448
+ action.changed.connect(self._onActionChanged)
449
+ return item
450
+
451
+ def _hasItemIcon(self):
452
+ return any(not i.icon().isNull() for i in self._actions+self._subMenus)
453
+
454
+ def _adjustItemText(self, item: QListWidgetItem, action: QAction):
455
+ """ adjust the text of item """
456
+ # leave some space for shortcut key
457
+ if isinstance(self.view.itemDelegate(), ShortcutMenuItemDelegate):
458
+ sw = self._longestShortcutWidth()
459
+ if sw:
460
+ sw += 22
461
+ else:
462
+ sw = 0
463
+
464
+ # adjust the width of item
465
+ if not self._hasItemIcon():
466
+ item.setText(action.text())
467
+ w = 40 + self.view.fontMetrics().boundingRect(action.text()).width() + sw
468
+ else:
469
+ # add a blank character to increase space between icon and text
470
+ item.setText(" " + action.text())
471
+ space = 4 - self.view.fontMetrics().boundingRect(" ").width()
472
+ w = 60 + self.view.fontMetrics().boundingRect(item.text()).width() + sw + space
473
+
474
+ item.setSizeHint(QSize(w, self.itemHeight))
475
+ return w
476
+
477
+ def _longestShortcutWidth(self):
478
+ """ longest shortcut key """
479
+ fm = QFontMetrics(getFont(12))
480
+ return max(fm.boundingRect(a.shortcut().toString()).width() for a in self.menuActions())
481
+
482
+ def _createItemIcon(self, w):
483
+ """ create the icon of menu item """
484
+ hasIcon = self._hasItemIcon()
485
+ icon = QIcon(FluentIconEngine(w.icon()))
486
+
487
+ if hasIcon and w.icon().isNull():
488
+ pixmap = QPixmap(self.view.iconSize())
489
+ pixmap.fill(Qt.transparent)
490
+ icon = QIcon(pixmap)
491
+ elif not hasIcon:
492
+ icon = QIcon()
493
+
494
+ return icon
495
+
496
+ def insertAction(self, before: Union[QAction, Action], action: Union[QAction, Action]):
497
+ """ inserts action to menu, before the action before """
498
+ if before not in self._actions:
499
+ return
500
+
501
+ beforeItem = before.property('item')
502
+ if not beforeItem:
503
+ return
504
+
505
+ index = self.view.row(beforeItem)
506
+ item = self._createActionItem(action, before)
507
+ self.view.insertItem(index, item)
508
+ self.adjustSize()
509
+
510
+ def addActions(self, actions: List[Union[QAction, Action]]):
511
+ """ add actions to menu
512
+
513
+ Parameters
514
+ ----------
515
+ actions: Iterable[QAction]
516
+ menu actions
517
+ """
518
+ for action in actions:
519
+ self.addAction(action)
520
+
521
+ def insertActions(self, before: Union[QAction, Action], actions: List[Union[QAction, Action]]):
522
+ """ inserts the actions actions to menu, before the action before """
523
+ for action in actions:
524
+ self.insertAction(before, action)
525
+
526
+ def removeAction(self, action: Union[QAction, Action]):
527
+ """ remove action from menu """
528
+ if action not in self._actions:
529
+ return
530
+
531
+ # remove action
532
+ item = action.property("item")
533
+ self._actions.remove(action)
534
+ action.setProperty('item', None)
535
+
536
+ if not item:
537
+ return
538
+
539
+ # remove item
540
+ self._removeItem(item)
541
+ super().removeAction(action)
542
+
543
+ def removeMenu(self, menu):
544
+ """ remove submenu """
545
+ if menu not in self._subMenus:
546
+ return
547
+
548
+ item = menu.menuItem
549
+ self._subMenus.remove(menu)
550
+ self._removeItem(item)
551
+
552
+ def setDefaultAction(self, action: Union[QAction, Action]):
553
+ """ set the default action """
554
+ if action not in self._actions:
555
+ return
556
+
557
+ item = action.property("item")
558
+ if item:
559
+ self.view.setCurrentItem(item)
560
+
561
+ def addMenu(self, menu):
562
+ """ add sub menu
563
+
564
+ Parameters
565
+ ----------
566
+ menu: RoundMenu
567
+ sub round menu
568
+ """
569
+ if not isinstance(menu, RoundMenu):
570
+ raise ValueError('`menu` should be an instance of `RoundMenu`.')
571
+
572
+ item, w = self._createSubMenuItem(menu)
573
+ self.view.addItem(item)
574
+ self.view.setItemWidget(item, w)
575
+ self.adjustSize()
576
+
577
+ def insertMenu(self, before: Union[QAction, Action], menu):
578
+ """ insert menu before action `before` """
579
+ if not isinstance(menu, RoundMenu):
580
+ raise ValueError('`menu` should be an instance of `RoundMenu`.')
581
+
582
+ if before not in self._actions:
583
+ raise ValueError('`before` should be in menu action list')
584
+
585
+ item, w = self._createSubMenuItem(menu)
586
+ self.view.insertItem(self.view.row(before.property('item')), item)
587
+ self.view.setItemWidget(item, w)
588
+ self.adjustSize()
589
+
590
+ def _createSubMenuItem(self, menu):
591
+ self._subMenus.append(menu)
592
+
593
+ item = QListWidgetItem(self._createItemIcon(menu), menu.title())
594
+ if not self._hasItemIcon():
595
+ w = 60 + self.view.fontMetrics().boundingRect(menu.title()).width()
596
+ else:
597
+ # add a blank character to increase space between icon and text
598
+ item.setText(" " + item.text())
599
+ w = 72 + self.view.fontMetrics().boundingRect(item.text()).width()
600
+
601
+ # add submenu item
602
+ menu._setParentMenu(self, item)
603
+ item.setSizeHint(QSize(w, self.itemHeight))
604
+ item.setData(Qt.UserRole, menu)
605
+ w = SubMenuItemWidget(menu, item, self)
606
+ w.showMenuSig.connect(self._showSubMenu)
607
+ w.resize(item.sizeHint())
608
+
609
+ return item, w
610
+
611
+ def _removeItem(self, item):
612
+ self.view.takeItem(self.view.row(item))
613
+ item.setData(Qt.UserRole, None)
614
+
615
+ # delete widget
616
+ widget = self.view.itemWidget(item)
617
+ if widget:
618
+ widget.deleteLater()
619
+
620
+ def _showSubMenu(self, item):
621
+ """ show sub menu """
622
+ self.lastHoverItem = item
623
+ self.lastHoverSubMenuItem = item
624
+ # delay 400 ms to anti-shake
625
+ self.timer.stop()
626
+ self.timer.start()
627
+
628
+ def _onShowMenuTimeOut(self):
629
+ if self.lastHoverSubMenuItem is None or not self.lastHoverItem is self.lastHoverSubMenuItem:
630
+ return
631
+
632
+ w = self.view.itemWidget(self.lastHoverSubMenuItem)
633
+
634
+ if w.menu.parentMenu.isHidden():
635
+ return
636
+
637
+ itemRect = QRect(w.mapToGlobal(w.rect().topLeft()), w.size())
638
+ x = itemRect.right() + 5
639
+ y = itemRect.y() - 5
640
+
641
+ screenRect = getCurrentScreenGeometry()
642
+ subMenuSize = w.menu.sizeHint()
643
+ if (x + subMenuSize.width()) > screenRect.right():
644
+ x = max(itemRect.left() - subMenuSize.width() - 5, screenRect.left())
645
+
646
+ if (y + subMenuSize.height()) > screenRect.bottom():
647
+ y = screenRect.bottom() - subMenuSize.height()
648
+
649
+ y = max(y, screenRect.top())
650
+
651
+ w.menu.exec(QPoint(x, y))
652
+
653
+ def addSeparator(self):
654
+ """ add seperator to menu """
655
+ m = self.view.viewportMargins()
656
+ w = self.view.width()-m.left()-m.right()
657
+
658
+ # add separator to list widget
659
+ item = QListWidgetItem()
660
+ item.setFlags(Qt.NoItemFlags)
661
+ item.setSizeHint(QSize(w, 9))
662
+ self.view.addItem(item)
663
+ item.setData(Qt.DecorationRole, "seperator")
664
+ self.adjustSize()
665
+
666
+ def _onItemClicked(self, item):
667
+ action = item.data(Qt.UserRole) # type: QAction
668
+ if action not in self._actions or not action.isEnabled():
669
+ return
670
+
671
+ if self.view.itemWidget(item) and not action.property('selectable'):
672
+ return
673
+
674
+ self._hideMenu(False)
675
+
676
+ if not self.isSubMenu:
677
+ action.trigger()
678
+ return
679
+
680
+ # close parent menu
681
+ self._closeParentMenu()
682
+ action.trigger()
683
+
684
+ def _closeParentMenu(self):
685
+ menu = self
686
+ while menu:
687
+ menu.close()
688
+ menu = menu.parentMenu
689
+
690
+ def _onItemEntered(self, item):
691
+ self.lastHoverItem = item
692
+ if not isinstance(item.data(Qt.UserRole), RoundMenu):
693
+ return
694
+
695
+ self._showSubMenu(item)
696
+
697
+ def _hideMenu(self, isHideBySystem=False):
698
+ self.isHideBySystem = isHideBySystem
699
+ self.view.clearSelection()
700
+ if self.isSubMenu:
701
+ self.hide()
702
+ else:
703
+ self.close()
704
+
705
+ def hideEvent(self, e):
706
+ if self.isHideBySystem and self.isSubMenu:
707
+ self._closeParentMenu()
708
+
709
+ self.isHideBySystem = True
710
+ e.accept()
711
+
712
+ def closeEvent(self, e):
713
+ e.accept()
714
+ self.closedSignal.emit()
715
+ self.view.clearSelection()
716
+
717
+ def menuActions(self):
718
+ return self._actions
719
+
720
+ def mousePressEvent(self, e):
721
+ w = self.childAt(e.pos())
722
+ if (w is not self.view) and (not self.view.isAncestorOf(w)):
723
+ self._hideMenu(True)
724
+
725
+ def mouseMoveEvent(self, e):
726
+ if not self.isSubMenu:
727
+ return
728
+
729
+ # hide submenu when mouse moves out of submenu item
730
+ pos = e.globalPos()
731
+ view = self.parentMenu.view
732
+
733
+ # get the rect of menu item
734
+ margin = view.viewportMargins()
735
+ rect = view.visualItemRect(self.menuItem).translated(view.mapToGlobal(QPoint()))
736
+ rect = rect.translated(margin.left(), margin.top()+2)
737
+ if self.parentMenu.geometry().contains(pos) and not rect.contains(pos) and \
738
+ not self.geometry().contains(pos):
739
+ view.clearSelection()
740
+ self._hideMenu(False)
741
+
742
+ def _onActionChanged(self):
743
+ """ action changed slot """
744
+ action = self.sender() # type: QAction
745
+ item = action.property('item') # type: QListWidgetItem
746
+ item.setIcon(self._createItemIcon(action))
747
+
748
+ if action.text() != action.toolTip():
749
+ item.setToolTip(action.toolTip())
750
+
751
+ self._adjustItemText(item, action)
752
+
753
+ if action.isEnabled():
754
+ item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
755
+ else:
756
+ item.setFlags(Qt.NoItemFlags)
757
+
758
+ self.view.adjustSize()
759
+ self.adjustSize()
760
+
761
+ def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
762
+ """ show menu
763
+
764
+ Parameters
765
+ ----------
766
+ pos: QPoint
767
+ pop-up position
768
+
769
+ ani: bool
770
+ Whether to show pop-up animation
771
+
772
+ aniType: MenuAnimationType
773
+ menu animation type
774
+ """
775
+ #if self.isVisible():
776
+ # aniType = MenuAnimationType.NONE
777
+
778
+ self.aniManager = MenuAnimationManager.make(self, aniType)
779
+ self.aniManager.exec(pos)
780
+
781
+ self.show()
782
+
783
+ if self.isSubMenu:
784
+ self.menuItem.setSelected(True)
785
+
786
+ def exec_(self, pos: QPoint, ani=True, aniType=MenuAnimationType.DROP_DOWN):
787
+ """ show menu
788
+
789
+ Parameters
790
+ ----------
791
+ pos: QPoint
792
+ pop-up position
793
+
794
+ ani: bool
795
+ Whether to show pop-up animation
796
+
797
+ aniType: MenuAnimationType
798
+ menu animation type
799
+ """
800
+ self.exec(pos, ani, aniType)
801
+
802
+ def adjustPosition(self):
803
+ m = self.layout().contentsMargins()
804
+ rect = getCurrentScreenGeometry()
805
+ w, h = self.layout().sizeHint().width() + 5, self.layout().sizeHint().height()
806
+
807
+ x = min(self.x() - m.left(), rect.right() - w)
808
+ y = self.y()
809
+ if y > rect.bottom() - h:
810
+ y = self.y() - h + m.bottom()
811
+
812
+ self.move(x, y)
813
+
814
+ def paintEvent(self, e):
815
+ pass
816
+
817
+
818
+ class MenuAnimationManager(QObject):
819
+ """ Menu animation manager """
820
+
821
+ managers = {}
822
+
823
+ def __init__(self, menu: RoundMenu):
824
+ super().__init__()
825
+ self.menu = menu
826
+ self.ani = QPropertyAnimation(menu, b'pos', menu)
827
+
828
+ self.ani.setDuration(250)
829
+ self.ani.setEasingCurve(QEasingCurve.OutQuad)
830
+ self.ani.valueChanged.connect(self._onValueChanged)
831
+ self.ani.valueChanged.connect(self._updateMenuViewport)
832
+
833
+ def _onValueChanged(self):
834
+ pass
835
+
836
+ def availableViewSize(self, pos: QPoint):
837
+ """ Return the available size of view """
838
+ ss = getCurrentScreenGeometry()
839
+ w, h = ss.width() - 100, ss.height() - 100
840
+ return w, h
841
+
842
+ def _updateMenuViewport(self):
843
+ self.menu.view.viewport().update()
844
+ self.menu.view.setAttribute(Qt.WA_UnderMouse, True)
845
+ e = QHoverEvent(QEvent.HoverEnter, QPoint(), QPoint(1, 1))
846
+ QApplication.sendEvent(self.menu.view, e)
847
+
848
+ def _endPosition(self, pos):
849
+ m = self.menu
850
+ rect = getCurrentScreenGeometry()
851
+ w, h = m.width() + 5, m.height()
852
+ x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
853
+ y = min(pos.y() - 4, rect.bottom() - h + 10)
854
+
855
+ return QPoint(x, y)
856
+
857
+ def _menuSize(self):
858
+ m = self.menu.layout().contentsMargins()
859
+ w = self.menu.view.width() + m.left() + m.right() + 120
860
+ h = self.menu.view.height() + m.top() + m.bottom() + 20
861
+ return w, h
862
+
863
+ def exec(self, pos: QPoint):
864
+ pass
865
+
866
+ @classmethod
867
+ def register(cls, name):
868
+ """ register menu animation manager
869
+
870
+ Parameters
871
+ ----------
872
+ name: Any
873
+ the name of manager, it should be unique
874
+ """
875
+ def wrapper(Manager):
876
+ if name not in cls.managers:
877
+ cls.managers[name] = Manager
878
+
879
+ return Manager
880
+
881
+ return wrapper
882
+
883
+ @classmethod
884
+ def make(cls, menu: RoundMenu, aniType: MenuAnimationType):
885
+ if aniType not in cls.managers:
886
+ raise ValueError(f'`{aniType}` is an invalid menu animation type.')
887
+
888
+ return cls.managers[aniType](menu)
889
+
890
+
891
+ @MenuAnimationManager.register(MenuAnimationType.NONE)
892
+ class DummyMenuAnimationManager(MenuAnimationManager):
893
+ """ Dummy menu animation manager """
894
+
895
+ def exec(self, pos: QPoint):
896
+ self.menu.move(self._endPosition(pos))
897
+
898
+
899
+ @MenuAnimationManager.register(MenuAnimationType.DROP_DOWN)
900
+ class DropDownMenuAnimationManager(MenuAnimationManager):
901
+ """ Drop down menu animation manager """
902
+
903
+ def exec(self, pos):
904
+ pos = self._endPosition(pos)
905
+ h = self.menu.height() + 5
906
+
907
+ self.ani.setStartValue(pos-QPoint(0, int(h/2)))
908
+ self.ani.setEndValue(pos)
909
+ self.ani.start()
910
+
911
+ def availableViewSize(self, pos: QPoint):
912
+ ss = getCurrentScreenGeometry()
913
+ return ss.width() - 100, max(ss.bottom() - pos.y() - 10, 1)
914
+
915
+ def _onValueChanged(self):
916
+ w, h = self._menuSize()
917
+ y = self.ani.endValue().y() - self.ani.currentValue().y()
918
+ self.menu.setMask(QRegion(0, y, w, h))
919
+
920
+
921
+ @MenuAnimationManager.register(MenuAnimationType.PULL_UP)
922
+ class PullUpMenuAnimationManager(MenuAnimationManager):
923
+ """ Pull up menu animation manager """
924
+
925
+ def _endPosition(self, pos):
926
+ m = self.menu
927
+ rect = getCurrentScreenGeometry()
928
+ w, h = m.width() + 5, m.height()
929
+ x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
930
+ y = max(pos.y() - h + 10, rect.top() + 4)
931
+ return QPoint(x, y)
932
+
933
+ def exec(self, pos):
934
+ pos = self._endPosition(pos)
935
+ h = self.menu.height() + 5
936
+
937
+ self.ani.setStartValue(pos+QPoint(0, int(h/2)))
938
+ self.ani.setEndValue(pos)
939
+ self.ani.start()
940
+
941
+ def availableViewSize(self, pos: QPoint):
942
+ ss = getCurrentScreenGeometry()
943
+ return ss.width() - 100, max(pos.y() - ss.top() - 28, 1)
944
+
945
+ def _onValueChanged(self):
946
+ w, h = self._menuSize()
947
+ y = self.ani.endValue().y() - self.ani.currentValue().y()
948
+ self.menu.setMask(QRegion(0, y, w, h - 28))
949
+
950
+
951
+ @MenuAnimationManager.register(MenuAnimationType.FADE_IN_DROP_DOWN)
952
+ class FadeInDropDownMenuAnimationManager(MenuAnimationManager):
953
+ """ Fade in drop down menu animation manager """
954
+
955
+ def __init__(self, menu: RoundMenu):
956
+ super().__init__(menu)
957
+ self.opacityAni = QPropertyAnimation(menu, b'windowOpacity', self)
958
+ self.aniGroup = QParallelAnimationGroup(self)
959
+ self.aniGroup.addAnimation(self.ani)
960
+ self.aniGroup.addAnimation(self.opacityAni)
961
+
962
+ def exec(self, pos):
963
+ pos = self._endPosition(pos)
964
+
965
+ self.opacityAni.setStartValue(0)
966
+ self.opacityAni.setEndValue(1)
967
+ self.opacityAni.setDuration(150)
968
+ self.opacityAni.setEasingCurve(QEasingCurve.OutQuad)
969
+
970
+ self.ani.setStartValue(pos-QPoint(0, 8))
971
+ self.ani.setEndValue(pos)
972
+ self.ani.setDuration(150)
973
+ self.ani.setEasingCurve(QEasingCurve.OutQuad)
974
+
975
+ self.aniGroup.start()
976
+
977
+ def availableViewSize(self, pos: QPoint):
978
+ ss = getCurrentScreenGeometry()
979
+ return ss.width() - 100, max(ss.bottom() - pos.y() - 10, 1)
980
+
981
+
982
+ @MenuAnimationManager.register(MenuAnimationType.FADE_IN_PULL_UP)
983
+ class FadeInPullUpMenuAnimationManager(MenuAnimationManager):
984
+ """ Fade in pull up menu animation manager """
985
+
986
+ def __init__(self, menu: RoundMenu):
987
+ super().__init__(menu)
988
+ self.opacityAni = QPropertyAnimation(menu, b'windowOpacity', self)
989
+ self.aniGroup = QParallelAnimationGroup(self)
990
+ self.aniGroup.addAnimation(self.ani)
991
+ self.aniGroup.addAnimation(self.opacityAni)
992
+
993
+ def _endPosition(self, pos):
994
+ m = self.menu
995
+ rect = getCurrentScreenGeometry()
996
+ w, h = m.width() + 5, m.height()
997
+ x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
998
+ y = max(pos.y() - h + 15, rect.top() + 4)
999
+ return QPoint(x, y)
1000
+
1001
+ def exec(self, pos):
1002
+ pos = self._endPosition(pos)
1003
+
1004
+ self.opacityAni.setStartValue(0)
1005
+ self.opacityAni.setEndValue(1)
1006
+ self.opacityAni.setDuration(150)
1007
+ self.opacityAni.setEasingCurve(QEasingCurve.OutQuad)
1008
+
1009
+ self.ani.setStartValue(pos+QPoint(0, 8))
1010
+ self.ani.setEndValue(pos)
1011
+ self.ani.setDuration(200)
1012
+ self.ani.setEasingCurve(QEasingCurve.OutQuad)
1013
+ self.aniGroup.start()
1014
+
1015
+ def availableViewSize(self, pos: QPoint):
1016
+ ss = getCurrentScreenGeometry()
1017
+ return ss.width() - 100, pos.y()- ss.top() - 28
1018
+
1019
+
1020
+ class EditMenu(RoundMenu):
1021
+ """ Edit menu """
1022
+
1023
+ def createActions(self):
1024
+ self.cutAct = QAction(
1025
+ FIF.CUT.icon(),
1026
+ self.tr("Cut"),
1027
+ self,
1028
+ shortcut="Ctrl+X",
1029
+ triggered=self.parent().cut,
1030
+ )
1031
+ self.copyAct = QAction(
1032
+ FIF.COPY.icon(),
1033
+ self.tr("Copy"),
1034
+ self,
1035
+ shortcut="Ctrl+C",
1036
+ triggered=self.parent().copy,
1037
+ )
1038
+ self.pasteAct = QAction(
1039
+ FIF.PASTE.icon(),
1040
+ self.tr("Paste"),
1041
+ self,
1042
+ shortcut="Ctrl+V",
1043
+ triggered=self.parent().paste,
1044
+ )
1045
+ self.cancelAct = QAction(
1046
+ FIF.CANCEL.icon(),
1047
+ self.tr("Cancel"),
1048
+ self,
1049
+ shortcut="Ctrl+Z",
1050
+ triggered=self.parent().undo,
1051
+ )
1052
+ self.selectAllAct = QAction(
1053
+ self.tr("Select all"),
1054
+ self,
1055
+ shortcut="Ctrl+A",
1056
+ triggered=self.parent().selectAll
1057
+ )
1058
+ self.action_list = [
1059
+ self.cutAct, self.copyAct,
1060
+ self.pasteAct, self.cancelAct, self.selectAllAct
1061
+ ]
1062
+
1063
+ def _parentText(self):
1064
+ raise NotImplementedError
1065
+
1066
+ def _parentSelectedText(self):
1067
+ raise NotImplementedError
1068
+
1069
+ def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
1070
+ self.clear()
1071
+ self.createActions()
1072
+
1073
+ if QApplication.clipboard().mimeData().hasText():
1074
+ if self._parentText():
1075
+ if self._parentSelectedText():
1076
+ if self.parent().isReadOnly():
1077
+ self.addActions([self.copyAct, self.selectAllAct])
1078
+ else:
1079
+ self.addActions(self.action_list)
1080
+ else:
1081
+ if self.parent().isReadOnly():
1082
+ self.addAction(self.selectAllAct)
1083
+ else:
1084
+ self.addActions(self.action_list[2:])
1085
+ elif not self.parent().isReadOnly():
1086
+ self.addAction(self.pasteAct)
1087
+ else:
1088
+ return
1089
+ else:
1090
+ if not self._parentText():
1091
+ return
1092
+
1093
+ if self._parentSelectedText():
1094
+ if self.parent().isReadOnly():
1095
+ self.addActions([self.copyAct, self.selectAllAct])
1096
+ else:
1097
+ self.addActions(
1098
+ self.action_list[:2] + self.action_list[3:])
1099
+ else:
1100
+ if self.parent().isReadOnly():
1101
+ self.addAction(self.selectAllAct)
1102
+ else:
1103
+ self.addActions(self.action_list[3:])
1104
+
1105
+ super().exec(pos, ani, aniType)
1106
+
1107
+
1108
+ class LineEditMenu(EditMenu):
1109
+ """ Line edit menu """
1110
+
1111
+ def __init__(self, parent: QLineEdit):
1112
+ super().__init__("", parent)
1113
+ self.selectionStart = parent.selectionStart()
1114
+ self.selectionLength = parent.selectionLength()
1115
+
1116
+ def _onItemClicked(self, item):
1117
+ if self.selectionStart >= 0:
1118
+ self.parent().setSelection(self.selectionStart, self.selectionLength)
1119
+
1120
+ super()._onItemClicked(item)
1121
+
1122
+ def _parentText(self):
1123
+ return self.parent().text()
1124
+
1125
+ def _parentSelectedText(self):
1126
+ return self.parent().selectedText()
1127
+
1128
+ def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
1129
+ return super().exec(pos, ani, aniType)
1130
+
1131
+
1132
+ class TextEditMenu(EditMenu):
1133
+ """ Text edit menu """
1134
+
1135
+ def __init__(self, parent: QTextEdit):
1136
+ super().__init__("", parent)
1137
+ cursor = parent.textCursor()
1138
+ self.selectionStart = cursor.selectionStart()
1139
+ self.selectionLength = cursor.selectionEnd() - self.selectionStart + 1
1140
+
1141
+ def _parentText(self):
1142
+ return self.parent().toPlainText()
1143
+
1144
+ def _parentSelectedText(self):
1145
+ return self.parent().textCursor().selectedText()
1146
+
1147
+ def _onItemClicked(self, item):
1148
+ if self.selectionStart >= 0:
1149
+ cursor = self.parent().textCursor()
1150
+ cursor.setPosition(self.selectionStart)
1151
+ cursor.movePosition(
1152
+ QTextCursor.Right, QTextCursor.KeepAnchor, self.selectionLength)
1153
+
1154
+ super()._onItemClicked(item)
1155
+
1156
+ def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
1157
+ return super().exec(pos, ani, aniType)
1158
+
1159
+
1160
+ class IndicatorMenuItemDelegate(MenuItemDelegate):
1161
+ """ Menu item delegate with indicator """
1162
+
1163
+ def paint(self, painter: QPainter, option, index):
1164
+ super().paint(painter, option, index)
1165
+ if not option.state & QStyle.State_Selected:
1166
+ return
1167
+
1168
+ painter.save()
1169
+ painter.setRenderHints(
1170
+ QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing)
1171
+
1172
+ painter.setPen(Qt.NoPen)
1173
+ painter.setBrush(themeColor())
1174
+ painter.drawRoundedRect(6, 11+option.rect.y(), 3, 15, 1.5, 1.5)
1175
+
1176
+ painter.restore()
1177
+
1178
+
1179
+ class CheckableMenuItemDelegate(ShortcutMenuItemDelegate):
1180
+ """ Checkable menu item delegate """
1181
+
1182
+ def _drawIndicator(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
1183
+ raise NotImplementedError
1184
+
1185
+ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
1186
+ super().paint(painter, option, index)
1187
+
1188
+ # draw indicator
1189
+ action = index.data(Qt.UserRole) # type: QAction
1190
+ if not (isinstance(action, QAction) and action.isChecked()):
1191
+ return
1192
+
1193
+ painter.save()
1194
+ self._drawIndicator(painter, option, index)
1195
+ painter.restore()
1196
+
1197
+
1198
+ class RadioIndicatorMenuItemDelegate(CheckableMenuItemDelegate):
1199
+ """ Checkable menu item delegate with radio indicator """
1200
+
1201
+ def _drawIndicator(self, painter, option, index):
1202
+ rect = option.rect
1203
+ r = 5
1204
+ x = rect.x() + 22
1205
+ y = rect.center().y() - r / 2
1206
+
1207
+ painter.setRenderHints(QPainter.Antialiasing)
1208
+ if not option.state & QStyle.State_MouseOver:
1209
+ painter.setOpacity(0.75 if isDarkTheme() else 0.65)
1210
+
1211
+ painter.setPen(Qt.NoPen)
1212
+ painter.setBrush(Qt.white if isDarkTheme() else Qt.black)
1213
+ painter.drawEllipse(QRectF(x, y, r, r))
1214
+
1215
+
1216
+ class CheckIndicatorMenuItemDelegate(CheckableMenuItemDelegate):
1217
+ """ Checkable menu item delegate with check indicator """
1218
+
1219
+ def _drawIndicator(self, painter, option, index):
1220
+ rect = option.rect
1221
+ s = 11
1222
+ x = rect.x() + 19
1223
+ y = rect.center().y() - s / 2
1224
+
1225
+ painter.setRenderHints(QPainter.Antialiasing)
1226
+ if not option.state & QStyle.State_MouseOver:
1227
+ painter.setOpacity(0.75)
1228
+
1229
+ FIF.ACCEPT.render(painter, QRectF(x, y, s, s))
1230
+
1231
+
1232
+ class MenuIndicatorType(Enum):
1233
+ """ Menu indicator type """
1234
+ CHECK = 0
1235
+ RADIO = 1
1236
+
1237
+
1238
+ def createCheckableMenuItemDelegate(style: MenuIndicatorType):
1239
+ """ create checkable menu item delegate """
1240
+ if style == MenuIndicatorType.RADIO:
1241
+ return RadioIndicatorMenuItemDelegate()
1242
+ if style == MenuIndicatorType.CHECK:
1243
+ return CheckIndicatorMenuItemDelegate()
1244
+
1245
+ raise ValueError(f'`{style}` is not a valid menu indicator type.')
1246
+
1247
+
1248
+ class CheckableMenu(RoundMenu):
1249
+ """ Checkable menu """
1250
+
1251
+ def __init__(self, title="", parent=None, indicatorType=MenuIndicatorType.CHECK):
1252
+ super().__init__(title, parent)
1253
+ self.view.setItemDelegate(createCheckableMenuItemDelegate(indicatorType))
1254
+ self.view.setObjectName('checkableListWidget')
1255
+
1256
+ def _adjustItemText(self, item: QListWidgetItem, action: QAction):
1257
+ w = super()._adjustItemText(item, action)
1258
+ item.setSizeHint(QSize(w + 26, self.itemHeight))
1259
+
1260
+ def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
1261
+ return super().exec(pos, ani, aniType)
1262
+
1263
+
1264
+ class SystemTrayMenu(RoundMenu):
1265
+ """ System tray menu """
1266
+
1267
+ def sizeHint(self) -> QSize:
1268
+ m = self.layout().contentsMargins()
1269
+ s = self.layout().sizeHint()
1270
+ return QSize(s.width() - m.right() + 5, s.height() - m.bottom())
1271
+
1272
+
1273
+ class CheckableSystemTrayMenu(CheckableMenu):
1274
+ """ Checkable system tray menu """
1275
+
1276
+ def sizeHint(self) -> QSize:
1277
+ m = self.layout().contentsMargins()
1278
+ s = self.layout().sizeHint()
1279
+ return QSize(s.width() - m.right() + 5, s.height() - m.bottom())
1280
+
1281
+
1282
+ class LabelContextMenu(RoundMenu):
1283
+ """ Label context menu """
1284
+
1285
+ def __init__(self, parent: QLabel):
1286
+ super().__init__("", parent)
1287
+ self.selectedText = parent.selectedText()
1288
+
1289
+ self.copyAct = QAction(
1290
+ FIF.COPY.icon(),
1291
+ self.tr("Copy"),
1292
+ self,
1293
+ shortcut="Ctrl+C",
1294
+ triggered=self._onCopy
1295
+ )
1296
+ self.selectAllAct = QAction(
1297
+ self.tr("Select all"),
1298
+ self,
1299
+ shortcut="Ctrl+A",
1300
+ triggered=self._onSelectAll
1301
+ )
1302
+
1303
+ def _onCopy(self):
1304
+ QApplication.clipboard().setText(self.selectedText)
1305
+
1306
+ def _onSelectAll(self):
1307
+ self.label().setSelection(0, len(self.label().text()))
1308
+
1309
+ def label(self) -> QLabel:
1310
+ return self.parent()
1311
+
1312
+ def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
1313
+ if self.label().hasSelectedText():
1314
+ self.addActions([self.copyAct, self.selectAllAct])
1315
+ else:
1316
+ self.addAction(self.selectAllAct)
1317
+
1318
+ return super().exec(pos, ani, aniType)