(I started with Andy's answer, so thank you Andy!)
1) aboutToHide() works, by re-popping the menu at a cached position, BUT it can also enter an infinite loop. Testing if the mouse is clicked outside the menu to ignore re-opening should do the trick.
2) I tried an event filter but it blocks the actual click to the menu item.
3) Use both.
Here is a dirty pattern to prove that it works. This keeps the menu open when the user holds down CTRL when clicking:
# in __init__ ...
self.options_button.installEventFilter(self)
self.options_menu.installEventFilter(self)
self.options_menu.aboutToHide.connect(self.onAboutToHideOptionsMenu)
self.__options_menu_pos_cache = None
self.__options_menu_open = False
def onAboutToHideOptionsMenu(self):
if self.__options_menu_open: # Option + avoid an infinite loop
self.__options_menu_open = False # Turn it off to "reset"
self.options_menu.popup(self.__options_menu_pos_cache)
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.MouseButtonRelease:
if obj is self.options_menu:
if event.modifiers() == QtCore.Qt.ControlModifier:
self.__options_menu_open = True
return False
self.__options_menu_pos_cache = event.globalPos()
self.options_menu.popup(event.globalPos())
return True
return False
I say it is dirty because the widget here is acting as an event filter for both the button that opens the menu as well as the menu itself. Using explicit event filter classes would be easy enough to add and it would make things a little easier to follow.
The bools could probably be replaced with a check to see if the mouse is over the menu, and if not, don't pop it open. However, the CTRL key still has to be factored in for my use case, so it probably isn't far off a nice solution as it is.
When the user holds down CTRL and clicks on the menu, it flips a switch so the menu opens itself back up when it tried to close. The position is cached so it opens at the same position. There is a quick flicker, but it feels OK since the user knows they are holding a key down to make this work.
At the end of the day (literally) I already had the whole menu doing the right thing. I just wanted to add this functionality and I definitely didn't want to change to using a widget just for this. For this reason, I am keeping even this dirty patch for now.