You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
850 lines
26 KiB
850 lines
26 KiB
# coding:utf-8
|
|
from enum import Enum
|
|
from typing import List, Union
|
|
|
|
from qframelesswindow import WindowEffect
|
|
from PySide6.QtCore import (QEasingCurve, QEvent, QPropertyAnimation, QObject,
|
|
Qt, QSize, QRectF, Signal, QPoint, QTimer, QObject)
|
|
from PySide6.QtGui import QAction, QIcon, QColor, QPainter, QPen, QPixmap, QRegion, QCursor, QTextCursor, QHoverEvent
|
|
from PySide6.QtWidgets import (QApplication, QMenu, QProxyStyle, QStyle,
|
|
QGraphicsDropShadowEffect, QListWidget, QWidget, QHBoxLayout,
|
|
QListWidgetItem, QLineEdit, QTextEdit, QStyledItemDelegate, QStyleOptionViewItem)
|
|
|
|
from ...common.smooth_scroll import SmoothScroll
|
|
from ...common.icon import FluentIcon as FIF
|
|
from ...common.icon import MenuIconEngine, Action, FluentIconBase, Icon
|
|
from ...common.style_sheet import FluentStyleSheet
|
|
from ...common.config import isDarkTheme
|
|
|
|
|
|
class CustomMenuStyle(QProxyStyle):
|
|
""" Custom menu style """
|
|
|
|
def __init__(self, iconSize=14):
|
|
"""
|
|
Parameters
|
|
----------
|
|
iconSizeL int
|
|
the size of icon
|
|
"""
|
|
super().__init__()
|
|
self.iconSize = iconSize
|
|
|
|
def pixelMetric(self, metric, option, widget):
|
|
if metric == QStyle.PM_SmallIconSize:
|
|
return self.iconSize
|
|
|
|
return super().pixelMetric(metric, option, widget)
|
|
|
|
|
|
class DWMMenu(QMenu):
|
|
""" A menu with DWM shadow """
|
|
|
|
def __init__(self, title="", parent=None):
|
|
super().__init__(title, parent)
|
|
self.windowEffect = WindowEffect(self)
|
|
self.setWindowFlags(
|
|
Qt.FramelessWindowHint | Qt.Popup | Qt.NoDropShadowWindowHint)
|
|
self.setAttribute(Qt.WA_StyledBackground)
|
|
self.setStyle(CustomMenuStyle())
|
|
FluentStyleSheet.MENU.apply(self)
|
|
|
|
def event(self, e: QEvent):
|
|
if e.type() == QEvent.WinIdChange:
|
|
self.windowEffect.addMenuShadowEffect(self.winId())
|
|
return QMenu.event(self, e)
|
|
|
|
|
|
|
|
class SubMenuItemWidget(QWidget):
|
|
""" Sub menu item """
|
|
|
|
showMenuSig = Signal(QListWidgetItem)
|
|
|
|
def __init__(self, menu, item, parent=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
menu: QMenu | RoundMenu
|
|
sub menu
|
|
|
|
item: QListWidgetItem
|
|
menu item
|
|
|
|
parent: QWidget
|
|
parent widget
|
|
"""
|
|
super().__init__(parent)
|
|
self.menu = menu
|
|
self.item = item
|
|
|
|
def enterEvent(self, e):
|
|
super().enterEvent(e)
|
|
self.showMenuSig.emit(self.item)
|
|
|
|
def paintEvent(self, e):
|
|
painter = QPainter(self)
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
|
|
|
# draw right arrow
|
|
FIF.CHEVRON_RIGHT.render(painter, QRectF(
|
|
self.width()-10, self.height()/2-9/2, 9, 9))
|
|
|
|
|
|
class MenuItemDelegate(QStyledItemDelegate):
|
|
""" Menu item delegate """
|
|
|
|
def paint(self, painter, option, index):
|
|
if index.model().data(index, Qt.DecorationRole) != "seperator":
|
|
return super().paint(painter, option, index)
|
|
|
|
# draw seperator
|
|
painter.save()
|
|
|
|
c = 0 if not isDarkTheme() else 255
|
|
pen = QPen(QColor(c, c, c, 25), 1)
|
|
pen.setCosmetic(True)
|
|
painter.setPen(pen)
|
|
rect = option.rect
|
|
painter.drawLine(0, rect.y() + 4, rect.width() + 12, rect.y() + 4)
|
|
|
|
painter.restore()
|
|
|
|
|
|
class MenuActionListWidget(QListWidget):
|
|
""" Menu action list widget """
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setViewportMargins(0, 6, 0, 6)
|
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
self.setTextElideMode(Qt.ElideNone)
|
|
self.setDragEnabled(False)
|
|
self.setMouseTracking(True)
|
|
self.setVerticalScrollMode(self.ScrollMode.ScrollPerPixel)
|
|
self.setIconSize(QSize(14, 14))
|
|
self.setItemDelegate(MenuItemDelegate(self))
|
|
|
|
self.smoothScroll = SmoothScroll(self)
|
|
self.setStyleSheet(
|
|
'MenuActionListWidget{font: 14px "Segoe UI", "Microsoft YaHei"}')
|
|
|
|
def wheelEvent(self, e):
|
|
self.smoothScroll.wheelEvent(e)
|
|
e.setAccepted(True)
|
|
|
|
def insertItem(self, row, item):
|
|
""" inserts menu item at the position in the list given by row """
|
|
super().insertItem(row, item)
|
|
self.adjustSize()
|
|
|
|
def addItem(self, item):
|
|
""" add menu item at the end """
|
|
super().addItem(item)
|
|
self.adjustSize()
|
|
|
|
def takeItem(self, row):
|
|
""" delete item from list """
|
|
item = super().takeItem(row)
|
|
self.adjustSize()
|
|
return item
|
|
|
|
def adjustSize(self):
|
|
size = QSize()
|
|
for i in range(self.count()):
|
|
s = self.item(i).sizeHint()
|
|
size.setWidth(max(s.width(), size.width()))
|
|
size.setHeight(size.height() + s.height())
|
|
|
|
# adjust the height of viewport
|
|
ss = QApplication.screenAt(QCursor.pos()).availableSize()
|
|
w, h = ss.width() - 100, ss.height() - 100
|
|
vsize = QSize(size)
|
|
vsize.setHeight(min(h-12, vsize.height()))
|
|
vsize.setWidth(min(w-12, vsize.width()))
|
|
self.viewport().adjustSize()
|
|
|
|
# adjust the height of list widget
|
|
m = self.viewportMargins()
|
|
size += QSize(m.left()+m.right()+2, m.top()+m.bottom())
|
|
size.setHeight(min(h, size.height()+3))
|
|
size.setWidth(min(w, size.width()))
|
|
self.setFixedSize(size)
|
|
|
|
def setItemHeight(self, height):
|
|
""" set the height of item """
|
|
for i in range(self.count()):
|
|
item = self.item(i)
|
|
item.setSizeHint(item.sizeHint().width(), height)
|
|
|
|
self.adjustSize()
|
|
|
|
|
|
class MenuAnimationType(Enum):
|
|
""" Menu animation type """
|
|
|
|
NONE = 0
|
|
DROP_DOWN = 1
|
|
PULL_UP = 2
|
|
|
|
|
|
class RoundMenu(QWidget):
|
|
""" Round corner menu """
|
|
|
|
closedSignal = Signal()
|
|
|
|
def __init__(self, title="", parent=None):
|
|
super().__init__(parent=parent)
|
|
self._title = title
|
|
self._icon = QIcon()
|
|
self._actions = [] # type: List[QAction]
|
|
self._subMenus = []
|
|
|
|
self.isSubMenu = False
|
|
self.parentMenu = None
|
|
self.menuItem = None
|
|
self.lastHoverItem = None
|
|
self.lastHoverSubMenuItem = None
|
|
self.isHideBySystem = True
|
|
self.itemHeight = 28
|
|
|
|
self.hBoxLayout = QHBoxLayout(self)
|
|
self.view = MenuActionListWidget(self)
|
|
|
|
self.aniManager = None
|
|
self.ani = QPropertyAnimation(self, b'pos', self)
|
|
self.timer = QTimer(self)
|
|
|
|
self.__initWidgets()
|
|
|
|
def __initWidgets(self):
|
|
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint |
|
|
Qt.NoDropShadowWindowHint)
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
|
self.setMouseTracking(True)
|
|
|
|
self.timer.setSingleShot(True)
|
|
self.timer.setInterval(400)
|
|
self.timer.timeout.connect(self._onShowMenuTimeOut)
|
|
|
|
self.setShadowEffect()
|
|
self.hBoxLayout.addWidget(self.view, 1, Qt.AlignCenter)
|
|
|
|
self.hBoxLayout.setContentsMargins(12, 8, 12, 20)
|
|
FluentStyleSheet.MENU.apply(self)
|
|
|
|
self.view.itemClicked.connect(self._onItemClicked)
|
|
self.view.itemEntered.connect(self._onItemEntered)
|
|
|
|
def setItemHeight(self, height):
|
|
""" set the height of menu item """
|
|
if height == self.itemHeight:
|
|
return
|
|
|
|
self.itemHeight = height
|
|
self.view.setItemHeight(height)
|
|
|
|
def setShadowEffect(self, blurRadius=30, offset=(0, 8), color=QColor(0, 0, 0, 30)):
|
|
""" add shadow to dialog """
|
|
self.shadowEffect = QGraphicsDropShadowEffect(self.view)
|
|
self.shadowEffect.setBlurRadius(blurRadius)
|
|
self.shadowEffect.setOffset(*offset)
|
|
self.shadowEffect.setColor(color)
|
|
self.view.setGraphicsEffect(None)
|
|
self.view.setGraphicsEffect(self.shadowEffect)
|
|
|
|
def _setParentMenu(self, parent, item):
|
|
self.parentMenu = parent
|
|
self.menuItem = item
|
|
self.isSubMenu = True if parent else False
|
|
|
|
def adjustSize(self):
|
|
m = self.layout().contentsMargins()
|
|
w = self.view.width() + m.left() + m.right()
|
|
h = self.view.height() + m.top() + m.bottom()
|
|
self.setFixedSize(w, h)
|
|
|
|
def icon(self):
|
|
return self._icon
|
|
|
|
def title(self):
|
|
return self._title
|
|
|
|
def clear(self):
|
|
""" clear all actions """
|
|
for i in range(len(self._actions)-1, -1, -1):
|
|
self.removeAction(self._actions[i])
|
|
|
|
def setIcon(self, icon: Union[QIcon, FluentIconBase]):
|
|
""" set the icon of menu """
|
|
if isinstance(icon, FluentIconBase):
|
|
icon = Icon(icon)
|
|
|
|
self._icon = icon
|
|
|
|
def addAction(self, action: Union[QAction, Action]):
|
|
""" add action to menu
|
|
|
|
Parameters
|
|
----------
|
|
action: QAction
|
|
menu action
|
|
"""
|
|
item = self._createActionItem(action)
|
|
self.view.addItem(item)
|
|
self.adjustSize()
|
|
|
|
def _createActionItem(self, action, before=None):
|
|
""" create menu action item """
|
|
if not before:
|
|
self._actions.append(action)
|
|
elif before in self._actions:
|
|
index = self._actions.index(before)
|
|
self._actions.insert(index, action)
|
|
else:
|
|
raise ValueError('`before` is not in the action list')
|
|
|
|
item = QListWidgetItem(self._createItemIcon(action), action.text())
|
|
if not self._hasItemIcon():
|
|
w = 40 + self.view.fontMetrics().boundingRect(action.text()).width()
|
|
else:
|
|
# add a blank character to increase space between icon and text
|
|
item.setText(" " + item.text())
|
|
w = 60 + self.view.fontMetrics().boundingRect(item.text()).width()
|
|
|
|
item.setSizeHint(QSize(w, self.itemHeight))
|
|
item.setData(Qt.UserRole, action)
|
|
action.setProperty('item', item)
|
|
action.changed.connect(self._onActionChanged)
|
|
return item
|
|
|
|
def _hasItemIcon(self):
|
|
return any(not i.icon().isNull() for i in self._actions+self._subMenus)
|
|
|
|
def _createItemIcon(self, w):
|
|
""" create the icon of menu item """
|
|
hasIcon = self._hasItemIcon()
|
|
icon = QIcon(MenuIconEngine(w.icon()))
|
|
|
|
if hasIcon and w.icon().isNull():
|
|
pixmap = QPixmap(self.view.iconSize())
|
|
pixmap.fill(Qt.transparent)
|
|
icon = QIcon(pixmap)
|
|
elif not hasIcon:
|
|
icon = QIcon()
|
|
|
|
return icon
|
|
|
|
def insertAction(self, before: Union[QAction, Action], action: Union[QAction, Action]):
|
|
""" inserts action to menu, before the action before """
|
|
if before not in self._actions:
|
|
return
|
|
|
|
beforeItem = before.property('item')
|
|
if not beforeItem:
|
|
return
|
|
|
|
index = self.view.row(beforeItem)
|
|
item = self._createActionItem(action, before)
|
|
self.view.insertItem(index, item)
|
|
self.adjustSize()
|
|
|
|
def addActions(self, actions: List[Union[QAction, Action]]):
|
|
""" add actions to menu
|
|
|
|
Parameters
|
|
----------
|
|
actions: Iterable[QAction]
|
|
menu actions
|
|
"""
|
|
for action in actions:
|
|
self.addAction(action)
|
|
|
|
def insertActions(self, before: Union[QAction, Action], actions: List[Union[QAction, Action]]):
|
|
""" inserts the actions actions to menu, before the action before """
|
|
for action in actions:
|
|
self.insertAction(before, action)
|
|
|
|
def removeAction(self, action: Union[QAction, Action]):
|
|
""" remove action from menu """
|
|
if action not in self._actions:
|
|
return
|
|
|
|
index = self._actions.index(action)
|
|
self._actions.remove(action)
|
|
action.setProperty('item', None)
|
|
item = self.view.takeItem(index)
|
|
item.setData(Qt.UserRole, None)
|
|
|
|
# delete widget
|
|
widget = self.view.itemWidget(item)
|
|
if widget:
|
|
widget.deleteLater()
|
|
|
|
def setDefaultAction(self, action: Union[QAction, Action]):
|
|
""" set the default action """
|
|
if action not in self._actions:
|
|
return
|
|
|
|
index = self._actions.index(action)
|
|
self.view.setCurrentRow(index)
|
|
|
|
def addMenu(self, menu):
|
|
""" add sub menu
|
|
|
|
Parameters
|
|
----------
|
|
menu: RoundMenu
|
|
sub round menu
|
|
"""
|
|
if not isinstance(menu, RoundMenu):
|
|
raise ValueError('`menu` should be an instance of `RoundMenu`.')
|
|
|
|
item, w = self._createSubMenuItem(menu)
|
|
self.view.addItem(item)
|
|
self.view.setItemWidget(item, w)
|
|
self.adjustSize()
|
|
|
|
def insertMenu(self, before: Union[QAction, Action], menu):
|
|
""" insert menu before action `before` """
|
|
if not isinstance(menu, RoundMenu):
|
|
raise ValueError('`menu` should be an instance of `RoundMenu`.')
|
|
|
|
if before not in self._actions:
|
|
raise ValueError('`before` should be in menu action list')
|
|
|
|
item, w = self._createSubMenuItem(menu)
|
|
self.view.insertItem(self.view.row(before.property('item')), item)
|
|
self.view.setItemWidget(item, w)
|
|
self.adjustSize()
|
|
|
|
def _createSubMenuItem(self, menu):
|
|
self._subMenus.append(menu)
|
|
|
|
item = QListWidgetItem(self._createItemIcon(menu), menu.title())
|
|
if not self._hasItemIcon():
|
|
w = 60 + self.view.fontMetrics().boundingRect(menu.title()).width()
|
|
else:
|
|
# add a blank character to increase space between icon and text
|
|
item.setText(" " + item.text())
|
|
w = 72 + self.view.fontMetrics().boundingRect(item.text()).width()
|
|
|
|
# add submenu item
|
|
menu._setParentMenu(self, item)
|
|
item.setSizeHint(QSize(w, self.itemHeight))
|
|
item.setData(Qt.UserRole, menu)
|
|
w = SubMenuItemWidget(menu, item, self)
|
|
w.showMenuSig.connect(self._showSubMenu)
|
|
w.resize(item.sizeHint())
|
|
|
|
return item, w
|
|
|
|
def _showSubMenu(self, item):
|
|
""" show sub menu """
|
|
self.lastHoverItem = item
|
|
self.lastHoverSubMenuItem = item
|
|
# delay 400 ms to anti-shake
|
|
self.timer.stop()
|
|
self.timer.start()
|
|
|
|
def _onShowMenuTimeOut(self):
|
|
if self.lastHoverSubMenuItem is None or not self.lastHoverItem is self.lastHoverSubMenuItem:
|
|
return
|
|
|
|
w = self.view.itemWidget(self.lastHoverSubMenuItem)
|
|
|
|
if w.menu.parentMenu.isHidden():
|
|
return
|
|
|
|
pos = w.mapToGlobal(QPoint(w.width()+5, -5))
|
|
w.menu.exec(pos)
|
|
|
|
def addSeparator(self):
|
|
""" add seperator to menu """
|
|
m = self.view.viewportMargins()
|
|
w = self.view.width()-m.left()-m.right()
|
|
|
|
# add separator to list widget
|
|
item = QListWidgetItem(self.view)
|
|
item.setFlags(Qt.NoItemFlags)
|
|
item.setSizeHint(QSize(w, 9))
|
|
self.view.addItem(item)
|
|
item.setData(Qt.DecorationRole, "seperator")
|
|
self.adjustSize()
|
|
|
|
def _onItemClicked(self, item):
|
|
action = item.data(Qt.UserRole)
|
|
if action not in self._actions or not action.isEnabled():
|
|
return
|
|
|
|
self._hideMenu(False)
|
|
|
|
if not self.isSubMenu:
|
|
action.trigger()
|
|
return
|
|
|
|
# close parent menu
|
|
self._closeParentMenu()
|
|
action.trigger()
|
|
|
|
def _closeParentMenu(self):
|
|
menu = self
|
|
while menu:
|
|
menu.close()
|
|
menu = menu.parentMenu
|
|
|
|
def _onItemEntered(self, item):
|
|
self.lastHoverItem = item
|
|
if not isinstance(item.data(Qt.UserRole), RoundMenu):
|
|
return
|
|
|
|
self._showSubMenu(item)
|
|
|
|
def _hideMenu(self, isHideBySystem=False):
|
|
self.isHideBySystem = isHideBySystem
|
|
self.view.clearSelection()
|
|
if self.isSubMenu:
|
|
self.hide()
|
|
else:
|
|
self.close()
|
|
|
|
def hideEvent(self, e):
|
|
if self.isHideBySystem and self.isSubMenu:
|
|
self._closeParentMenu()
|
|
|
|
self.isHideBySystem = True
|
|
e.accept()
|
|
|
|
def closeEvent(self, e):
|
|
e.accept()
|
|
self.closedSignal.emit()
|
|
self.view.clearSelection()
|
|
|
|
def menuActions(self):
|
|
return self._actions
|
|
|
|
def mousePressEvent(self, e):
|
|
if self.childAt(e.pos()) is not self.view:
|
|
self._hideMenu(True)
|
|
|
|
def mouseMoveEvent(self, e):
|
|
if not self.isSubMenu:
|
|
return
|
|
|
|
# hide submenu when mouse moves out of submenu item
|
|
pos = e.globalPos()
|
|
view = self.parentMenu.view
|
|
|
|
# get the rect of menu item
|
|
margin = view.viewportMargins()
|
|
rect = view.visualItemRect(self.menuItem).translated(view.mapToGlobal(QPoint()))
|
|
rect = rect.translated(margin.left(), margin.top()+2)
|
|
if self.parentMenu.geometry().contains(pos) and not rect.contains(pos) and \
|
|
not self.geometry().contains(pos):
|
|
view.clearSelection()
|
|
self._hideMenu(False)
|
|
|
|
def _onActionChanged(self):
|
|
""" action changed slot """
|
|
action = self.sender() # type: QAction
|
|
item = action.property('item') # type: QListWidgetItem
|
|
item.setIcon(self._createItemIcon(action))
|
|
|
|
if not self._hasItemIcon():
|
|
item.setText(action.text())
|
|
w = 28 + self.view.fontMetrics().width(action.text())
|
|
else:
|
|
# add a blank character to increase space between icon and text
|
|
item.setText(" " + action.text())
|
|
w = 60 + self.view.fontMetrics().width(item.text())
|
|
|
|
item.setSizeHint(QSize(w, self.itemHeight))
|
|
|
|
if action.isEnabled():
|
|
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
else:
|
|
item.setFlags(Qt.NoItemFlags)
|
|
|
|
self.view.adjustSize()
|
|
self.adjustSize()
|
|
|
|
def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
|
|
""" show menu
|
|
|
|
Parameters
|
|
----------
|
|
pos: QPoint
|
|
pop-up position
|
|
|
|
ani: bool
|
|
Whether to show pop-up animation
|
|
|
|
aniType: MenuAnimationType
|
|
menu animation type
|
|
"""
|
|
if self.isVisible():
|
|
return
|
|
|
|
self.aniManager = MenuAnimationManager.make(self, aniType)
|
|
self.aniManager.exec(pos)
|
|
|
|
self.show()
|
|
|
|
if self.isSubMenu:
|
|
self.menuItem.setSelected(True)
|
|
|
|
def exec_(self, pos: QPoint, ani=True, aniType=MenuAnimationType.DROP_DOWN):
|
|
""" show menu
|
|
|
|
Parameters
|
|
----------
|
|
pos: QPoint
|
|
pop-up position
|
|
|
|
ani: bool
|
|
Whether to show pop-up animation
|
|
|
|
aniType: MenuAnimationType
|
|
menu animation type
|
|
"""
|
|
self.exec(pos, ani, aniType)
|
|
|
|
|
|
class MenuAnimationManager(QObject):
|
|
""" Menu animation manager """
|
|
|
|
managers = {}
|
|
|
|
def __init__(self, menu: RoundMenu):
|
|
super().__init__()
|
|
self.menu = menu
|
|
self.ani = QPropertyAnimation(menu, b'pos', menu)
|
|
|
|
self.ani.setDuration(250)
|
|
self.ani.setEasingCurve(QEasingCurve.OutQuad)
|
|
self.ani.valueChanged.connect(self._onValueChanged)
|
|
self.ani.valueChanged.connect(self._updateMenuViewport)
|
|
|
|
def _onValueChanged(self):
|
|
raise NotImplementedError
|
|
|
|
def _updateMenuViewport(self):
|
|
self.menu.view.viewport().update()
|
|
self.menu.view.setAttribute(Qt.WA_UnderMouse, True)
|
|
e = QHoverEvent(QEvent.HoverEnter, QPoint(), QPoint(1, 1))
|
|
QApplication.sendEvent(self.menu.view, e)
|
|
|
|
def _endPosition(self, pos):
|
|
m = self.menu
|
|
rect = QApplication.screenAt(QCursor.pos()).availableGeometry()
|
|
w, h = m.width() + 5, m.height() + 5
|
|
x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
|
|
y = min(pos.y() - 4, rect.bottom() - h)
|
|
return QPoint(x, y)
|
|
|
|
def _menuSize(self):
|
|
m = self.menu.layout().contentsMargins()
|
|
w = self.menu.view.width() + m.left() + m.right() + 120
|
|
h = self.menu.view.height() + m.top() + m.bottom() + 20
|
|
return w, h
|
|
|
|
def exec(self, pos: QPoint):
|
|
self._initAni(pos)
|
|
|
|
@classmethod
|
|
def register(cls, name):
|
|
""" register menu animation manager
|
|
|
|
Parameters
|
|
----------
|
|
name: Any
|
|
the name of manager, it should be unique
|
|
"""
|
|
def wrapper(Manager):
|
|
if name not in cls.managers:
|
|
cls.managers[name] = Manager
|
|
|
|
return Manager
|
|
|
|
return wrapper
|
|
|
|
@classmethod
|
|
def make(cls, menu: RoundMenu, aniType: MenuAnimationType):
|
|
if aniType not in cls.managers:
|
|
raise ValueError(f'`{aniType}` is an invalid menu animation type.')
|
|
|
|
return cls.managers[aniType](menu)
|
|
|
|
|
|
@MenuAnimationManager.register(MenuAnimationType.NONE)
|
|
class DummyMenuAnimationManager(MenuAnimationManager):
|
|
""" Dummy menu animation manager """
|
|
|
|
def exec(self, pos: QPoint):
|
|
self.menu.move(self._endPosition(pos))
|
|
|
|
|
|
@MenuAnimationManager.register(MenuAnimationType.DROP_DOWN)
|
|
class DropDownMenuAnimationManager(MenuAnimationManager):
|
|
""" Drop down menu animation manager """
|
|
|
|
def exec(self, pos):
|
|
pos = self._endPosition(pos)
|
|
h = self.menu.height() + 5
|
|
|
|
self.ani.setStartValue(pos-QPoint(0, int(h/2)))
|
|
self.ani.setEndValue(pos)
|
|
self.ani.start()
|
|
|
|
def _onValueChanged(self):
|
|
w, h = self._menuSize()
|
|
y = self.ani.endValue().y() - self.ani.currentValue().y()
|
|
self.menu.setMask(QRegion(0, y, w, h))
|
|
|
|
|
|
@MenuAnimationManager.register(MenuAnimationType.PULL_UP)
|
|
class PullUpMenuAnimationManager(MenuAnimationManager):
|
|
""" Pull up menu animation manager """
|
|
|
|
def _endPosition(self, pos):
|
|
m = self.menu
|
|
rect = QApplication.screenAt(QCursor.pos()).availableGeometry()
|
|
w, h = m.width() + 5, m.height()
|
|
x = min(pos.x() - m.layout().contentsMargins().left(), rect.right() - w)
|
|
y = max(pos.y() - h + 15, 4)
|
|
return QPoint(x, y)
|
|
|
|
def exec(self, pos):
|
|
pos = self._endPosition(pos)
|
|
h = self.menu.height() + 5
|
|
|
|
self.ani.setStartValue(pos+QPoint(0, int(h/2)))
|
|
self.ani.setEndValue(pos)
|
|
self.ani.start()
|
|
|
|
def _onValueChanged(self):
|
|
w, h = self._menuSize()
|
|
y = self.ani.endValue().y() - self.ani.currentValue().y()
|
|
self.menu.setMask(QRegion(0, y, w, h))
|
|
|
|
|
|
class EditMenu(RoundMenu):
|
|
""" Edit menu """
|
|
|
|
def createActions(self):
|
|
self.cutAct = QAction(
|
|
FIF.CUT.icon(),
|
|
self.tr("Cut"),
|
|
self,
|
|
shortcut="Ctrl+X",
|
|
triggered=self.parent().cut,
|
|
)
|
|
self.copyAct = QAction(
|
|
FIF.COPY.icon(),
|
|
self.tr("Copy"),
|
|
self,
|
|
shortcut="Ctrl+C",
|
|
triggered=self.parent().copy,
|
|
)
|
|
self.pasteAct = QAction(
|
|
FIF.PASTE.icon(),
|
|
self.tr("Paste"),
|
|
self,
|
|
shortcut="Ctrl+V",
|
|
triggered=self.parent().paste,
|
|
)
|
|
self.cancelAct = QAction(
|
|
FIF.CANCEL.icon(),
|
|
self.tr("Cancel"),
|
|
self,
|
|
shortcut="Ctrl+Z",
|
|
triggered=self.parent().undo,
|
|
)
|
|
self.selectAllAct = QAction(
|
|
self.tr("Select all"),
|
|
self,
|
|
shortcut="Ctrl+A",
|
|
triggered=self.parent().selectAll
|
|
)
|
|
self.action_list = [
|
|
self.cutAct, self.copyAct,
|
|
self.pasteAct, self.cancelAct, self.selectAllAct
|
|
]
|
|
|
|
def _parentText(self):
|
|
raise NotImplementedError
|
|
|
|
def _parentSelectedText(self):
|
|
raise NotImplementedError
|
|
|
|
def exec(self, pos, ani=True, aniType=MenuAnimationType.DROP_DOWN):
|
|
self.clear()
|
|
self.createActions()
|
|
|
|
if QApplication.clipboard().mimeData().hasText():
|
|
if self._parentText():
|
|
if self._parentSelectedText():
|
|
self.addActions(self.action_list)
|
|
else:
|
|
self.addActions(self.action_list[2:])
|
|
else:
|
|
self.addAction(self.pasteAct)
|
|
else:
|
|
if self._parentText():
|
|
if self._parentSelectedText():
|
|
self.addActions(
|
|
self.action_list[:2] + self.action_list[3:])
|
|
else:
|
|
self.addActions(self.action_list[3:])
|
|
else:
|
|
return
|
|
|
|
super().exec(pos, ani, aniType)
|
|
|
|
|
|
class LineEditMenu(EditMenu):
|
|
""" Line edit menu """
|
|
|
|
def __init__(self, parent: QLineEdit):
|
|
super().__init__("", parent)
|
|
self.selectionStart = parent.selectionStart()
|
|
self.selectionLength = parent.selectionLength()
|
|
|
|
def _onItemClicked(self, item):
|
|
if self.selectionStart >= 0:
|
|
self.parent().setSelection(self.selectionStart, self.selectionLength)
|
|
|
|
super()._onItemClicked(item)
|
|
|
|
def _parentText(self):
|
|
return self.parent().text()
|
|
|
|
def _parentSelectedText(self):
|
|
return self.parent().selectedText()
|
|
|
|
|
|
class TextEditMenu(EditMenu):
|
|
""" Text edit menu """
|
|
|
|
def __init__(self, parent: QTextEdit):
|
|
super().__init__("", parent)
|
|
cursor = parent.textCursor()
|
|
self.selectionStart = cursor.selectionStart()
|
|
self.selectionLength = cursor.selectionEnd() - self.selectionStart + 1
|
|
|
|
def _parentText(self):
|
|
return self.parent().toPlainText()
|
|
|
|
def _parentSelectedText(self):
|
|
return self.parent().textCursor().selectedText()
|
|
|
|
def _onItemClicked(self, item):
|
|
if self.selectionStart >= 0:
|
|
cursor = self.parent().textCursor()
|
|
cursor.setPosition(self.selectionStart)
|
|
cursor.movePosition(
|
|
QTextCursor.Right, QTextCursor.KeepAnchor, self.selectionLength)
|
|
|
|
super()._onItemClicked(item)
|