How to intercept QProgressDialog cancel click

760 views Asked by At

I have a standard QProgressDialog with a cancel button. If/When the user clicks the cancel button, I don't want the dialog to immediately hide, instead I would prefer to disable the cancel button and perform some clean-up work, and then close the QProgressDialog once I'm sure this work is complete. How to I intercept the current function?

From the docs it seems like I should be overwriting:

PySide.QtGui.QProgressDialog.cancel()

Resets the progress dialog. PySide.QtGui.QProgressDialog.wasCanceled() becomes true until the progress dialog is reset. The progress dialog becomes hidden.

I've tried subclassing this method but it doesn't even seem to be called when I click the cancel button.

1

There are 1 answers

5
musicamante On BEST ANSWER

To disable the button of the dialog you have to get a reference to it. Since it is a basic QPushButton, you can use findChild():

dialog = QProgressDialog(self)
cancelButton = dialog.findChild(QPushButton)
cancelButton.setEnabled(False)

Consider that disabling a button that would never get enabled is annoying from the UX point of view, so a better choice would be to not show it at all, and setCancelButton() explains how to do it:

If nullptr is passed, no cancel button will be shown.

In python terms, nullptr means None:

dialog = QProgressDialog(self)
dialog.setCancelButton(None)

Unfortunately, this won't prevent the user to cancel the dialog by closing it or by pressing Esc.

This is valid for any QDialog, and, to properly avoid that, subclassing is the better choice: you need to prevent rejecting the dialog (the Esc key) and the close event. While they have similar results, they are handled in different ways.

Overriding reject() (and doing nothing) prevents any action that would trigger a rejection (cancelling), including pressing Esc.

Overriding the closeEvent() requires an extra step: you have to ensure that the event is spontaneous() (triggered by the system - normally, the user presses the close button of the window), and eventually ignore that. This is necessary as you might need to call close() or accept() to actually close the dialog upon completing the process.

class NonStopProgressDialog(QtWidgets.QProgressDialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setCancelButton(None)

    def reject(self):
        pass

    def closeEvent(self, event):
        if event.spontaneous():
            event.ignore()

Note that there is no direct way to know if the spontaneous close event is directly triggered by the user (trying to close the window), or the system (while shutting down).

Also note that if you do need to close the dialog programmatically, you either call accept(), or you call the base implementation, which allows you to get the proper return value from the dialog's reject():

    def rejectNoMatterWhat(self):
        super().reject()

Finally, if, for any reason, you still need the cancel button, you have to disconnect its signals.

In general, this might do the work:

dialog = QProgressDialog(self)
cancelButton = dialog.findChild(QPushButton)
cancelButton.disconnect()

But the above would disconnect any signal to any slot, and there are some cases for which this should be avoided.
We know from the sources that the clicked signal is actually connected to the canceled() slot, so a better solution would be to do the following instead:

dialog = QProgressDialog(self)
cancelButton = dialog.findChild(QPushButton)
cancelButton.clicked.disconnect(self.canceled)

Since you may need to be notified about that in the parent/main class, a more appropriate solution would be to create a custom signal in the subclass used above:

class NonStopProgressDialog(QtWidgets.QProgressDialog):
    userCancel = QtCore.pyqtSignal()
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        cancelButton = self.findChild(QPushButton)
        cancelButton.clicked.disconnect(self.canceled)
        cancelButton.clicked.connect(
            lambda: cancelButton.setEnabled(False))
        cancelButton.clicked.connect(self.userCancel)

    def reject(self):
        pass

    def closeEvent(self, event):
        if event.spontaneous():
            event.ignore()

class SomeWindow(QtWidgets.QWidget):
    def showProgress(self):
        self.progressDialog = NonStopProgressDialog(self)
        self.progressDialog.userCancel.connect(self.stopSomething)
        # ...

    def stopSomething(self):
        self.progressDialog.setCancelButtonText('Please wait')
        # do something...