1
votes

I have a situation where I'm exchanging data asynchronously in reaction to button presses in a python PySide GUI. I have my code sending the data in a different thread (and exchanging data via Qt signals and slots for thread safety), and that all works correctly.

The problem is that I don't want a double click on a toolbar button to generate two transfers. I'd like the main window to be blocked while the transfer is going on. The QProgressDialog seemed like exactly what I wanted, especially since it doesn't show itself if the transfer is quick.

I wrote some code to utilize it:

def startUSBProgress(self, message, operation):
    title = self.tr("Transferring USB Data")
    progress = QtGui.QProgressDialog(message, "", 0, 1, self)
    progress.setWindowModality(QtCore.Qt.WindowModal)
    progress.setCancelButton(None)
    progress.setMinimumDuration(500)  # half second
    progress.setWindowTitle(title)

    operation.completed.connect(progress.reset)
    operation.completed.connect(progress.deleteLater)
    operation.error.connect(progress.reset)
    operation.error.connect(progress.deleteLater)

    progress.setValue(0)

And I'm testing it with an artificial 1 second sleep in all of my transfers. There's no error, but the progress dialog is not modal before the 500 millisecond delay has elapsed (and the progress dialog becomes visible), so it doesn't actually solve my problem. Multiple button presses are still possible. The dialog is modal after the minimum duration, just not before.

I'm not sure if this is a bug or an expected feature. I couldn't find any discussion about it. Regardless, it's undesirable behavior for my application.

I don't want to set minimumDelay to 0, since 99.9% of the time the transactions are quite quick, and having the dialog blip wouldn't look good.

Is there any way to solve this? I'm open to slightly hacky options like temporarily suppressing mouse & keyboard events. Note, the event loop needs to run as that's how the completion signals are processed.

My development environment (where I'm seeing this issue) is Windows 7, but my application will eventually be cross-platform.

2

2 Answers

0
votes

If there are now Qt/Python specifics, I think

progress.show()

should immediately show the modal dialog.

0
votes

I found a way to implement what I was looking for using Qt event filters.

Idea taken from https://stackoverflow.com/a/2017400/1011276

Filtering Class:

class UserEventsFilter(QtCore.QObject):
    """
    Reference: https://stackoverflow.com/a/2017400/1011276
    """
    def __init__(self, parent=None):
        super().__init__(parent)

        self.filterEnabled = False

    @QtCore.Slot()
    def enableFilter(self):
        self.filterEnabled = True

    @QtCore.Slot()
    def disableFilter(self):
        self.filterEnabled = False

    def eventFilter(self, obj, event):
        if self.filterEnabled:
            t = event.type()
            if (t == QtCore.QEvent.KeyPress or
                t == QtCore.QEvent.KeyRelease or 
                t == QtCore.QEvent.MouseButtonPress or
                t == QtCore.QEvent.MouseButtonDblClick or
                t == QtCore.QEvent.MouseMove or
                t == QtCore.QEvent.Enter or
                t == QtCore.QEvent.HoverEnter or
                t == QtCore.QEvent.HoverLeave or
                t == QtCore.QEvent.HoverMove or
                t == QtCore.QEvent.DragEnter or
                t == QtCore.QEvent.DragLeave or
                t == QtCore.QEvent.DragMove or
                t == QtCore.QEvent.Drop):
                return True
        return QtCore.QObject.eventFilter(self, obj, event)

Init Code (in main window's __init__):

self.userEventFilter = UserEventsFilter(self)
app = QtCore.QCoreApplication.instance()
app.installEventFilter(self.userEventFilter)

Updated usage code:

def startUSBProgress(self, message, operation):
    title = self.tr("Transferring USB Data")
    progress = QtGui.QProgressDialog(message, "", 0, 1, self)

    progress.setWindowModality(QtCore.Qt.WindowModal)
    progress.setCancelButton(None)
    progress.setMinimumDuration(500)  # half second
    progress.setWindowTitle(title)

    operation.completed.connect(progress.reset)
    operation.completed.connect(progress.deleteLater)
    operation.completed.connect(self.userEventFilter.disableFilter)
    operation.error.connect(progress.reset)
    operation.error.connect(progress.deleteLater)
    operation.error.connect(self.userEventFilter.disableFilter)

    self.userEventFilter.enableFilter()
    progress.setValue(0)