From c2f39e99267381025f358588bcd7d0481ef85fe8 Mon Sep 17 00:00:00 2001 From: Nils <> Date: Fri, 18 Dec 2020 13:52:16 +0100 Subject: [PATCH] update template --- template/qtgui/mainwindow.py | 44 +++++++++++++++++++++++++++++++++ template/qtgui/menu.py | 47 +++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/template/qtgui/mainwindow.py b/template/qtgui/mainwindow.py index a94153f..8ad730b 100644 --- a/template/qtgui/mainwindow.py +++ b/template/qtgui/mainwindow.py @@ -129,6 +129,8 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.menubar.setNativeMenuBar(False) #Force a real menu bar. Qt on wayland will not display it otherwise. self.menu = Menu(mainWindow=self) #needs the about dialog, save file and the api.session ready. + self.installEventFilter(self) #Bottom of the file. Works around the hover numpad bug that is in Qt for years. + self.initiGuiSharedDataToSave() def start(self): @@ -148,6 +150,7 @@ class MainWindow(QtWidgets.QMainWindow): self._zoom() #enable zoom factor loaded from save file + #def event(self, event): # print (event.type()) # return super().event(event) @@ -333,5 +336,46 @@ class MainWindow(QtWidgets.QMainWindow): self.show() self.nsmClient.announceGuiVisibility(True) + def eventFilter(self, obj, event): + """Qt has a known but unresolved bug (for many years now) + that shortcuts with the numpad don't trigger. This has little + chance of ever getting fixed, despite getting reported multiple + times. + + Try to uninstall this filter (self.__init__) after a new qt release + and check if it works without. It should (it did already in the past) + but so far no luck... + + What do we do? + + We intercept every key and see if it was with a numpad modifier. + If yes we trigger the menu action ourselves and discard the event + We also need to separate the action of assigning a hover shortcut + to a menu and actually triggering it. Since we are in the template + part of the program we can't assume there is a main widget, + as in Patroneo and Laborejos case. + The obj is always MainWindow, so we can't detect if the menu was open or not. + self.qtApp.focusWidget() is also NOT the menu. + + The menu is only triggered AFTER the filter. But we need to + detect if a menu is open while we are running the filter. + self.menu.ui.menubar.activeAction() + + For each key press/release we receive 3 keypress events, not + counting autoRepeats. event.type() which is different from type(event) + returns 51 (press), 6(release), 7(accept) for the three. This is key press, release and event accept(?). + + This event filter somehow does not differentiate between numpad + on or numpad off. There maybe is another qt check for that. + """ + if (not self.menu.ui.menubar.activeAction()) and event.type() == 51 and type(event) is QtGui.QKeyEvent and event.modifiers() == QtCore.Qt.KeypadModifier and event.text() and event.text() in "0123456789": + action = self.menu.hoverShortcutDict[event.text()] + if action: + action.trigger() + event.accept() #despite what Qt docs say it is not enough to return True to "eat" the event. + return True + else: + #No keypad shortcut. Just use the normal handling. + return super().eventFilter(obj, event) diff --git a/template/qtgui/menu.py b/template/qtgui/menu.py index 8af0eab..2216787 100644 --- a/template/qtgui/menu.py +++ b/template/qtgui/menu.py @@ -30,7 +30,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets #Template Modules from .about import About -from .resources import * +from .resources import * #User Modules import engine.api as api @@ -41,12 +41,12 @@ class Menu(object): def __init__(self, mainWindow): """We can receive a dict of extra menu actions, which also override our hardcoded ones""" - self.mainWindow = mainWindow + self.mainWindow = mainWindow self.ui = mainWindow.ui #just shorter to write. #Hover Shortcuts self.hoverShortcutDict = HoverActionDictionary(self.mainWindow) #for temporary hover shortcuts. key=actual key, value=QAction - self.lastHoverAction = None #the last time the mouse hovered over a menu it was this QAction + self.lastHoverAction = None #the last time the mouse hovered over a menu it was this QAction. This gets never empty again, but works because we only check it during menu KeyPressEvent def _rememberHover(action): self.lastHoverAction = action self.ui.menubar.hovered.connect(_rememberHover) self._originalMenuKeyPressEvent = self.ui.menubar.keyPressEvent @@ -64,7 +64,7 @@ class Menu(object): def renameUndoRedoByHistory(self, undoList, redoList): undoString = QtCore.QCoreApplication.translate("TemplateMainWindow", "Undo") redoString = QtCore.QCoreApplication.translate("TemplateMainWindow", "Redo") - + if undoList: undoInfo = QtCore.QCoreApplication.translate("NOOPengineHistory", undoList[-1]) self.ui.actionUndo.setText(undoString +": {}".format(undoInfo)) @@ -81,18 +81,18 @@ class Menu(object): self.ui.actionRedo.setText(redoString) self.ui.actionRedo.setEnabled(False) - def _setupHelpMenu(self): - self.addSubmenu("menuHelp", QtCore.QCoreApplication.translate("TemplateMainWindow", "Help")) + def _setupHelpMenu(self): + self.addSubmenu("menuHelp", QtCore.QCoreApplication.translate("TemplateMainWindow", "Help")) self.addMenuEntry("menuHelp", "actionUser_Manual", QtCore.QCoreApplication.translate("TemplateMainWindow", "User Manual"), connectedFunction=self.mainWindow.userManual.show) self.addSeparator("menuHelp") self.addMenuEntry("menuHelp", "actionAbout", QtCore.QCoreApplication.translate("TemplateMainWindow", "About and Tips"), connectedFunction=self.mainWindow.about.show) - def _setupEditMenu(self): + def _setupEditMenu(self): self.addSubmenu("menuEdit", QtCore.QCoreApplication.translate("TemplateMainWindow", "Edit")) self.addMenuEntry("menuEdit", "actionUndo", QtCore.QCoreApplication.translate("TemplateMainWindow", "Undo"), connectedFunction=api.undo, shortcut="Ctrl+Z") self.addMenuEntry("menuEdit", "actionRedo", QtCore.QCoreApplication.translate("TemplateMainWindow", "Redo"), connectedFunction=api.redo, shortcut="Ctrl+Shift+Z") - def _setupFileMenu(self): + def _setupFileMenu(self): self.addSubmenu("menuFile", QtCore.QCoreApplication.translate("TemplateMainWindow", "File")) self.addMenuEntry("menuFile", "actionSave", QtCore.QCoreApplication.translate("TemplateMainWindow", "Save"), connectedFunction=api.save, shortcut="Ctrl+S") self.addMenuEntry("menuFile", "actionQuit", QtCore.QCoreApplication.translate("TemplateMainWindow", "Quit (without saving)"), connectedFunction=self.mainWindow.nsmClient.serverSendExitToSelf, shortcut="Ctrl+Q") @@ -121,12 +121,14 @@ class Menu(object): self.ui.menuDebug.menuAction().setVisible(state) #...therefore we hide manually. def menuKeyPressInterceptor(self, event): - """Menu Entries without their own shortcut can be temporarily assigned one of the ten + """ + Triggered when a menu is open and mouse hovers over a menu. + + Menu Entries without their own shortcut can be temporarily assigned one of the ten number keypad keys as shortcuts. If the numpad key is already in use it will be rerouted (freed first). """ - strings = set(("Num+1", "Num+2", "Num+3", "Num+4", "Num+5", "Num+6", "Num+7", "Num+8", "Num+9", "Num+0")) if self.lastHoverAction and (not self.lastHoverAction.menu()) and event.modifiers() == QtCore.Qt.KeypadModifier and ((not self.lastHoverAction.shortcut().toString()) or self.lastHoverAction.shortcut().toString() in strings): key = event.text() #just number 0-9 #event.key() is 49, 50... @@ -135,10 +137,11 @@ class Menu(object): else: self._originalMenuKeyPressEvent(event) + def addSubmenu(self, submenuActionAsString, text): """ - Returns the submenu. - + Returns the submenu. + submenuActionAsString is something like menuHelp @@ -213,7 +216,7 @@ class Menu(object): def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False): """ parameterstrings must already be translated. - + Returns the QAction Add a new menu entry to an existing submenu. @@ -242,7 +245,7 @@ class Menu(object): action.setText(text) #Text must already be translated action.setToolTip(tooltip) #Text must already be translated if iconResource: - assert QtCore.QFile.exists(iconResource) + assert QtCore.QFile.exists(iconResource) ico = QtGui.QIcon(iconResource) action.setIcon(ico) if checkable: @@ -257,14 +260,14 @@ class Menu(object): def hideMenuEntry(self, menuAction:str): action = getattr(self.ui, menuAction) - action.setVisible(False) - + action.setVisible(False) + def removeMenuEntry(self, menuAction:str): - raise NotImplementedError #TODO + raise NotImplementedError #TODO def addSeparator(self, submenu): submenu = getattr(self.ui, submenu) - submenu.addSeparator() + submenu.addSeparator() def connectMenuEntry(self, actionAsString, connectedFunction, shortcut=""): """ @@ -295,15 +298,15 @@ class Menu(object): def disable(self): """This is meant for a short temporary disabling.""" - + for a in self.mainWindow.findChildren(QtWidgets.QAction): if not a.menu(): - a.setEnabled(False) - + a.setEnabled(False) + def enable(self): for a in self.mainWindow.findChildren(QtWidgets.QAction): a.setEnabled(True) - + self.renameUndoRedoByHistory(*api.getUndoLists())