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,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)
|