Add option to join nested progress dialogs into a single window
This commit is contained in:
parent
45999e0f2b
commit
f4c3d88251
53
examples/ProgressDialog.py
Normal file
53
examples/ProgressDialog.py
Normal 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 3 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.03)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
|
|
||||||
__all__ = ['ProgressDialog']
|
__all__ = ['ProgressDialog']
|
||||||
|
|
||||||
|
|
||||||
class ProgressDialog(QtGui.QProgressDialog):
|
class ProgressDialog(QtGui.QProgressDialog):
|
||||||
"""
|
"""
|
||||||
Extends QProgressDialog for use in 'with' statements.
|
Extends QProgressDialog for use in 'with' statements.
|
||||||
|
@ -14,7 +16,10 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
if dlg.wasCanceled():
|
if dlg.wasCanceled():
|
||||||
raise Exception("Processing canceled by user")
|
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:**
|
**Arguments:**
|
||||||
|
@ -29,6 +34,9 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
and calls to wasCanceled() will always return False.
|
and calls to wasCanceled() will always return False.
|
||||||
If ProgressDialog is entered from a non-gui thread, it will
|
If ProgressDialog is entered from a non-gui thread, it will
|
||||||
always be disabled.
|
always be disabled.
|
||||||
|
nested (bool) If True, then this progress bar will be displayed inside
|
||||||
|
any pre-existing progress dialogs that also allow nesting (if
|
||||||
|
any).
|
||||||
============== ================================================================
|
============== ================================================================
|
||||||
"""
|
"""
|
||||||
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
|
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
|
||||||
|
@ -42,20 +50,40 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
noCancel = True
|
noCancel = True
|
||||||
|
|
||||||
self.busyCursor = busyCursor
|
self.busyCursor = busyCursor
|
||||||
|
|
||||||
QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent)
|
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.setWindowModality(QtCore.Qt.WindowModal)
|
||||||
self.setValue(self.minimum())
|
self.setValue(self.minimum())
|
||||||
if noCancel:
|
if noCancel:
|
||||||
self.setCancelButton(None)
|
self.setCancelButton(None)
|
||||||
|
|
||||||
|
|
||||||
|
# attributes used for nesting dialogs
|
||||||
|
self.nestedLayout = None
|
||||||
|
self._nestableWidgets = None
|
||||||
|
self._nestingReady = False
|
||||||
|
self._topDialog = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if self.disabled:
|
if self.disabled:
|
||||||
return self
|
return self
|
||||||
if self.busyCursor:
|
if self.busyCursor:
|
||||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||||
|
|
||||||
|
if 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
|
return self
|
||||||
|
|
||||||
def __exit__(self, exType, exValue, exTrace):
|
def __exit__(self, exType, exValue, exTrace):
|
||||||
|
@ -63,6 +91,12 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
return
|
return
|
||||||
if self.busyCursor:
|
if self.busyCursor:
|
||||||
QtGui.QApplication.restoreOverrideCursor()
|
QtGui.QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
|
if self._topDialog is not None:
|
||||||
|
self._topDialog._removeSubDialog(self)
|
||||||
|
|
||||||
|
ProgressDialog.allDialogs.pop(-1)
|
||||||
|
|
||||||
self.setValue(self.maximum())
|
self.setValue(self.maximum())
|
||||||
|
|
||||||
def __iadd__(self, val):
|
def __iadd__(self, val):
|
||||||
|
@ -72,6 +106,94 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
self.setValue(self.value()+val)
|
self.setValue(self.value()+val)
|
||||||
return self
|
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()
|
||||||
|
bar.removed = False
|
||||||
|
|
||||||
|
# where should we insert this widget? Find the first slot with a
|
||||||
|
# "removed" widget (that was left as a placeholder)
|
||||||
|
nw = self.nestedLayout.count()
|
||||||
|
inserted = False
|
||||||
|
if nw > 1:
|
||||||
|
for i in range(1, nw):
|
||||||
|
bar2 = self.nestedLayout.itemAt(i).widget()
|
||||||
|
if bar2.removed:
|
||||||
|
self.nestedLayout.removeWidget(bar2)
|
||||||
|
bar2.hide()
|
||||||
|
bar2.setParent(None)
|
||||||
|
self.nestedLayout.insertWidget(i, bar)
|
||||||
|
inserted = True
|
||||||
|
break
|
||||||
|
if not inserted:
|
||||||
|
self.nestedLayout.addWidget(bar)
|
||||||
|
|
||||||
|
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.layout().setCurrentIndex(1) # causes widgets to be hidden without changing size
|
||||||
|
bar.removed = True # mark as removed so we know we can insert another bar here later
|
||||||
|
|
||||||
|
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._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 a single widget containing all sub-widgets nicely arranged
|
||||||
|
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]
|
||||||
|
|
||||||
|
# join label and bar into a stacked layout so they can be hidden
|
||||||
|
# without changing size
|
||||||
|
sw = QtGui.QWidget()
|
||||||
|
sl = QtGui.QStackedLayout()
|
||||||
|
sw.setLayout(sl)
|
||||||
|
sl.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# inside the stacked layout, the bar and label are in a vbox
|
||||||
|
w = QtGui.QWidget()
|
||||||
|
sl.addWidget(w)
|
||||||
|
l = QtGui.QVBoxLayout()
|
||||||
|
w.setLayout(l)
|
||||||
|
l.addWidget(label)
|
||||||
|
l.addWidget(bar)
|
||||||
|
|
||||||
|
# add a blank page to the stacked layout
|
||||||
|
blank = QtGui.QWidget()
|
||||||
|
sl.addWidget(blank)
|
||||||
|
|
||||||
|
self._nestableWidgets = (sw, btn)
|
||||||
|
|
||||||
|
return self._nestableWidgets
|
||||||
|
|
||||||
## wrap all other functions to make sure they aren't being called from non-gui threads
|
## wrap all other functions to make sure they aren't being called from non-gui threads
|
||||||
|
|
||||||
|
@ -80,6 +202,11 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
return
|
return
|
||||||
QtGui.QProgressDialog.setValue(self, val)
|
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):
|
def setLabelText(self, val):
|
||||||
if self.disabled:
|
if self.disabled:
|
||||||
return
|
return
|
||||||
|
@ -109,4 +236,3 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||||
if self.disabled:
|
if self.disabled:
|
||||||
return 0
|
return 0
|
||||||
return QtGui.QProgressDialog.minimum(self)
|
return QtGui.QProgressDialog.minimum(self)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user