Make dialog modal but don't block context menu

47 views
Skip to first unread message

Efrem Braun

unread,
Feb 8, 2021, 11:56:14 PM2/8/21
to pyqtgraph
I have an application which has a pyqtgraph instance in a dialog box. I want this dialog box to be modal (I don't want the user to be able to interact with the main window while the dialog box is open), so normally I would set `setWindowModality()` to `Qt.ApplicationModal`. However, this blocks the pyqtgraph's context menu from being accessible (I can right click on the pyqtgraph instance, and it shows me the context menu, but I'm not allowed to click on it).

I tried instead to set `setWindowModality()` to `Qt.WindowModal`. However, this locks the dialog box to the main window, which I don't like (I want the user to be able to see the main window while interacting with the dialog box).

Is there anyway to either:
1. Use  `setWindowModality(Qt.ApplicationModal)` but not block the pyqtgraph context menu?
or
2. Use  `setWindowModality(Qt.WindowModal)` but be able to undock the dialog box?

A minimal working example showing the behavior is below. You can see the 3 behaviors that result when setting the window modality to Qt.ApplicationModal, Qt.WindowModal, and Qt.NonModal.

```
from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *

import pyqtgraph as pg

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        b = QPushButton()
        b.setText("Open dialog")
        self.setCentralWidget(b)
        b.clicked.connect(self.showdialog)

    def showdialog(self):
        self.d = CustomDialog(self)

#        # not modal
#        self.d.setWindowModality(Qt.NonModal)
#        self.d.show()

        # location can't be moved
        self.d.setWindowModality(Qt.WindowModal)
        self.d.exec_()

#        # blocks context menu
#        self.d.setWindowModality(Qt.ApplicationModal)
#        self.d.exec_()


class CustomDialog(QDialog):
    def __init__(self, *args, **kwargs):
        super(CustomDialog, self).__init__(*args, **kwargs)

        buttons = QDialogButtonBox.Cancel
        self.buttonBox = QDialogButtonBox(buttons)
        self.buttonBox.rejected.connect(self.reject)
        self.layout = QVBoxLayout()
        self.layout.addWidget(pg.PlotWidget(self))
        self.layout.addWidget(self.buttonBox)
        self.setLayout(self.layout)


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

Thank you.

Efrem Braun

Efrem Braun

unread,
Feb 9, 2021, 11:09:28 AM2/9/21
to pyqtgraph
Update:

Apparently the pyqtgraph context menu only gets blocked on a Mac. When I run the working example using `Qt.ApplicationModal` on my Ubuntu OS, I can access the context menu. Still trying to figure out how to fix this issue on a Mac...

Efrem Braun

Efrem Braun

unread,
Feb 9, 2021, 12:14:32 PM2/9/21
to pyqtgraph
Solution:

Replace:
`self.d = CustomDialog(self)`
with:
`self.d = CustomDialog()`

Then, using Qt.WindowModal won't anchor the dialog to its parent window, since it doesn't have a parent window.

Efrem Braun

Efrem Braun

unread,
Feb 9, 2021, 12:35:20 PM2/9/21
to pyqtgraph
Sorry, that is not a valid solution. That makes the dialog box modeless; it's the same as doing `self.d.setWindowModality(Qt.NonModal)`. The user can then access the MainWindow dialog, which I don't want.

Any suggestions?

Patrick

unread,
Feb 9, 2021, 11:25:19 PM2/9/21
to pyqtgraph
Hi,

I think the odd behaviour you describe is because MacOS attaches modal windows to their parent. On Linux this is an option which can be toggled, say in gnome tweak tools. In any case, I think I've got around this in the past by manually disabling the UI widgets I don't want the user to interact with. This involves a bunch of "somewidget.setEnabled(False)" lines before showing the (non-modal) dialog, and hooking the dialog close() to re-enable widgets once the dialog is hidden. This can be somewhat simplified if you place widgets inside groupboxes/frames/etc, so you can just disable a couple of frames which also disables all containing widgets.
I can't actually find an example of this right now, but I'm pretty sure it worked OK.

Patrick

Efrem Braun

unread,
Feb 10, 2021, 11:12:09 AM2/10/21
to pyqtgraph
Very creative solution (though it's not so nice that such a workaround needs to be put in place). I'm able to implement it successfully in the manner below. I just disabled the entire parent window (repainting it to get around a separate bug: https://bugreports.qt.io/browse/PYSIDE-695), enabled the dialog window (which was disabled automatically when its parent was disabled), threw in a Qt.WindowStaysOnTopHint window flag, and connected the dialog window's close to enable the parent window. This has almost the same effect as making the dialog modal (the only difference I can discern is that the parent window is grayed out), but the PyQtGraph object's context menu is accessible.

Thanks! Solution below.

Efrem Braun

```
import sys

from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *

import pyqtgraph as pg

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        b = QPushButton()
        b.setText("Open dialog")
        self.setCentralWidget(b)
        b.clicked.connect(self.showdialog)

    def showdialog(self):
        self.d = CustomDialog(self)

#        # not modal
#        self.d.setWindowModality(Qt.NonModal)
#        self.d.show()

#        # location can't be moved
#        self.d.setWindowModality(Qt.WindowModal)
#        self.d.exec_()

#        # blocks context menu
#        self.d.setWindowModality(Qt.ApplicationModal)
#        self.d.exec_()

        if sys.platform == 'darwin':
            self.setEnabled(False)
            self.repaint()
            self.d.setEnabled(True)
            self.d.setWindowFlag(Qt.WindowStaysOnTopHint)
            self.d.finished.connect(lambda: self.setEnabled(True))
            self.d.show()   # This dialog is not modal (though we make it modal manually).
        else:
            self.d.exec_()  # This dialog is modal.


class CustomDialog(QDialog):
    def __init__(self, *args, **kwargs):
        super(CustomDialog, self).__init__(*args, **kwargs)

        buttons = QDialogButtonBox.Cancel
        self.buttonBox = QDialogButtonBox(buttons)
        self.buttonBox.rejected.connect(self.reject)
        self.layout = QVBoxLayout()
        self.layout.addWidget(pg.PlotWidget(self))
        self.layout.addWidget(self.buttonBox)
        self.setLayout(self.layout)


if __name__ == '__main__':
   app = QApplication(sys.argv)
   window = MainWindow()
   window.show()
   sys.exit(app.exec_())
```
Reply all
Reply to author
Forward
0 new messages