Lorem Ipsum
Lorem Ipsum

Reputation: 4554

Prevent toolbar from being added to context menu

I am creating a custom toolbar which automatically adds itself to the parent (if present) when initialized.

I would like this custom toolbar to not appear in the context menu. However, something (I don't know what) related to the toolbar shows up, despite using setContextMenuPolicy :

enter image description here

I can't tell what that context menu item is. My understanding is that any widget, by way of its contextMenuPolicy, can be added to the context menu. Yet there isn't any other widget within the CustomToolBar.

A workaround is to disable the context menu altogether on the MainWindow and create a menu item (e.g. View) which toggles visibility.

import sys
import time
from PyQt5 import QtCore, QtWidgets


class CustomToolBar(QtWidgets.QToolBar):

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

        if parent:
            self.setParent(parent)
            self.parent().addToolBar(QtCore.Qt.BottomToolBarArea, self)

        self.setObjectName('Custom ToolBar')
        self.setContextMenuPolicy(QtCore.Qt.NoContextMenu)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.counter = 0
        self.resize(250, 75)

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction('&Exit', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.setToolTip('Exit application')
        self.exit_action.triggered.connect(self.close)

        self.menu = self.menuBar()

        self.menu_file = self.menu.addMenu('&File')
        self.menu_file.addAction(self.exit_action)

        # setting parent to self embeds the custom toolbar in the main window
        self.status = CustomToolBar(self)

    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

Upvotes: 1

Views: 694

Answers (2)

eyllanesc
eyllanesc

Reputation: 244282

Note: The image does not illustrate correctly if you are clicking on the menuBar or the lower toolBar.

Some widgets don't override the contextMenuEvent method so using self.setContextMenuPolicy(QtCore.Qt.NoContextMenu) won't work, and that's the case with the QToolBar (and the QMenuBar as well). In those cases the event method must be override and rejected.

Assuming you just want it not to appear in the QToolBar then just use:

class CustomToolBar(QtWidgets.QToolBar):

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

        if isinstance(parent, QtWidgets.QMainWindow):
            self.setParent(parent)
            parent.addToolBar(QtCore.Qt.BottomToolBarArea, self)

        self.setObjectName('Custom ToolBar')

    def event(self, event):
        if event.type() == QtCore.QEvent.ContextMenu:
            return True
        return super().event(event)

If you also want to do the same for the MenuBar you have to implement a similar logic:

class StatusBar(QtWidgets.QMenuBar):
    def event(self, event):
        if event.type() == QtCore.QEvent.ContextMenu:
            return True
        return super().event(event)

Finally, it is advisable to use names that indicate what type of widget it is, so I will change menu to menu_bar:

def init_widgets(self):

    self.exit_action = QtWidgets.QAction('&Exit', self)
    self.exit_action.setShortcut('Ctrl+Q')
    self.exit_action.setToolTip('Exit application')
    self.exit_action.triggered.connect(self.close)

    self.menu_bar = StatusBar()
    self.setMenuBar(self.menu_bar)

    self.menu_file = self.menu_bar.addMenu('&File')
    self.menu_file.addAction(self.exit_action)

    # setting parent to self embeds the custom toolbar in the main window
    self.tool_bar = CustomToolBar(self)

Update:

It seems that the objective of the OP is that the QAction associated with the QToolBar implemented in the QMenu of QMainWindow is not shown, so for this it is better to override the createPopupMenu method and remove the QAction and for this it is not necessary to implement a custom QToolBar.

import sys
import uuid

from PyQt5 import QtCore, QtWidgets


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction("&Exit", self)
        self.exit_action.setShortcut("Ctrl+Q")
        self.exit_action.setToolTip("Exit application")
        self.exit_action.triggered.connect(self.close)

        self.menu_bar = self.menuBar()

        self.menu_file = self.menu_bar.addMenu("&File")
        self.menu_file.addAction(self.exit_action)

        self.custom_toolbar = QtWidgets.QToolBar()
        self.addToolBar(QtCore.Qt.BottomToolBarArea, self.custom_toolbar)
        self.custom_toolbar.setProperty("hide_action_toolbar", True)

        self.dock = QtWidgets.QDockWidget("Dock")
        self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.dock)

    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)

    def createPopupMenu(self):
        titles = []
        for toolbar in self.findChildren(QtWidgets.QToolBar):
            if toolbar.property("hide_action_toolbar") is None:
                continue
            if toolbar.property("hide_action_toolbar"):
                toolbar.setProperty("last_window_title", toolbar.windowTitle())
                toolbar.setWindowTitle(uuid.uuid4().hex)
                titles.append(toolbar.windowTitle())
            else:
                toolbar.setWindowTitle(toolbar.property("last_window_title") or "")
        menu = super().createPopupMenu()
        for action in menu.actions():
            if action.text() in titles:
                menu.removeAction(action)
        return menu


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.resize(640, 480)
    main_window.show()
    sys.exit(app.exec_())

