Merge pull request #591 from campagnola/nested-progress-dialog

Nested progress dialog
This commit is contained in:
Luke Campagnola 2017-10-04 10:13:22 -07:00 committed by GitHub
commit f160494127
2 changed files with 208 additions and 5 deletions

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Using ProgressDialog to show progress updates in a nested process.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import time
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
app = QtGui.QApplication([])
def runStage(i):
"""Waste time for 2 seconds while incrementing a progress bar.
"""
with pg.ProgressDialog("Running stage %s.." % i, maximum=100, nested=True) as dlg:
for j in range(100):
time.sleep(0.02)
dlg += 1
if dlg.wasCanceled():
print("Canceled stage %s" % i)
break
def runManyStages(i):
"""Iterate over runStage() 3 times while incrementing a progress bar.
"""
with pg.ProgressDialog("Running stage %s.." % i, maximum=3, nested=True, wait=0) as dlg:
for j in range(1,4):
runStage('%d.%d' % (i, j))
dlg += 1
if dlg.wasCanceled():
print("Canceled stage %s" % i)
break
with pg.ProgressDialog("Doing a multi-stage process..", maximum=5, nested=True, wait=0) as dlg1:
for i in range(1,6):
if i == 3:
# this stage will have 3 nested progress bars
runManyStages(i)
else:
# this stage will have 2 nested progress bars
runStage(i)
dlg1 += 1
if dlg1.wasCanceled():
print("Canceled process")
break

View File

@ -2,9 +2,14 @@
from ..Qt import QtGui, QtCore
__all__ = ['ProgressDialog']
class ProgressDialog(QtGui.QProgressDialog):
"""
Extends QProgressDialog for use in 'with' statements.
Extends QProgressDialog:
* Adds context management so the dialog may be used in `with` statements
* Allows nesting multiple progress dialogs
Example::
@ -14,7 +19,10 @@ class ProgressDialog(QtGui.QProgressDialog):
if dlg.wasCanceled():
raise Exception("Processing canceled by user")
"""
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False):
allDialogs = []
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False):
"""
============== ================================================================
**Arguments:**
@ -29,8 +37,18 @@ class ProgressDialog(QtGui.QProgressDialog):
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
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
self.disabled = disable or (not isGuiThread)
if self.disabled:
@ -42,20 +60,34 @@ class ProgressDialog(QtGui.QProgressDialog):
noCancel = True
self.busyCursor = busyCursor
QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent)
self.setMinimumDuration(wait)
# 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.WindowModal)
self.setValue(self.minimum())
if noCancel:
self.setCancelButton(None)
def __enter__(self):
if self.disabled:
return self
if self.busyCursor:
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.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):
@ -63,6 +95,12 @@ class ProgressDialog(QtGui.QProgressDialog):
return
if self.busyCursor:
QtGui.QApplication.restoreOverrideCursor()
if self._topDialog is not None:
self._topDialog._removeSubDialog(self)
ProgressDialog.allDialogs.pop(-1)
self.setValue(self.maximum())
def __iadd__(self, val):
@ -72,6 +110,88 @@ class ProgressDialog(QtGui.QProgressDialog):
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 = QtGui.QGridLayout()
self.setLayout(self._topLayout)
self._topLayout.setContentsMargins(0, 0, 0, 0)
# A vbox to contain all progress bars
self.nestedVBox = QtGui.QWidget()
self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2)
self.nestedLayout = QtGui.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:
widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)]
label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0]
bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0]
btn = [ch for ch in self.children() if isinstance(ch, QtGui.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 QtGui.QProgressDialog.resizeEvent(self, ev)
## wrap all other functions to make sure they aren't being called from non-gui threads
@ -80,6 +200,11 @@ class ProgressDialog(QtGui.QProgressDialog):
return
QtGui.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.WindowModal:
QtGui.QApplication.processEvents()
def setLabelText(self, val):
if self.disabled:
return
@ -109,4 +234,29 @@ class ProgressDialog(QtGui.QProgressDialog):
if self.disabled:
return 0
return QtGui.QProgressDialog.minimum(self)
class ProgressWidget(QtGui.QWidget):
"""Container for a label + progress bar that also allows its child widgets
to be hidden without changing size.
"""
def __init__(self, label, bar):
QtGui.QWidget.__init__(self)
self.hidden = False
self.layout = QtGui.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.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