Browse Source

re-implement the qt-broken hover shortcuts by using our own event filter

Nils 4 years ago
  1. 44
  2. 47


@ -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. = 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.
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):
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
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.
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 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 =[event.text()]
if action:
event.accept() #despite what Qt docs say it is not enough to return True to "eat" the event.
return True
#No keypad shortcut. Just use the normal handling.
return super().eventFilter(obj, event)


@ -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._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):
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"),
self.addMenuEntry("menuHelp", "actionAbout", QtCore.QCoreApplication.translate("TemplateMainWindow", "About and Tips"),
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"),, 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 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):
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)
if checkable:
@ -257,14 +260,14 @@ class Menu(object):
def hideMenuEntry(self, menuAction:str):
action = getattr(self.ui, menuAction)
def removeMenuEntry(self, menuAction:str):
raise NotImplementedError #TODO
raise NotImplementedError #TODO
def addSeparator(self, submenu):
submenu = getattr(self.ui, submenu)
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
def enable(self):
for a in self.mainWindow.findChildren(QtWidgets.QAction):