2

I'm trying to implement an "open" button such that if a user presses it and holds, a menu of applications for the user to choose from will pop up; but as soon as the user releases the mouse, the menu should disappear. if the user releases the mouse without choosing an application on the menu, it should open the file with the default application. I'm implementing this button as a QToolButton and connect the signals as follows:

self.ui_open_btn.pressed.connect(self._onOpenBtnPressed)
self.ui_open_btn.triggered.connect(self._onOpenBtnTriggered)

def _onOpenBtnPressed(self):
    self.ui_open_btn.showMenu()

def _onOpenBtnTriggered(self, action):
    application_name = action.text()
    # code to launch the application

Right now, when the user presses the button, the menu will pop up. However, the menu is still there when the user release the button, and action on the menu is triggered by clicking on it. I tried under both DelayedPopup and InstantPopup mode. As long as the menu has been set for self.ui_open_btn, I can no longer catch any released signal. How can I hide the menu when the user releases the mouse? How can the action on the menu be triggered by releasing the mouse?

--added---

I found another problem about using QToolButton: the menu always pops up when the button is pressed. Instead, I would like to catch the pressed signal, do some check to determine if the menu should pop up or not. So I changed my approach to write my customized toolbutton by subclassing QPushButton and QMenu. Please see my code posed in the answer below.

Thanks.

user110
  • 315
  • 1
  • 11
  • Did you try hiding the menu using the `released` signal ? – mguijarr Oct 03 '13 at 00:48
  • @mguijarr: Yes, I tried `self.ui_open_btn.menu().close()` and `self.ui_open_btn.menu().hide()`. Neither worked. I think the problem is as long as the menu has been set for `self.ui_open_btn`, I can no longer catch any `released` signal. – user110 Oct 03 '13 at 01:06

2 Answers2

0

Create a custom QToolButton that will filter events from the menu, and react on the mouse release event received by the menu:

class MyToolButton(QtGui.QToolButton):
    def __init__(self, *args):
        QtGui.QToolButton.__init__(self, *args)
    def eventFilter(self, menu, event):
        if event.type() == QtCore.QEvent.MouseButtonRelease:
            if self.underMouse():
                menu.close()
                # and now do default action
                print "doing default action"
                return True
        return False

Install the event filter after you have set the menu:

self.ui_open_btn.menu().installEventFilter(self.ui_open_btn)
mguijarr
  • 7,641
  • 6
  • 45
  • 72
  • I just tried. It doesn't work. `eventFilter()` is not called at all. – user110 Oct 03 '13 at 17:16
  • You created a MyToolButton, right? Not a QToolButton. You also have to install the event filter. Please show some code. – mguijarr Oct 03 '13 at 17:17
  • yes. I did: `self.open_btn = MyToolButton(self)' and 'self.open_btn.installEventFilter(self.openBtn.menu())` and `MyToolButton` is exactly like what you posted. – user110 Oct 03 '13 at 17:22
  • I also wonder why do we use `eventFilter()` instead of `mouseReleaseEvent()`? – user110 Oct 03 '13 at 17:24
  • Just to make sure: you call installEventFilter after you associated the menu? I mean, self.open_btn.menu() returns something, right? I can tell you I managed to get it working fine with the code of the answer so you can, too :) – mguijarr Oct 03 '13 at 17:26
  • We use eventFilter because we are filtering the menu widget events in the tool button, we are not interested in events happening in the tool button ; we want to alter the behaviour of the menu – mguijarr Oct 03 '13 at 17:26
  • you really got this to work? According to the doc, `eventFilter(self, watchedObj, event)` is filtering this object's events if it has been installed as an eventFilter for the `watchedObj`. If we want to filter the menu events, shouldn't we implement menu's eventFilter instead of toolbutton's? – user110 Oct 03 '13 at 18:24
  • not gonna work, since, in your code, you're filtering button events instead of the menu. But in fact, once the menu pops up after pressing the button, mouseReleaseEvent will belong to the menu instead of the button. Therefore, we should subclass `QMenu`. Besides, since we don't need to watch the button, we can override `mouseReleaseEvent` for `QMenu` instead of `eventFilter`. I posted my solution. You can check it out. Thank you very much, though. You posts helped my understanding a lot. – user110 Oct 03 '13 at 20:41
  • Ok, as you want... My solution works for me but it was not my problem ;) lol - Glad I could help. By the way why don't you like the event filter? – mguijarr Oct 03 '13 at 20:43
  • because `mouseReleaseEvent` already filters the mouse release events for us and I could call parent class `QMenu`'s original mouseReleaseEvent method in my overridden one such that I don't need to write the code to calculate which action on the menu is triggered. – user110 Oct 03 '13 at 20:53
0

I managed to achieve what I want by subclassing QPushButton and QMennu:

class MyMenu(QtGui.QMenu):
    """ Custom menu which will close when mouse is released. ""'"
    def mouseReleaseEvent(self, event):
        action = self.actionAt(event.pos())
        self.triggered.emit(action)
        self.close()

class MyButton(QtGui.QPushButton):
    triggered = QtCore.pyqtSignal("QAction")

    def __init__(self, menu=None, parent=None):
        super(MyButton, self).__init__(parent)
        self.setMenu(menu)

    def menu(self):
        return self._menu

    def setMenu(self, menu):
        self._menu = menu if menu else MyMenu(self)
        self._menu.triggered.connect(self.triggered.emit)

and in the QDialog containing this button, I do the following:

menu = MyMenu(self)
# insert here code to add actions to menu
self.open_btn = MyButton(parent=self, menu=menu)
self.open_btn.pressed.connect(self._onOpenBtnPressed)
self.open_btn.triggered.connect(self._onOpenBtnTriggered)

def _onOpenBtnPressed(self):
    # insert here code to check whether we should pop up the menu
    pos = self.mapToGlobal(self.open_btn.pos())
    pos.setY(pos.y() + self.open_btn.height())
    self.open_btn.menu().move(pos)
    self.open_btn.menu().show()

def _onOpenBtnTriggered(self, action):
    if action:
        application_name = str(action.text())
        # insert here code to launch this application
    else:
        # insert here code to launch the default application
    self.close() # close this dialog
user110
  • 315
  • 1
  • 11