| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- import os
- import logging
- from PySide6.QtCore import QRect, QSize, Qt, QRectF, QPoint, QEvent, Property
- from PySide6.QtWidgets import QMenu, QStyle, QStyleOptionMenuItem, QStyleOption, QWidget, QApplication
- from PySide6.QtWidgets import QToolTip
- from PySide6.QtGui import QIcon, QImage, QPainter, QFont, QPalette, QColor, QKeySequence, QAction, QActionGroup
- import Kikka
- from Kikka.KikkaConst import *
- from Kikka.Utils.Singleton import singleton
- class KMenu(QMenu):
-
- def __init__(self, title='', parent=None):
- QMenu.__init__(self, title, parent)
- self._parent = parent
- self._aRect = {}
- self._bg_image = None
- self._fg_image = None
- self._side_image = None
- self._hover_color = None
- self.installEventFilter(self)
- self.setMouseTracking(True)
- self.setSeparatorsCollapsible(False)
-
- def getHoverColor(self):
- return self._hover_color
- def setHoverColor(self, color):
- if isinstance(color, QColor):
- self._hover_color = color
-
- hoverColor = Property(QColor, fget=getHoverColor, fset=setHoverColor)
- def addMenuItem(self, text, callbackfunc=None, iconfilepath=None, group=None):
- if iconfilepath is None:
- act = QAction(text, self._parent)
- elif os.path.exists(iconfilepath):
- act = QAction(QIcon(iconfilepath), text, self._parent)
- else:
- logging.info("fail to add menu item")
- return
- if callbackfunc is not None:
- act.triggered.connect(callbackfunc)
- if group is None:
- self.addAction(act)
- else:
- self.addAction(group.addAction(act))
- return act
- def addSubMenu(self, menu):
- act = self.addMenu(menu)
- return act
- def getSubMenu(self, menuName):
- for i in range(len(self.actions())):
- act = self.actions()[i]
- if act.text() == menuName:
- return act.menu()
- return None
- def getAction(self, actionName):
- for i in range(len(self.actions())):
- act = self.actions()[i]
- if act.text() == actionName:
- return act
- return None
- def getActionByData(self, data):
- for i in range(len(self.actions())):
- act = self.actions()[i]
- if act.data() == data:
- return act
- return None
- def checkAction(self, actionName, isChecked):
- for i in range(len(self.actions())):
- act = self.actions()[i]
- if act.text() != actionName:
- continue
- act.setChecked(isChecked)
- pass
- def setPosition(self, pos):
- rect = QApplication.instance().primaryScreen().geometry()
- w = rect.width()
- h = rect.height()
- if pos.y() + self.height() > h: pos.setY(h - self.height())
- if pos.y() < 0: pos.setY(0)
- if pos.x() + self.width() > w: pos.setX(w - self.width())
- if pos.x() < 0: pos.setX(0)
- self.move(pos)
- def updateActionRect(self):
- """
- void QMenuPrivate::updateActionRects(const QRect &screen) const
- https://cep.xray.aps.anl.gov/software/qt4-x11-4.8.6-browser/da/d61/class_q_menu_private.html#acf93cda3ebe88b1234dc519c5f1b0f5d
- """
- self._aRect = {}
- topmargin = 0
- leftmargin = 0
- rightmargin = 0
- # qmenu.cpp Line 259:
- # init
- max_column_width = 0
- dh = self.height()
- y = 0
- style = self.style()
- opt = QStyleOption()
- opt.initFrom(self)
- hmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuHMargin, opt, self)
- vmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuVMargin, opt, self)
- icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
- fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
- deskFw = style.pixelMetric(QStyle.PixelMetric.PM_MenuDesktopFrameWidth, opt, self)
- tearoffHeight = style.pixelMetric(QStyle.PixelMetric.PM_MenuTearoffHeight, opt, self) if self.isTearOffEnabled() else 0
- # for compatibility now - will have to refactor this away
- tabWidth = 0
- maxIconWidth = 0
- hasCheckableItems = False
- # ncols = 1
- # sloppyAction = 0
- for i in range(len(self.actions())):
- act = self.actions()[i]
- if act.isSeparator() or act.isVisible() is False:
- continue
- # ..and some members
- hasCheckableItems |= act.isCheckable()
- ic = act.icon()
- if ic.isNull() is False:
- maxIconWidth = max(maxIconWidth, icone + 4)
- # qmenu.cpp Line 291:
- # calculate size
- qfm = self.fontMetrics()
- previousWasSeparator = True # this is true to allow removing the leading separators
- for i in range(len(self.actions())):
- act = self.actions()[i]
- if act.isVisible() is False \
- or (self.separatorsCollapsible() and previousWasSeparator and act.isSeparator()):
- # we continue, this action will get an empty QRect
- self._aRect[i] = QRect()
- continue
- previousWasSeparator = act.isSeparator()
- # let the style modify the above size..
- opt = QStyleOptionMenuItem()
- self.initStyleOption(opt, act)
- fm = opt.fontMetrics
- sz = QSize()
- # sz = self.sizeHint().expandedTo(self.minimumSize()).expandedTo(self.minimumSizeHint()).boundedTo(self.maximumSize())
- # calc what I think the size is..
- if act.isSeparator():
- sz = QSize(2, 2)
- else:
- s = act.text()
- if '\t' in s:
- t = s.index('\t')
- act.setText(s[t + 1:])
- tabWidth = max(int(tabWidth), qfm.boundingRect(s[t + 1:]).width())
- else:
- seq = act.shortcut()
- if seq.isEmpty() is False:
- tabWidth = max(int(tabWidth), qfm.boundingRect(seq.toString()).width())
- sz.setWidth(fm.boundingRect(QRect(), Qt.TextFlag.TextSingleLine | Qt.TextFlag.TextShowMnemonic, s).width())
- sz.setHeight(fm.height())
- if not act.icon().isNull():
- is_sz = QSize(icone, icone)
- if is_sz.height() > sz.height():
- sz.setHeight(is_sz.height())
- sz = style.sizeFromContents(QStyle.ContentsType.CT_MenuItem, opt, sz, self)
- if sz.isEmpty() is False:
- max_column_width = max(max_column_width, sz.width())
- # wrapping
- if y + sz.height() + vmargin > dh - deskFw * 2:
- # ncols += 1
- y = vmargin
- y += sz.height()
- # update the item
- self._aRect[i] = QRect(0, 0, sz.width(), sz.height())
- pass # exit for
- max_column_width += tabWidth # finally add in the tab width
- sfcMargin = style.sizeFromContents(QStyle.ContentsType.CT_Menu, opt, QSize(0, 0), self).width()
- min_column_width = self.minimumWidth() - (sfcMargin + leftmargin + rightmargin + 2 * (fw + hmargin))
- max_column_width = max(min_column_width, max_column_width)
- # qmenu.cpp Line 259:
- # calculate position
- base_y = vmargin + fw + topmargin + tearoffHeight
- x = hmargin + fw + leftmargin
- y = base_y
- for i in range(len(self.actions())):
- if self._aRect[i].isNull():
- continue
- if y + self._aRect[i].height() > dh - deskFw * 2:
- x += max_column_width + hmargin
- y = base_y
- self._aRect[i].translate(x, y) # move
- self._aRect[i].setWidth(max_column_width) # uniform width
- y += self._aRect[i].height()
- # update menu size
- s = self.sizeHint()
- self.resize(s)
-
- def getPenColor(self, opt):
- # if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
- # color = self.separator_color
- if opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
- if self._hover_color:
- color = self._hover_color
- else:
- color = opt.palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText)
- elif not (opt.state & QStyle.StateFlag.State_Enabled):
- color = opt.palette.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text)
- else:
- color = opt.palette.color(QPalette.ColorGroup.Normal, QPalette.ColorRole.Text)
- return color
-
- def drawControl(self, p, opt, arect, icon):
- """
- due to overrides the "paintEvent" method, so we must repaint all menu item by self.
- luckly, we have qt source code to reference.
-
- File: qtbase\src\widgets\styles\qstylesheetstyle.cpp
- Function: void drawControl (ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w=0) const
- """
-
- # Line 3891: case CE_MenuItem:
- style = self.style()
- p.setPen(self.getPenColor(opt))
- # Line 3932: draw icon and checked sign
- checkable = opt.checkType != QStyleOptionMenuItem.CheckType.NotCheckable
- checked = opt.checked if checkable else False
- if opt.icon.isNull() is False: # has custom icon
- dis = not (opt.state & QStyle.StateFlag.State_Enabled)
- active = opt.state & QStyle.StateFlag.State_Selected
- mode = QIcon.Mode.Disabled if dis else QIcon.Mode.Normal
- if active != 0 and not dis:
- mode = QIcon.Mode.Active
- fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
- icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
- iconRect = QRectF(arect.x() - fw, arect.y(), self._side_image.width(), arect.height())
- if checked:
- pixmap = icon.pixmap(QSize(icone, icone), mode, QIcon.State.On)
- else:
- pixmap = icon.pixmap(QSize(icone, icone), mode)
- pixw = pixmap.width()
- pixh = pixmap.height()
- pmr = QRectF(0, 0, pixw, pixh)
- pmr.moveCenter(iconRect.center())
- if checked:
- p.drawRect(QRectF(pmr.x() - 1, pmr.y() - 1, pixw + 2, pixh + 2))
- p.drawPixmap(pmr.topLeft(), pixmap)
- elif checkable and checked: # draw default checked sign
- opt.rect = QRect(0, arect.y(), self._side_image.width(), arect.height())
- opt.palette.setColor(QPalette.ColorRole.Text, self.getPenColor(opt))
- style.drawPrimitive(QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opt, p, self)
- # Line 3952: draw menu text
- p.setFont(opt.font)
- text_flag = Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextShowMnemonic | Qt.TextFlag.TextDontClip | Qt.TextFlag.TextSingleLine
- tr = QRect(arect)
- s = opt.text
- if '\t' in s:
- ss = s[s.index('\t') + 1:]
- fontwidth = opt.fontMetrics.boundingRect(ss).width()
- tr.moveLeft(opt.rect.right() - fontwidth)
- tr = QStyle.visualRect(opt.direction, opt.rect, tr)
- p.drawText(tr, text_flag, ss)
- tr.moveLeft(self._side_image.width() + arect.x())
- tr = QStyle.visualRect(opt.direction, opt.rect, tr)
- p.drawText(tr, text_flag, s)
- # Line 3973: draw sub menu arrow
- if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.SubMenu:
- arrowW = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorWidth, opt, self)
- arrowH = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorHeight, opt, self)
- arrowRect = QRect(0, 0, arrowW, arrowH)
- arrowRect.moveBottomRight(arect.bottomRight())
- arrow = QStyle.PrimitiveElement.PE_IndicatorArrowLeft if opt.direction == Qt.LayoutDirection.RightToLeft else QStyle.PrimitiveElement.PE_IndicatorArrowRight
- opt.rect = arrowRect
- opt.palette.setColor(QPalette.ColorRole.ButtonText, self.getPenColor(opt))
- style.drawPrimitive(arrow, opt, p, self)
- pass
- def paintEvent(self, event):
- # init
- self._bg_image = Kikka.style.getMenuBGImage()
- self._fg_image = Kikka.style.getMenuFGImage()
- self._side_image = Kikka.style.getMenuSideImage()
- self.updateActionRect()
- p = QPainter(self)
- # draw background
- p.fillRect(QRect(QPoint(), self.size()), self._side_image.pixelColor(0, 0))
- vertical = False
- y = self.height()
- while y > 0:
- yy = y - self._bg_image.height()
- p.drawImage(0, yy, self._side_image.mirrored(False, vertical))
- x = self._side_image.width()
- while x < self.width():
- p.drawImage(x, yy, self._bg_image.mirrored(False, vertical))
- x += self._bg_image.width()
- p.drawImage(x, yy, self._bg_image.mirrored(True, vertical))
- x += self._bg_image.width() + 1
- y -= self._bg_image.height()
- vertical = not vertical
- # draw item
- actioncount = len(self.actions())
- for i in range(actioncount):
- act = self.actions()[i]
- arect = QRect(self._aRect[i])
- if event.rect().intersects(arect) is False:
- continue
- opt = QStyleOptionMenuItem()
- self.initStyleOption(opt, act)
- opt.rect = arect
- if opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
- # Selected Item, draw foreground image
- p.setClipping(True)
- p.setClipRect(arect.x() + self._side_image.width(), arect.y(), self.width() - self._side_image.width(),
- arect.height())
- p.fillRect(QRect(QPoint(), self.size()), self._fg_image.pixelColor(0, 0))
- vertical = False
- y = self.height()
- while y > 0:
- x = self._side_image.width()
- while x < self.width():
- yy = y - self._fg_image.height()
- p.drawImage(x, yy, self._fg_image.mirrored(False, vertical))
- x += self._fg_image.width()
- p.drawImage(x, yy, self._fg_image.mirrored(True, vertical))
- x += self._fg_image.width() + 1
- y -= self._fg_image.height()
- vertical = not vertical
- p.setClipping(False)
- if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
- # Separator
- # p.setPen(opt.palette.color(QPalette.ColorRole.Text))
- y = int(arect.y() + arect.height() / 2)
- p.drawLine(self._side_image.width(), y, arect.width(), y)
- else:
- # MenuItem
- self.drawControl(p, opt, arect, act.icon())
- pass # exit for
- def eventFilter(self, obj, event):
- if obj == self:
- if event.type() == QEvent.Type.WindowDeactivate:
- self.Hide()
- elif event.type() == QEvent.Type.ToolTip:
- act = self.activeAction()
- if act != 0 and act.toolTip() != act.text():
- QToolTip.showText(event.globalPos(), act.toolTip())
- else:
- QToolTip.hideText()
- return False
- def _createTestMenu(parent=None):
- # test callback function
- def _test_callback(index=0, title=''):
- logging.info("MainMenu_callback: click [%d] %s" % (index, title))
- def _test_Exit(testmenu):
- # from kikka_app import KikkaApp
- callbackfunc = lambda: print("Exit!!")
- testmenu.addMenuItem("Exit", callbackfunc)
- def _test_MenuItemState(testmenu):
- menu = KMenu("MenuItem State", testmenu)
- c = 16
- for i in range(c):
- text = str("%s-item%d" % (menu.title(), i))
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- act = menu.addMenuItem(text, callbackfunc)
- if i >= c / 2:
- act.setDisabled(True)
- act.setText("%s-disable" % act.text())
- if i % 8 >= c / 4:
- act.setIcon(icon)
- act.setText("%s-icon" % act.text())
- if i % 4 >= c / 8:
- act.setCheckable(True)
- act.setText("%s-ckeckable" % act.text())
- if i % 2 >= c / 16:
- act.setChecked(True)
- act.setText("%s-checked" % act.text())
- testmenu.addSubMenu(menu)
- def _test_Shortcut(testmenu):
- menu = KMenu("Shortcut", testmenu)
- c = 4
- for i in range(c):
- text = str("%s-item" % (str(chr(ord('A') + i))))
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- act = menu.addMenuItem(text, callbackfunc)
- if i == 0:
- act.setShortcut(QKeySequence("Ctrl+T"))
- act.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
- act.setShortcutVisibleInContextMenu(True)
- testmenu.addSubMenu(menu)
- pass
- def _test_StatusTip(testmenu):
- pass
- def _test_Separator(testmenu):
- menu = KMenu("Separator", testmenu)
- menu.addSeparator()
- c = 5
- for i in range(c):
- text = str("%s-item%d" % (menu.title(), i))
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- menu.addMenuItem(text, callbackfunc)
- for j in range(i + 2): menu.addSeparator()
- testmenu.addSubMenu(menu)
- def _test_MultipleItem(testmenu):
- menu = KMenu("Multiple item", testmenu)
- for i in range(100):
- text = str("%s-item%d" % (menu.title(), i))
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- menu.addMenuItem(text, callbackfunc)
- testmenu.addSubMenu(menu)
- def _test_LongTextItem(testmenu):
- menu = KMenu("Long text item", testmenu)
- for i in range(5):
- text = str("%s-item%d " % (menu.title(), i)) * 20
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- menu.addMenuItem(text, callbackfunc)
- testmenu.addSubMenu(menu)
- def _test_LargeMenu(testmenu):
- menu = KMenu("Large menu", testmenu)
- for i in range(60):
- text = str("%s-item%d " % (menu.title(), i)) * 10
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- menu.addMenuItem(text, callbackfunc)
- if i % 5 == 0: menu.addSeparator()
- testmenu.addSubMenu(menu)
- def _test_LimitTest(testmenu):
- menu = KMenu("LimitTest", testmenu)
- _test_LargeMenu(menu)
- _test_MultipleItem(menu)
- _test_LongTextItem(menu)
- testmenu.addSubMenu(menu)
- def _test_Submenu(testmenu):
- menu = KMenu("Submenu", testmenu)
- testmenu.addSubMenu(menu)
- submenu = KMenu("submenu1", menu)
- menu.addSubMenu(submenu)
- m = submenu
- for i in range(8):
- next = KMenu("submenu%d" % (i + 2), testmenu)
- m.addSubMenu(next)
- m = next
- submenu = KMenu("submenu2", menu)
- menu.addSubMenu(submenu)
- m = submenu
- for i in range(8):
- for j in range(10):
- text = str("%s-item%d" % (m.title(), j))
- callbackfunc = lambda checked, a=j, b=text: _test_callback(a, b)
- m.addMenuItem(text, callbackfunc)
- next = KMenu("submenu%d" % (i + 2), testmenu)
- m.addSubMenu(next)
- m = next
- submenu = KMenu("SubMenu State", testmenu)
- c = 16
- for i in range(c):
- text = str("%s-%d" % (submenu.title(), i))
- m = KMenu(text, submenu)
- act = submenu.addSubMenu(m)
- callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
- act.triggered.connect(callbackfunc)
- if i >= c / 2:
- act.setDisabled(True)
- act.setText("%s-disable" % act.text())
- if i % 8 >= c / 4:
- act.setIcon(icon)
- act.setText("%s-icon" % act.text())
- if i % 4 >= c / 8:
- act.setCheckable(True)
- act.setText("%s-ckeckable" % act.text())
- if i % 2 >= c / 16:
- act.setChecked(True)
- act.setText("%s-checked" % act.text())
- submenu.addSubMenu(m)
- menu.addSubMenu(submenu)
- def _test_ImageTest(testmenu):
- imagetestmenu = KMenu("ImageTest", testmenu)
- testmenu.addSubMenu(imagetestmenu)
- menu = KMenu("MenuImage-normal", imagetestmenu)
- for i in range(32):
- text = " " * 54
- menu.addMenuItem(text)
- imagetestmenu.addSubMenu(menu)
- menu = KMenu("MenuImage-bit", imagetestmenu)
- menu.addMenuItem('')
- imagetestmenu.addSubMenu(menu)
- menu = KMenu("MenuImage-small", imagetestmenu)
- for i in range(10):
- text = " " * 30
- menu.addMenuItem(text)
- imagetestmenu.addSubMenu(menu)
- menu = KMenu("MenuImage-long", imagetestmenu)
- for i in range(64):
- text = " " * 54
- menu.addMenuItem(text)
- imagetestmenu.addSubMenu(menu)
- menu = KMenu("MenuImage-long2", imagetestmenu)
- for i in range(32):
- text = " " * 30
- menu.addMenuItem(text)
- imagetestmenu.addSubMenu(menu)
- menu = KMenu("MenuImage-large", imagetestmenu)
- for i in range(64):
- text = " " * 300
- menu.addMenuItem(text)
- imagetestmenu.addSubMenu(menu)
- menu = KMenu("MenuImage-verylarge", imagetestmenu)
- for i in range(100):
- text = " " * 600
- menu.addMenuItem(text)
- imagetestmenu.addSubMenu(menu)
- if parent is None:
- parent = QWidget(f=Qt.WindowType.Dialog)
- icon = QIcon(r"icon.ico")
- menu_test = KMenu("TestMenu", parent)
- _test_Exit(menu_test)
- menu_test.addSeparator()
- _test_MenuItemState(menu_test)
- _test_Shortcut(menu_test)
- _test_StatusTip(menu_test)
- _test_Separator(menu_test)
- _test_LimitTest(menu_test)
- _test_Submenu(menu_test)
- menu_test.addSeparator()
- _test_ImageTest(menu_test)
- menu_test.addSeparator()
- _test_Exit(menu_test)
- return menu_test
- if __name__ == '__main__':
- menu = _createTestMenu()
- menu.setPosition(QPoint(100, 100))
- menu.show()
|