Upvotes: 4

Lorem Ipsum
Lorem Ipsum

Reputation: 4554

The item showing up in the context menu is, in fact, the toolbar. The menu item comes from the corresponding action text. This, in turn, comes from the toolbar window title. Since this is an empty string by default, it shows as blank in the menu.

Changing the line self.setObjectName('Custom ToolBar') to self.setWindowTitle('Custom ToolBar') will show the toolbar name, as expected.

enter image description here

import sys
import time
from PyQt5 import QtCore, QtWidgets


class CustomToolBar(QtWidgets.QToolBar):

    def __init__(self, parent=None, title='CustomToolBar'):
        super().__init__(parent=parent)

        # Sets the action text in the context menu
        self.setWindowTitle(title)

        if parent:
            self.setParent(parent)
            self.parent().addToolBar(QtCore.Qt.BottomToolBarArea, self)

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.counter = 0
        self.resize(250, 75)

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction('&Exit', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.setToolTip('Exit application')
        self.exit_action.triggered.connect(self.close)

        # self.menu_bar = CustomMenuBar()
        # self.setMenuBar(self.menu_bar)
        self.menu_bar = self.menuBar()

        self.menu_file = self.menu_bar.addMenu('&File')
        self.menu_file.addAction(self.exit_action)

        # self embeds the toolbar in the main window
        self.status = CustomToolBar(self)

    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()

    main_window.show()
    sys.exit(app.exec_())

As for the CustomToolBar not appearing in the context menu, that's more complicated. One option is to use a custom context menu via the parent (i.e. MainWindow). This post explains how that can be done.

However, wanting to have the CustomToolBar automatically remove itself is better for the user, but more of a challenge. The context menu is actually created on the fly by the QMainWindow via the createContextPopupMenu method. The CustomToolBar has to override this method such that it removes itself from the resulting list. Ideally, this should be done in a way that preserves the context menu's behavior.

The createContextPopupMenu method is called by the MainWindow's contextMenuEvent handler. So, using the fact that Python treats functions as first class citizens, we can replace the createPopupMenu method with a version that removes the CustomToolBar from it. The following example has added a QDockWidget to demonstrate that the CustomToolBar has removed itself.

enter image description here

import sys
import time
from PyQt5 import QtCore, QtWidgets


class CustomToolBar(QtWidgets.QToolBar):

    def __init__(self, parent=None, title='CustomToolBar'):
        super().__init__(parent=parent)

        # Sets the action text in the context menu
        self.setWindowTitle(title)

        if parent:
            self.setParent(parent)
            self.parent().addToolBar(QtCore.Qt.BottomToolBarArea, self)

            ############################################################
            # Remove the custom toolbar from the parent's context menu #
            ############################################################
            original_popup = self.parent().createPopupMenu
            original_handler = self.parent().contextMenuEvent

            # Create custom contextMenuEvent which repurposes the
            # original createPopupMenu method through use of closures
            def customContextMenuEvent(event):

                def custom_popup():
                    popup = original_popup()

                    # Because this is being handled by CustomToolBar,
                    # which adds itself to the MainWindow, there is
                    # guaranteed to be at least one item
                    for action in popup.actions():
                        if action.text() == self.windowTitle():
                            popup.removeAction(action)
                    return popup

                # call the original handler with its call to
                # createPopupMenu sneakily replaced with our own
                self.parent().createPopupMenu = custom_popup
                original_handler(event)

            # Replace the MainWindow's contextMenuEvent with our own
            self.parent().contextMenuEvent = customContextMenuEvent

        # Prevents right click on CustomToolBar from showing context menu
        self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.counter = 0
        self.resize(250, 75)

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction('&Exit', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.setToolTip('Exit application')
        self.exit_action.triggered.connect(self.close)

        self.menu_bar = self.menuBar()

        self.menu_file = self.menu_bar.addMenu('&File')
        self.menu_file.addAction(self.exit_action)

        # self embeds the status toolbar in the main window
        self.status = CustomToolBar(self)

        self.dock = QtWidgets.QDockWidget('Dock')
        self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.dock)


    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()

    main_window.show()
    sys.exit(app.exec_())

Upvotes: 1

Related Questions