Search code examples
pythonpython-2.7pyqtpyqt4

floating QDockWidget does not close when parentWidget closes


I have two open MainWindows: MainWindowWithButton and MainWindowWithDock. The later contains a QDockWidget.

IS behaviour: When the users makes the DockWidget floatable and closes MainWindowWithDock, the dockWidget doesn't close.

SHOULD behaviour: When the users makes the DockWidget floatable and closes MainWindowWithDock, the dockWidget closes as well.

Notes:

  • Reason for "IS behaviour": A floating DockWidget seems to be independent from it's parent
  • I cannot listen for onClose / reject (as it will give false information in my particular case.
  • The MainWindow does not emit clear signals about it's behaviour
  • It is important, that the DockWidget closes before the MainWindow closed. Otherwise the focus goes unexpected

Example code :

from PyQt4 import QtCore, QtGui
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QApplication, QDialog, QMainWindow
import sys

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class Ui_MainWindowWithButton(object):
    def setupUi(self, MainWindowWithButton):
        MainWindowWithButton.setObjectName(_fromUtf8("MainWindowWithButton"))
        MainWindowWithButton.resize(567, 384)
        self.centralwidget = QtGui.QWidget(MainWindowWithButton)
        self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
        MainWindowWithButton.setCentralWidget(self.centralwidget)

    def retranslateUi(self, MainWindowWithButton):
        MainWindowWithButton.setWindowTitle(_translate("MainWindowWithButton", "MainWindow", None))

class Ui_MainWindowWithDock(object):
    def setupUi(self, MainWindowWithDock):
        MainWindowWithDock.setObjectName(_fromUtf8("MainWindowWithDock"))
        MainWindowWithDock.resize(509, 316)
        self.centralwidget = QtGui.QWidget(MainWindowWithDock)
        self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
        MainWindowWithDock.setCentralWidget(self.centralwidget)

        # # # # # # # # # # # # # # # # # # # # # #
        # # #     setup dock      # # # # # # # # #
        # # # # # # # # # # # # # # # # # # # # # #
        self.theDock = QtGui.QDockWidget(MainWindowWithDock)
        self.theDock.setObjectName(_fromUtf8("theDock"))
        self.dockWidgetContents = QtGui.QWidget(self.theDock)
        self.dockWidgetContents.setObjectName(_fromUtf8("dockWidgetContents"))
        self.theDock.setWidget(self.dockWidgetContents)
        MainWindowWithDock.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.theDock)

        self.retranslateUi(MainWindowWithDock)
        QtCore.QMetaObject.connectSlotsByName(MainWindowWithDock)

    def retranslateUi(self, MainWindowWithDock):
        MainWindowWithDock.setWindowTitle(_translate("MainWindowWithDock", "MainWindow", None))

class MainWindowWithButtonDlg(QMainWindow):
    pass

class MainWindowWithDockDlg(QMainWindow):
    pass

def main():
    app = QApplication(sys.argv)

    windowWithDockUi = Ui_MainWindowWithDock()
    windowWithDock = MainWindowWithDockDlg()
    windowWithDockUi.setupUi(windowWithDock)
    windowWithDock.show()
    app.exec()

    # the dock widget should be closed by now

    ui = Ui_MainWindowWithButton()
    window = MainWindowWithButtonDlg()
    ui.setupUi(window)
    window.show()
    app.exec()



if __name__ == '__main__':
    main()

reject method of the original Source. Here we have a QDialog with a QMainwindow as it's central Widget - therefore it becomes a QMainWindow in some sense(from Anki addCards.py (scroll to bottom):

def reject(self):
    if not self.canClose(): # this way of calling is basically the problem: we might leave this method without doing anything
        return
    remHook('reset', self.onReset)
    remHook('currentModelChanged', self.onModelChange)
    clearAudioQueue()
    self.removeTempNote(self.editor.note)
    self.editor.setNote(None)
    self.modelChooser.cleanup()
    self.deckChooser.cleanup()
    self.mw.maybeReset()
    saveGeom(self, "add")
    aqt.dialogs.close("AddCards")
    QDialog.reject(self)

Solution

  • You can use an event-filter to monitor all the events of a given window. Just before a window closes, it will always post a close-event. It doesn't matter whether the window has modified the normal closing process or not. If and when it eventually closes, the accept property of the corresponding close-event is guaranteed to be True. So, if you watch for this event, you can simply check to see if it was accepted, and then act accordingly.

    The main problem to solve is how to find the right window to watch. At the time the dock-widget is created, this may not be accessible. So one approach is to wait until the parent of the dock-widget is first shown, then look for the top-level window and install an event-filter on that. Doing things this way means the dock-widget never needs to know anything about the window it is ultimately dependant upon.

    Below is a working demo of this approach based on your example (with most of the irrelevant stuff removed):

    import sys
    from PyQt4 import QtCore, QtGui
    from PyQt4.QtGui import QApplication, QDialog, QMainWindow
    
    class EventWatcher(QtCore.QObject):
        def __init__(self, parent):
            QtCore.QObject.__init__(self, parent)
            parent.installEventFilter(self)
    
        def eventFilter(self, source, event):
            if source is self.parent():
                if event.type() == QtCore.QEvent.Show:
                    target = source.parent()
                    while target.parent() is not None:
                        target = target.parent()
                    print('found target window: %r' % target)
                    source.removeEventFilter(self)
                    target.installEventFilter(self)
            elif event.type() == QtCore.QEvent.Close:
                source.closeEvent(event)
                print('test filter accepted: %s' % event.isAccepted())
                if event.isAccepted():
                    self.parent().close()
                return True
            return QtCore.QObject.eventFilter(self, source, event)
    
    class Ui_MainWindowWithDock(object):
        def setupUi(self, MainWindowWithDock):
            self.theDock = QtGui.QDockWidget(MainWindowWithDock)
            MainWindowWithDock.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.theDock)
            # add the event watcher
            EventWatcher(self.theDock)
    
    class MainWindowWithDockDlg(QMainWindow):
        pass
    
    # mock-up class for testing
    class MockDialog(QDialog):
        def __init__(self):
            QDialog.__init__(self)
            windowWithDock = MainWindowWithDockDlg()
            windowWithDockUi = Ui_MainWindowWithDock()
            windowWithDockUi.setupUi(windowWithDock)
            layout = QtGui.QVBoxLayout(self)
            layout.addWidget(windowWithDock)
            self.canClose = False
    
        def reject(self):
            if not self.canClose:
                self.canClose = True
                return
            QDialog.reject(self)
    
        def closeEvent(self, event):
            QDialog.closeEvent(self, event)
            print('test close accepted: %s' % event.isAccepted())
    
    def main():
        app = QApplication(sys.argv)
    
        dialog = MockDialog()
        dialog.show()
        app.exec_()
    
        # the dock widget should be closed by now
    
        window = QMainWindow()
        window.show()
        app.exec_()
    
    if __name__ == '__main__':
        main()