Source code for pyqtgraph.widgets.ProgressDialog

from time import perf_counter

from ..Qt import QtCore, QtGui, QtWidgets

__all__ = ['ProgressDialog']


[docs] class ProgressDialog(QtWidgets.QProgressDialog): """ Extends QProgressDialog: * Adds context management so the dialog may be used in `with` statements * Allows nesting multiple progress dialogs Example:: with ProgressDialog("Processing..", minVal, maxVal) as dlg: # do stuff dlg.setValue(i) ## could also use dlg += 1 if dlg.wasCanceled(): raise Exception("Processing canceled by user") """ allDialogs = []
[docs] def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False): """ ============== ================================================================ **Arguments:** labelText (required) cancelText Text to display on cancel button, or None to disable it. minimum maximum parent wait Length of time (im ms) to wait before displaying dialog busyCursor If True, show busy cursor until dialog finishes disable If True, the progress dialog will not be displayed and calls to wasCanceled() will always return False. If ProgressDialog is entered from a non-gui thread, it will always be disabled. nested (bool) If True, then this progress bar will be displayed inside any pre-existing progress dialogs that also allow nesting. ============== ================================================================ """ # attributes used for nesting dialogs self.nestedLayout = None self._nestableWidgets = None self._nestingReady = False self._topDialog = None self._subBars = [] self.nested = nested # for rate-limiting Qt event processing during progress bar update self._lastProcessEvents = None isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() self.disabled = disable or (not isGuiThread) if self.disabled: return noCancel = False if cancelText is None: cancelText = '' noCancel = True self.busyCursor = busyCursor QtWidgets.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent) # If this will be a nested dialog, then we ignore the wait time if nested is True and len(ProgressDialog.allDialogs) > 0: self.setMinimumDuration(2**30) else: self.setMinimumDuration(wait) self.setWindowModality(QtCore.Qt.WindowModality.WindowModal) self.setValue(self.minimum()) if noCancel: self.setCancelButton(None)
def __enter__(self): if self.disabled: return self if self.busyCursor: QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) if self.nested and len(ProgressDialog.allDialogs) > 0: topDialog = ProgressDialog.allDialogs[0] topDialog._addSubDialog(self) self._topDialog = topDialog topDialog.canceled.connect(self.cancel) ProgressDialog.allDialogs.append(self) return self def __exit__(self, exType, exValue, exTrace): if self.disabled: return if self.busyCursor: QtWidgets.QApplication.restoreOverrideCursor() if self._topDialog is not None: self._topDialog._removeSubDialog(self) ProgressDialog.allDialogs.pop(-1) self.setValue(self.maximum()) def __iadd__(self, val): """Use inplace-addition operator for easy incrementing.""" if self.disabled: return self self.setValue(self.value()+val) return self def _addSubDialog(self, dlg): # insert widgets from another dialog into this one. # set a new layout and arrange children into it (if needed). self._prepareNesting() bar, btn = dlg._extractWidgets() # where should we insert this widget? Find the first slot with a # "removed" widget (that was left as a placeholder) inserted = False for i,bar2 in enumerate(self._subBars): if bar2.hidden: self._subBars.pop(i) bar2.hide() bar2.setParent(None) self._subBars.insert(i, bar) inserted = True break if not inserted: self._subBars.append(bar) # reset the layout while self.nestedLayout.count() > 0: self.nestedLayout.takeAt(0) for b in self._subBars: self.nestedLayout.addWidget(b) def _removeSubDialog(self, dlg): # don't remove the widget just yet; instead we hide it and leave it in # as a placeholder. bar, btn = dlg._extractWidgets() bar.hide() def _prepareNesting(self): # extract all child widgets and place into a new layout that we can add to if self._nestingReady is False: # top layout contains progress bars + cancel button at the bottom self._topLayout = QtWidgets.QGridLayout() self.setLayout(self._topLayout) self._topLayout.setContentsMargins(0, 0, 0, 0) # A vbox to contain all progress bars self.nestedVBox = QtWidgets.QWidget() self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2) self.nestedLayout = QtWidgets.QVBoxLayout() self.nestedVBox.setLayout(self.nestedLayout) # re-insert all widgets bar, btn = self._extractWidgets() self.nestedLayout.addWidget(bar) self._subBars.append(bar) self._topLayout.addWidget(btn, 1, 1, 1, 1) self._topLayout.setColumnStretch(0, 100) self._topLayout.setColumnStretch(1, 1) self._topLayout.setRowStretch(0, 100) self._topLayout.setRowStretch(1, 1) self._nestingReady = True def _extractWidgets(self): # return: # 1. a single widget containing the label and progress bar # 2. the cancel button if self._nestableWidgets is None: label = [ch for ch in self.children() if isinstance(ch, QtWidgets.QLabel)][0] bar = [ch for ch in self.children() if isinstance(ch, QtWidgets.QProgressBar)][0] btn = [ch for ch in self.children() if isinstance(ch, QtWidgets.QPushButton)][0] sw = ProgressWidget(label, bar) self._nestableWidgets = (sw, btn) return self._nestableWidgets def resizeEvent(self, ev): if self._nestingReady: # don't let progress dialog manage widgets anymore. return return super().resizeEvent(ev) ## wrap all other functions to make sure they aren't being called from non-gui threads def setValue(self, val): if self.disabled: return QtWidgets.QProgressDialog.setValue(self, val) # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModality.WindowModal: now = perf_counter() if self._lastProcessEvents is None or (now - self._lastProcessEvents) > 0.2: QtWidgets.QApplication.processEvents() self._lastProcessEvents = now def setLabelText(self, val): if self.disabled: return QtWidgets.QProgressDialog.setLabelText(self, val) def setMaximum(self, val): if self.disabled: return QtWidgets.QProgressDialog.setMaximum(self, val) def setMinimum(self, val): if self.disabled: return QtWidgets.QProgressDialog.setMinimum(self, val) def wasCanceled(self): if self.disabled: return False return QtWidgets.QProgressDialog.wasCanceled(self) def maximum(self): if self.disabled: return 0 return QtWidgets.QProgressDialog.maximum(self) def minimum(self): if self.disabled: return 0 return QtWidgets.QProgressDialog.minimum(self)
class ProgressWidget(QtWidgets.QWidget): """Container for a label + progress bar that also allows its child widgets to be hidden without changing size. """ def __init__(self, label, bar): QtWidgets.QWidget.__init__(self) self.hidden = False self.layout = QtWidgets.QVBoxLayout() self.setLayout(self.layout) self.label = label self.bar = bar self.layout.addWidget(label) self.layout.addWidget(bar) def eventFilter(self, obj, ev): return ev.type() == QtCore.QEvent.Type.Paint def hide(self): # hide label and bar, but continue occupying the same space in the layout for widget in (self.label, self.bar): widget.installEventFilter(self) widget.update() self.hidden = True