KikkaMenu.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. import os
  2. import logging
  3. from PySide6.QtCore import QRect, QSize, Qt, QRectF, QPoint, QEvent
  4. from PySide6.QtWidgets import QMenu, QStyle, QStyleOptionMenuItem, QStyleOption, QWidget, QApplication
  5. from PySide6.QtWidgets import QToolTip
  6. from PySide6.QtGui import QIcon, QImage, QPainter, QFont, QPalette, QColor, QKeySequence, QAction, QActionGroup
  7. # import kikka
  8. from Kikka.KikkaConst import *
  9. from Kikka.Utils.Singleton import singleton
  10. @singleton
  11. class KikkaMenu:
  12. _instance = None
  13. isDebug = False
  14. @staticmethod
  15. def getMenu(ghost_id, soul_id):
  16. pass
  17. # ghost = kikka.core.getGhost(ghost_id)
  18. # if ghost is not None:
  19. # return ghost.getSoul(soul_id).getMenu()
  20. # else:
  21. # logging.warning('menu lost')
  22. # return None
  23. @staticmethod
  24. def setAppMenu(menu):
  25. # QApplication.instance().trayIcon.setContextMenu(None)
  26. # QApplication.instance().trayIcon.setContextMenu(menu)
  27. pass
  28. @staticmethod
  29. def createSoulMainMenu(ghost):
  30. return
  31. # import kikka
  32. #
  33. # parent = QWidget(f=Qt.WindowType.Dialog)
  34. # mainmenu = KMenu(parent, ghost.ID, "Main")
  35. #
  36. # # shell list
  37. # menu = KMenu(mainmenu, ghost.ID, "Shells")
  38. # group1 = QActionGroup(parent)
  39. # for i in range(kikka.shell.getShellCount()):
  40. # shell = kikka.shell.getShellByIndex(i)
  41. # callbackfunc = lambda checked, name=shell.name: ghost.changeShell(name)
  42. # act = menu.addMenuItem(shell.unicode_name, callbackfunc, None, group1)
  43. # act.setData(shell.name)
  44. # act.setCheckable(True)
  45. # act.setToolTip(shell.description)
  46. # menu.setToolTipsVisible(True)
  47. # mainmenu.addSubMenu(menu)
  48. #
  49. # # clothes list
  50. # menu = KMenu(mainmenu, ghost.ID, "Clothes")
  51. # menu.setEnabled(False)
  52. # mainmenu.addSubMenu(menu)
  53. #
  54. # # balloon list
  55. # menu = KMenu(mainmenu, ghost.ID, "Balloons")
  56. # group2 = QActionGroup(parent)
  57. # for i in range(kikka.balloon.getBalloonCount()):
  58. # balloon = kikka.balloon.getBalloonByIndex(i)
  59. # callbackfunc = lambda checked, name=balloon.name: ghost.setBalloon(name)
  60. # act = menu.addMenuItem(balloon.unicode_name, callbackfunc, None, group2)
  61. # act.setCheckable(True)
  62. # mainmenu.addSubMenu(menu)
  63. #
  64. # optionmenu = KikkaMenu.createOptionMenu(parent, ghost)
  65. # mainmenu.addSubMenu(optionmenu)
  66. #
  67. # # debug option
  68. # if kikka.core.isDebug is True:
  69. #
  70. # def callbackfunction1():
  71. # kikka.core.isDebug = not kikka.core.isDebug
  72. # kikka.core.repaintAllGhost()
  73. #
  74. # def callbackfunction2():
  75. # kikka.shell.isDebug = not kikka.shell.isDebug
  76. # kikka.core.repaintAllGhost()
  77. #
  78. # menu = KMenu(mainmenu, ghost.ID, "Debug")
  79. #
  80. # act = menu.addMenuItem("Show ghost data", callbackfunction1)
  81. # act.setCheckable(True)
  82. # act.setChecked(kikka.core.isDebug is True)
  83. #
  84. # act = menu.addMenuItem("Show shell frame", callbackfunction2)
  85. # act.setCheckable(True)
  86. # act.setChecked(kikka.shell.isDebug is True)
  87. #
  88. # menu.addSubMenu(KMenu(menu, ghost.ID, "TestSurface"))
  89. # menu.addSubMenu(KikkaMenu.createTestMenu(menu))
  90. #
  91. # mainmenu.addSeparator()
  92. # mainmenu.addSubMenu(menu)
  93. # pass
  94. #
  95. # from kikka_app import KikkaApp
  96. # callbackfunc = lambda: KikkaApp.this().exitApp()
  97. # mainmenu.addSeparator()
  98. # mainmenu.addMenuItem("Exit", callbackfunc)
  99. # return mainmenu
  100. @staticmethod
  101. def createSoulDefaultMenu(ghost):
  102. import kikka
  103. parent = QWidget(f=Qt.WindowType.Dialog)
  104. mainmenu = KMenu(parent, ghost.ID, "Main")
  105. # shell list
  106. menu = KMenu(mainmenu, ghost.ID, "Shells")
  107. group1 = QActionGroup(parent)
  108. for i in range(kikka.shell.getShellCount()):
  109. shell = kikka.shell.getShellByIndex(i)
  110. callbackfunc = lambda checked, name=shell.name: ghost.changeShell(name)
  111. act = menu.addMenuItem(shell.unicode_name, callbackfunc, None, group1)
  112. act.setData(shell.name)
  113. act.setCheckable(True)
  114. mainmenu.addSubMenu(menu)
  115. # clothes list
  116. menu = KMenu(mainmenu, ghost.ID, "Clothes")
  117. menu.setEnabled(False)
  118. mainmenu.addSubMenu(menu)
  119. from kikka_app import KikkaApp
  120. callbackfunc = lambda: KikkaApp.this().exitApp()
  121. mainmenu.addMenuItem("Exit", callbackfunc)
  122. return mainmenu
  123. @staticmethod
  124. def createOptionMenu(parent, ghost):
  125. optionmenu = KMenu(parent, ghost.ID, "Option")
  126. callbackfunc1 = lambda checked: ghost.resetWindowsPosition(True, False)
  127. optionmenu.addMenuItem("Reset Shell Position", callbackfunc1)
  128. callbackfunc2 = lambda checked, g=ghost: g.setIsLockOnTaskbar(checked)
  129. act = optionmenu.addMenuItem("Lock on taskbar", callbackfunc2)
  130. act.setCheckable(True)
  131. act.setChecked(ghost.getIsLockOnTaskbar())
  132. return optionmenu
  133. # ###########################################################################
  134. @staticmethod
  135. def createTestMenu(parent=None):
  136. # test callback function
  137. def _test_callback(index=0, title=''):
  138. logging.info("MainMenu_callback: click [%d] %s" % (index, title))
  139. def _test_Exit(testmenu):
  140. # from kikka_app import KikkaApp
  141. callbackfunc = lambda: print("Exit!!")
  142. testmenu.addMenuItem("Exit", callbackfunc)
  143. def _test_MenuItemState(testmenu):
  144. menu = KMenu(testmenu, 0, "MenuItem State")
  145. c = 16
  146. for i in range(c):
  147. text = str("%s-item%d" % (menu.title(), i))
  148. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  149. act = menu.addMenuItem(text, callbackfunc)
  150. if i >= c / 2:
  151. act.setDisabled(True)
  152. act.setText("%s-disable" % act.text())
  153. if i % 8 >= c / 4:
  154. act.setIcon(icon)
  155. act.setText("%s-icon" % act.text())
  156. if i % 4 >= c / 8:
  157. act.setCheckable(True)
  158. act.setText("%s-ckeckable" % act.text())
  159. if i % 2 >= c / 16:
  160. act.setChecked(True)
  161. act.setText("%s-checked" % act.text())
  162. testmenu.addSubMenu(menu)
  163. def _test_Shortcut(testmenu):
  164. menu = KMenu(testmenu, 0, "Shortcut")
  165. c = 4
  166. for i in range(c):
  167. text = str("%s-item" % (str(chr(ord('A') + i))))
  168. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  169. act = menu.addMenuItem(text, callbackfunc)
  170. if i == 0:
  171. act.setShortcut(QKeySequence("Ctrl+T"))
  172. act.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
  173. act.setShortcutVisibleInContextMenu(True)
  174. testmenu.addSubMenu(menu)
  175. pass
  176. def _test_StatusTip(testmenu):
  177. pass
  178. def _test_Separator(testmenu):
  179. menu = KMenu(testmenu, 0, "Separator")
  180. menu.addSeparator()
  181. c = 5
  182. for i in range(c):
  183. text = str("%s-item%d" % (menu.title(), i))
  184. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  185. menu.addMenuItem(text, callbackfunc)
  186. for j in range(i + 2): menu.addSeparator()
  187. testmenu.addSubMenu(menu)
  188. def _test_MultipleItem(testmenu):
  189. menu = KMenu(testmenu, 0, "Multiple item")
  190. for i in range(100):
  191. text = str("%s-item%d" % (menu.title(), i))
  192. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  193. menu.addMenuItem(text, callbackfunc)
  194. testmenu.addSubMenu(menu)
  195. def _test_LongTextItem(testmenu):
  196. menu = KMenu(testmenu, 0, "Long text item")
  197. for i in range(5):
  198. text = str("%s-item%d " % (menu.title(), i)) * 20
  199. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  200. menu.addMenuItem(text, callbackfunc)
  201. testmenu.addSubMenu(menu)
  202. def _test_LargeMenu(testmenu):
  203. menu = KMenu(testmenu, 0, "Large menu")
  204. for i in range(60):
  205. text = str("%s-item%d " % (menu.title(), i)) * 10
  206. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  207. menu.addMenuItem(text, callbackfunc)
  208. if i % 5 == 0: menu.addSeparator()
  209. testmenu.addSubMenu(menu)
  210. def _test_LimitTest(testmenu):
  211. menu = KMenu(testmenu, 0, "LimitTest")
  212. _test_LargeMenu(menu)
  213. _test_MultipleItem(menu)
  214. _test_LongTextItem(menu)
  215. testmenu.addSubMenu(menu)
  216. def _test_Submenu(testmenu):
  217. menu = KMenu(testmenu, 0, "Submenu")
  218. testmenu.addSubMenu(menu)
  219. submenu = KMenu(menu, 0, "submenu1")
  220. menu.addSubMenu(submenu)
  221. m = submenu
  222. for i in range(8):
  223. next = KMenu(testmenu, 0, "submenu%d" % (i + 2))
  224. m.addSubMenu(next)
  225. m = next
  226. submenu = KMenu(menu, 0, "submenu2")
  227. menu.addSubMenu(submenu)
  228. m = submenu
  229. for i in range(8):
  230. for j in range(10):
  231. text = str("%s-item%d" % (m.title(), j))
  232. callbackfunc = lambda checked, a=j, b=text: _test_callback(a, b)
  233. m.addMenuItem(text, callbackfunc)
  234. next = KMenu(testmenu, 0, "submenu%d" % (i + 2))
  235. m.addSubMenu(next)
  236. m = next
  237. submenu = KMenu(testmenu, 0, "SubMenu State")
  238. c = 16
  239. for i in range(c):
  240. text = str("%s-%d" % (submenu.title(), i))
  241. m = KMenu(submenu, 0, text)
  242. act = submenu.addSubMenu(m)
  243. callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
  244. act.triggered.connect(callbackfunc)
  245. if i >= c / 2:
  246. act.setDisabled(True)
  247. act.setText("%s-disable" % act.text())
  248. if i % 8 >= c / 4:
  249. act.setIcon(icon)
  250. act.setText("%s-icon" % act.text())
  251. if i % 4 >= c / 8:
  252. act.setCheckable(True)
  253. act.setText("%s-ckeckable" % act.text())
  254. if i % 2 >= c / 16:
  255. act.setChecked(True)
  256. act.setText("%s-checked" % act.text())
  257. submenu.addSubMenu(m)
  258. menu.addSubMenu(submenu)
  259. def _test_ImageTest(testmenu):
  260. imagetestmenu = KMenu(testmenu, 0, "ImageTest")
  261. testmenu.addSubMenu(imagetestmenu)
  262. menu = KMenu(imagetestmenu, 0, "MenuImage-normal")
  263. for i in range(32):
  264. text = " " * 54
  265. menu.addMenuItem(text)
  266. imagetestmenu.addSubMenu(menu)
  267. menu = KMenu(imagetestmenu, 0, "MenuImage-bit")
  268. menu.addMenuItem('')
  269. imagetestmenu.addSubMenu(menu)
  270. menu = KMenu(imagetestmenu, 0, "MenuImage-small")
  271. for i in range(10):
  272. text = " " * 30
  273. menu.addMenuItem(text)
  274. imagetestmenu.addSubMenu(menu)
  275. menu = KMenu(imagetestmenu, 0, "MenuImage-long")
  276. for i in range(64):
  277. text = " " * 54
  278. menu.addMenuItem(text)
  279. imagetestmenu.addSubMenu(menu)
  280. menu = KMenu(imagetestmenu, 0, "MenuImage-long2")
  281. for i in range(32):
  282. text = " " * 30
  283. menu.addMenuItem(text)
  284. imagetestmenu.addSubMenu(menu)
  285. menu = KMenu(imagetestmenu, 0, "MenuImage-large")
  286. for i in range(64):
  287. text = " " * 300
  288. menu.addMenuItem(text)
  289. imagetestmenu.addSubMenu(menu)
  290. menu = KMenu(imagetestmenu, 0, "MenuImage-verylarge")
  291. for i in range(100):
  292. text = " " * 600
  293. menu.addMenuItem(text)
  294. imagetestmenu.addSubMenu(menu)
  295. if parent is None:
  296. parent = QWidget(f=Qt.WindowType.Dialog)
  297. icon = QIcon(r"icon.ico")
  298. menu_test = KMenu(parent, 0, "TestMenu")
  299. _test_Exit(menu_test)
  300. menu_test.addSeparator()
  301. _test_MenuItemState(menu_test)
  302. _test_Shortcut(menu_test)
  303. _test_StatusTip(menu_test)
  304. _test_Separator(menu_test)
  305. _test_LimitTest(menu_test)
  306. _test_Submenu(menu_test)
  307. menu_test.addSeparator()
  308. _test_ImageTest(menu_test)
  309. menu_test.addSeparator()
  310. _test_Exit(menu_test)
  311. return menu_test
  312. @staticmethod
  313. def updateTestSurface(menu, ghost, curSurface=-1):
  314. if kikka.core.isDebug is False or menu is None:
  315. return
  316. debugmenu = None
  317. for act in menu.actions():
  318. if act.text() == 'Debug':
  319. debugmenu = act.menu()
  320. break
  321. if debugmenu is None:
  322. return
  323. sufacemenu = None
  324. for act in debugmenu.actions():
  325. if act.text() == 'TestSurface':
  326. sufacemenu = act.menu()
  327. break
  328. if sufacemenu is None:
  329. return
  330. sufacemenu.clear()
  331. surfacelist = ghost.getShell().getSurfaceNameList()
  332. group = QActionGroup(sufacemenu.parent())
  333. for surfaceID, item in surfacelist.items():
  334. callbackfunc = lambda checked, faceID=surfaceID: ghost.getSoul(0).setSurface(faceID)
  335. name = "%3d - %s(%s)" % (surfaceID, item[0], item[1])
  336. act = sufacemenu.addMenuItem(name, callbackfunc, None, group)
  337. act.setCheckable(True)
  338. if surfaceID == curSurface:
  339. act.setChecked(True)
  340. pass
  341. def getDefaultImage():
  342. return QImage(os.path.join(RESOURCES_PATH, "shell.png"))
  343. def getMenuStyle():
  344. shellMenuStyle = ShellMenuStyle()
  345. shellMenuStyle.background_image = os.path.join(RESOURCES_PATH, "menu_background.png")
  346. shellMenuStyle.foreground_image = os.path.join(RESOURCES_PATH, "menu_foreground.png")
  347. shellMenuStyle.sidebar_image = os.path.join(RESOURCES_PATH, "menu_sidebar.png")
  348. return MenuStyle(shellMenuStyle)
  349. class ShellMenuStyle:
  350. def __init__(self):
  351. self.hidden = False
  352. self.font_family = ''
  353. self.font_size = -1
  354. self.background_image = ''
  355. self.background_font_color = [-1, -1, -1]
  356. self.background_alignment = 'lefttop'
  357. self.foreground_image = ''
  358. self.foreground_font_color = [-1, -1, -1]
  359. self.foreground_alignment = 'lefttop'
  360. self.disable_font_color = [-1, -1, -1]
  361. self.separator_color = [-1, -1, -1]
  362. self.sidebar_image = ''
  363. self.sidebar_alignment = 'lefttop'
  364. class MenuStyle:
  365. def __init__(self, shellMenu):
  366. # image
  367. if os.path.exists(shellMenu.background_image):
  368. self.bg_image = QImage(shellMenu.background_image)
  369. else:
  370. self.bg_image = getDefaultImage()
  371. logging.warning("Menu background image NOT found: %s" % shellMenu.background_image)
  372. if os.path.exists(shellMenu.foreground_image):
  373. self.fg_image = QImage(shellMenu.foreground_image)
  374. else:
  375. self.fg_image = getDefaultImage()
  376. logging.warning("Menu foreground image NOT found: %s" % shellMenu.foreground_image)
  377. if os.path.exists(shellMenu.sidebar_image):
  378. self.side_image = QImage(shellMenu.sidebar_image)
  379. else:
  380. self.side_image = getDefaultImage()
  381. logging.warning("Menu sidebar image NOT found: %s" % shellMenu.sidebar_image)
  382. # font and color
  383. if shellMenu.font_family != '':
  384. self.font = QFont(shellMenu.font_family, shellMenu.font_size)
  385. else:
  386. self.font = None
  387. if -1 in shellMenu.background_font_color:
  388. self.bg_font_color = None
  389. else:
  390. self.bg_font_color = QColor(shellMenu.background_font_color[0],
  391. shellMenu.background_font_color[1],
  392. shellMenu.background_font_color[2])
  393. if -1 in shellMenu.foreground_font_color:
  394. self.fg_font_color = None
  395. else:
  396. self.fg_font_color = QColor(shellMenu.foreground_font_color[0],
  397. shellMenu.foreground_font_color[1],
  398. shellMenu.foreground_font_color[2])
  399. if -1 in shellMenu.disable_font_color:
  400. self.disable_font_color = None
  401. else:
  402. self.disable_font_color = QColor(shellMenu.disable_font_color[0],
  403. shellMenu.disable_font_color[1],
  404. shellMenu.disable_font_color[2])
  405. if -1 in shellMenu.separator_color:
  406. self.separator_color = None
  407. else:
  408. self.separator_color = QColor(shellMenu.separator_color[0],
  409. shellMenu.separator_color[1],
  410. shellMenu.separator_color[2])
  411. # others
  412. self.hidden = shellMenu.hidden
  413. self.background_alignment = shellMenu.background_alignment
  414. self.foreground_alignment = shellMenu.foreground_alignment
  415. self.sidebar_alignment = shellMenu.sidebar_alignment
  416. pass
  417. def getPenColor(self, opt):
  418. if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
  419. color = self.separator_color
  420. elif opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
  421. color = self.fg_font_color
  422. elif not (opt.state & QStyle.StateFlag.State_Enabled):
  423. color = self.disable_font_color
  424. else:
  425. color = self.bg_font_color
  426. if color is None:
  427. color = opt.palette.color(QPalette.ColorRole.Text)
  428. return color
  429. class KMenu(QMenu):
  430. def __init__(self, parent, gid, title=''):
  431. QMenu.__init__(self, title, parent)
  432. self.gid = gid
  433. self._parent = parent
  434. self._aRect = {}
  435. self._bg_image = None
  436. self._fg_image = None
  437. self._side_image = None
  438. self.installEventFilter(self)
  439. self.setMouseTracking(True)
  440. self.setStyleSheet("QMenu { menu-scrollable: 1; }")
  441. self.setSeparatorsCollapsible(False)
  442. def addMenuItem(self, text, callbackfunc=None, iconfilepath=None, group=None):
  443. if iconfilepath is None:
  444. act = QAction(text, self._parent)
  445. elif os.path.exists(iconfilepath):
  446. act = QAction(QIcon(iconfilepath), text, self._parent)
  447. else:
  448. logging.info("fail to add menu item")
  449. return
  450. if callbackfunc is not None:
  451. act.triggered.connect(callbackfunc)
  452. if group is None:
  453. self.addAction(act)
  454. else:
  455. self.addAction(group.addAction(act))
  456. self.confirmMenuSize(act)
  457. return act
  458. def addSubMenu(self, menu):
  459. act = self.addMenu(menu)
  460. self.confirmMenuSize(act, menu.title())
  461. return act
  462. def getSubMenu(self, menuName):
  463. for i in range(len(self.actions())):
  464. act = self.actions()[i]
  465. if act.text() == menuName:
  466. return act.menu()
  467. return None
  468. def getAction(self, actionName):
  469. for i in range(len(self.actions())):
  470. act = self.actions()[i]
  471. if act.text() == actionName:
  472. return act
  473. return None
  474. def getActionByData(self, data):
  475. for i in range(len(self.actions())):
  476. act = self.actions()[i]
  477. if act.data() == data:
  478. return act
  479. return None
  480. def checkAction(self, actionName, isChecked):
  481. for i in range(len(self.actions())):
  482. act = self.actions()[i]
  483. if act.text() != actionName:
  484. continue
  485. act.setChecked(isChecked)
  486. pass
  487. def confirmMenuSize(self, item, text=''):
  488. # s = self.sizeHint()
  489. # w, h = kikka.helper.getScreenResolution()
  490. # if text == '':
  491. # text = item.text()
  492. # if KikkaMenu.isDebug and s.height() > h:
  493. # logging.warning("the Menu_Height out of Screen_Height, too many menu item when add: %s" % text)
  494. # if KikkaMenu.isDebug and s.width() > w:
  495. # logging.warning("the Menu_Width out of Screen_Width, too menu item text too long when add: %s" % text)
  496. return
  497. def setPosition(self, pos):
  498. rect = QApplication.instance().primaryScreen().geometry()
  499. w = rect.width()
  500. h = rect.height()
  501. if pos.y() + self.height() > h: pos.setY(h - self.height())
  502. if pos.y() < 0: pos.setY(0)
  503. if pos.x() + self.width() > w: pos.setX(w - self.width())
  504. if pos.x() < 0: pos.setX(0)
  505. self.move(pos)
  506. def updateActionRect(self):
  507. """
  508. void QMenuPrivate::updateActionRects(const QRect &screen) const
  509. https://cep.xray.aps.anl.gov/software/qt4-x11-4.8.6-browser/da/d61/class_q_menu_private.html#acf93cda3ebe88b1234dc519c5f1b0f5d
  510. """
  511. self._aRect = {}
  512. topmargin = 0
  513. leftmargin = 0
  514. rightmargin = 0
  515. # qmenu.cpp Line 259:
  516. # init
  517. max_column_width = 0
  518. dh = self.height()
  519. y = 0
  520. style = self.style()
  521. opt = QStyleOption()
  522. opt.initFrom(self)
  523. hmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuHMargin, opt, self)
  524. vmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuVMargin, opt, self)
  525. icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
  526. fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
  527. deskFw = style.pixelMetric(QStyle.PixelMetric.PM_MenuDesktopFrameWidth, opt, self)
  528. tearoffHeight = style.pixelMetric(QStyle.PixelMetric.PM_MenuTearoffHeight, opt, self) if self.isTearOffEnabled() else 0
  529. # for compatibility now - will have to refactor this away
  530. tabWidth = 0
  531. maxIconWidth = 0
  532. hasCheckableItems = False
  533. # ncols = 1
  534. # sloppyAction = 0
  535. for i in range(len(self.actions())):
  536. act = self.actions()[i]
  537. if act.isSeparator() or act.isVisible() is False:
  538. continue
  539. # ..and some members
  540. hasCheckableItems |= act.isCheckable()
  541. ic = act.icon()
  542. if ic.isNull() is False:
  543. maxIconWidth = max(maxIconWidth, icone + 4)
  544. # qmenu.cpp Line 291:
  545. # calculate size
  546. qfm = self.fontMetrics()
  547. previousWasSeparator = True # this is true to allow removing the leading separators
  548. for i in range(len(self.actions())):
  549. act = self.actions()[i]
  550. if act.isVisible() is False \
  551. or (self.separatorsCollapsible() and previousWasSeparator and act.isSeparator()):
  552. # we continue, this action will get an empty QRect
  553. self._aRect[i] = QRect()
  554. continue
  555. previousWasSeparator = act.isSeparator()
  556. # let the style modify the above size..
  557. opt = QStyleOptionMenuItem()
  558. self.initStyleOption(opt, act)
  559. fm = opt.fontMetrics
  560. sz = QSize()
  561. # sz = self.sizeHint().expandedTo(self.minimumSize()).expandedTo(self.minimumSizeHint()).boundedTo(self.maximumSize())
  562. # calc what I think the size is..
  563. if act.isSeparator():
  564. sz = QSize(2, 2)
  565. else:
  566. s = act.text()
  567. if '\t' in s:
  568. t = s.index('\t')
  569. act.setText(s[t + 1:])
  570. tabWidth = max(int(tabWidth), qfm.width(s[t + 1:]))
  571. else:
  572. seq = act.shortcut()
  573. if seq.isEmpty() is False:
  574. tabWidth = max(int(tabWidth), qfm.boundingRect(seq.toString()).width())
  575. sz.setWidth(fm.boundingRect(QRect(), Qt.TextFlag.TextSingleLine | Qt.TextFlag.TextShowMnemonic, s).width())
  576. sz.setHeight(fm.height())
  577. if not act.icon().isNull():
  578. is_sz = QSize(icone, icone)
  579. if is_sz.height() > sz.height():
  580. sz.setHeight(is_sz.height())
  581. sz = style.sizeFromContents(QStyle.ContentsType.CT_MenuItem, opt, sz, self)
  582. if sz.isEmpty() is False:
  583. max_column_width = max(max_column_width, sz.width())
  584. # wrapping
  585. if y + sz.height() + vmargin > dh - deskFw * 2:
  586. # ncols += 1
  587. y = vmargin
  588. y += sz.height()
  589. # update the item
  590. self._aRect[i] = QRect(0, 0, sz.width(), sz.height())
  591. pass # exit for
  592. max_column_width += tabWidth # finally add in the tab width
  593. sfcMargin = style.sizeFromContents(QStyle.ContentsType.CT_Menu, opt, QSize(0, 0), self).width()
  594. min_column_width = self.minimumWidth() - (sfcMargin + leftmargin + rightmargin + 2 * (fw + hmargin))
  595. max_column_width = max(min_column_width, max_column_width)
  596. # qmenu.cpp Line 259:
  597. # calculate position
  598. base_y = vmargin + fw + topmargin + tearoffHeight
  599. x = hmargin + fw + leftmargin
  600. y = base_y
  601. for i in range(len(self.actions())):
  602. if self._aRect[i].isNull():
  603. continue
  604. if y + self._aRect[i].height() > dh - deskFw * 2:
  605. x += max_column_width + hmargin
  606. y = base_y
  607. self._aRect[i].translate(x, y) # move
  608. self._aRect[i].setWidth(max_column_width) # uniform width
  609. y += self._aRect[i].height()
  610. # update menu size
  611. s = self.sizeHint()
  612. self.resize(s)
  613. def drawControl(self, p, opt, arect, icon, menustyle):
  614. """
  615. due to overrides the "paintEvent" method, so we must repaint all menu item by self.
  616. luckly, we have qt source code to reference.
  617. void drawControl (ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w=0) const
  618. https://cep.xray.aps.anl.gov/software/qt4-x11-4.8.6-browser/df/d91/class_q_style_sheet_style.html#ab92c0e0406eae9a15bc126b67f88c110
  619. Line 3533: element = CE_MenuItem
  620. """
  621. style = self.style()
  622. p.setPen(menustyle.getPenColor(opt))
  623. # Line 3566: draw icon and checked sign
  624. checkable = opt.checkType != QStyleOptionMenuItem.CheckType.NotCheckable
  625. checked = opt.checked if checkable else False
  626. if opt.icon.isNull() is False: # has custom icon
  627. dis = not (opt.state & QStyle.StateFlag.State_Enabled)
  628. active = opt.state & QStyle.StateFlag.State_Selected
  629. mode = QIcon.Mode.Disabled if dis else QIcon.Mode.Normal
  630. if active != 0 and not dis: mode = QIcon.Mode.Active
  631. fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
  632. icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
  633. iconRect = QRectF(arect.x() - fw, arect.y(), self._side_image.width(), arect.height())
  634. if checked:
  635. pixmap = icon.pixmap(QSize(icone, icone), mode, QIcon.State.On)
  636. else:
  637. pixmap = icon.pixmap(QSize(icone, icone), mode)
  638. pixw = pixmap.width()
  639. pixh = pixmap.height()
  640. pmr = QRectF(0, 0, pixw, pixh)
  641. pmr.moveCenter(iconRect.center())
  642. if checked: p.drawRect(QRectF(pmr.x() - 1, pmr.y() - 1, pixw + 2, pixh + 2))
  643. p.drawPixmap(pmr.topLeft(), pixmap)
  644. elif checkable and checked: # draw default checked sign
  645. opt.rect = QRect(0, arect.y(), self._side_image.width(), arect.height())
  646. opt.palette.setColor(QPalette.ColorRole.Text, menustyle.getPenColor(opt))
  647. style.drawPrimitive(QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opt, p, self)
  648. # Line 3604: draw emnu text
  649. font = menustyle.font
  650. if font is not None:
  651. p.setFont(font)
  652. else:
  653. p.setFont(opt.font)
  654. text_flag = Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextShowMnemonic | Qt.TextFlag.TextDontClip | Qt.TextFlag.TextSingleLine
  655. tr = QRect(arect)
  656. s = opt.text
  657. if '\t' in s:
  658. ss = s[s.index('\t') + 1:]
  659. fontwidth = opt.fontMetrics.width(ss)
  660. tr.moveLeft(opt.rect.right() - fontwidth)
  661. tr = QStyle.visualRect(opt.direction, opt.rect, tr)
  662. p.drawText(tr, text_flag, ss)
  663. tr.moveLeft(self._side_image.width() + arect.x())
  664. tr = QStyle.visualRect(opt.direction, opt.rect, tr)
  665. p.drawText(tr, text_flag, s)
  666. # Line 3622: draw sub menu arrow
  667. if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.SubMenu:
  668. arrowW = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorWidth, opt, self)
  669. arrowH = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorHeight, opt, self)
  670. arrowRect = QRect(0, 0, arrowW, arrowH)
  671. arrowRect.moveBottomRight(arect.bottomRight())
  672. arrow = QStyle.PrimitiveElement.PE_IndicatorArrowLeft if opt.direction == Qt.LayoutDirection.RightToLeft else QStyle.PrimitiveElement.PE_IndicatorArrowRight
  673. opt.rect = arrowRect
  674. opt.palette.setColor(QPalette.ColorRole.ButtonText, menustyle.getPenColor(opt))
  675. style.drawPrimitive(arrow, opt, p, self)
  676. pass
  677. def paintEvent(self, event):
  678. # init
  679. menustyle = getMenuStyle()
  680. self._bg_image = menustyle.bg_image
  681. self._fg_image = menustyle.fg_image
  682. self._side_image = menustyle.side_image
  683. self.updateActionRect()
  684. p = QPainter(self)
  685. # draw background
  686. p.fillRect(QRect(QPoint(), self.size()), self._side_image.pixelColor(0, 0))
  687. vertical = False
  688. y = self.height()
  689. while y > 0:
  690. yy = y - self._bg_image.height()
  691. p.drawImage(0, yy, self._side_image.mirrored(False, vertical))
  692. x = self._side_image.width()
  693. while x < self.width():
  694. p.drawImage(x, yy, self._bg_image.mirrored(False, vertical))
  695. x += self._bg_image.width()
  696. p.drawImage(x, yy, self._bg_image.mirrored(True, vertical))
  697. x += self._bg_image.width() + 1
  698. y -= self._bg_image.height()
  699. vertical = not vertical
  700. # draw item
  701. actioncount = len(self.actions())
  702. for i in range(actioncount):
  703. act = self.actions()[i]
  704. arect = QRect(self._aRect[i])
  705. if event.rect().intersects(arect) is False:
  706. continue
  707. opt = QStyleOptionMenuItem()
  708. self.initStyleOption(opt, act)
  709. opt.rect = arect
  710. if opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
  711. # Selected Item, draw foreground image
  712. p.setClipping(True)
  713. p.setClipRect(arect.x() + self._side_image.width(), arect.y(), self.width() - self._side_image.width(),
  714. arect.height())
  715. p.fillRect(QRect(QPoint(), self.size()), self._fg_image.pixelColor(0, 0))
  716. vertical = False
  717. y = self.height()
  718. while y > 0:
  719. x = self._side_image.width()
  720. while x < self.width():
  721. yy = y - self._fg_image.height()
  722. p.drawImage(x, yy, self._fg_image.mirrored(False, vertical))
  723. x += self._fg_image.width()
  724. p.drawImage(x, yy, self._fg_image.mirrored(True, vertical))
  725. x += self._fg_image.width() + 1
  726. y -= self._fg_image.height()
  727. vertical = not vertical
  728. p.setClipping(False)
  729. if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
  730. # Separator
  731. p.setPen(menustyle.getPenColor(opt))
  732. y = int(arect.y() + arect.height() / 2)
  733. p.drawLine(self._side_image.width(), y, arect.width(), y)
  734. else:
  735. # MenuItem
  736. self.drawControl(p, opt, arect, act.icon(), menustyle)
  737. pass # exit for
  738. def eventFilter(self, obj, event):
  739. if obj == self:
  740. if event.type() == QEvent.Type.WindowDeactivate:
  741. self.Hide()
  742. elif event.type() == QEvent.Type.ToolTip:
  743. act = self.activeAction()
  744. if act != 0 and act.toolTip() != act.text():
  745. QToolTip.showText(event.globalPos(), act.toolTip())
  746. else:
  747. QToolTip.hideText()
  748. return False