From f4c3d88251be17f6ad8bab282d4f7cc17cd74b5e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 15:22:31 -0700 Subject: [PATCH 1/6] Add option to join nested progress dialogs into a single window --- examples/ProgressDialog.py | 53 +++++++++++ pyqtgraph/widgets/ProgressDialog.py | 136 +++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 examples/ProgressDialog.py diff --git a/examples/ProgressDialog.py b/examples/ProgressDialog.py new file mode 100644 index 00000000..08cffa7e --- /dev/null +++ b/examples/ProgressDialog.py @@ -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 + + diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 8c669be4..7c60004b 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -2,6 +2,8 @@ from ..Qt import QtGui, QtCore __all__ = ['ProgressDialog'] + + class ProgressDialog(QtGui.QProgressDialog): """ Extends QProgressDialog for use in 'with' statements. @@ -14,7 +16,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,6 +34,9 @@ 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 (if + any). ============== ================================================================ """ isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() @@ -42,20 +50,40 @@ 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) - + # attributes used for nesting dialogs + self.nestedLayout = None + self._nestableWidgets = None + self._nestingReady = False + self._topDialog = None + def __enter__(self): if self.disabled: return self if self.busyCursor: 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 def __exit__(self, exType, exValue, exTrace): @@ -63,6 +91,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 +106,94 @@ 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() + 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 @@ -80,6 +202,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 +236,3 @@ class ProgressDialog(QtGui.QProgressDialog): if self.disabled: return 0 return QtGui.QProgressDialog.minimum(self) - From e6507f860176ad6d3ce5d68e425afb8bc3de610e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 17:14:32 -0700 Subject: [PATCH 2/6] try a different approach to managing nested bars.. --- pyqtgraph/widgets/ProgressDialog.py | 99 +++++++++++++++++------------ 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 7c60004b..4964771d 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -39,6 +39,13 @@ class ProgressDialog(QtGui.QProgressDialog): any). ============== ================================================================ """ + # attributes used for nesting dialogs + self.nestedLayout = None + self._nestableWidgets = None + self._nestingReady = False + self._topDialog = None + self._subBars = [] + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() self.disabled = disable or (not isGuiThread) if self.disabled: @@ -63,12 +70,6 @@ class ProgressDialog(QtGui.QProgressDialog): self.setValue(self.minimum()) if noCancel: self.setCancelButton(None) - - # attributes used for nesting dialogs - self.nestedLayout = None - self._nestableWidgets = None - self._nestingReady = False - self._topDialog = None def __enter__(self): if self.disabled: @@ -113,31 +114,32 @@ class ProgressDialog(QtGui.QProgressDialog): 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 + 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.nestedLayout.addWidget(bar) - + 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.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 + bar.hide() def _prepareNesting(self): # extract all child widgets and place into a new layout that we can add to @@ -156,6 +158,7 @@ class ProgressDialog(QtGui.QProgressDialog): # 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) @@ -165,31 +168,17 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestingReady = True def _extractWidgets(self): - # return a single widget containing all sub-widgets nicely arranged + # 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] - # 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) + sw = ProgressWidget(label, bar) self._nestableWidgets = (sw, btn) @@ -202,6 +191,11 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) + if self._topDialog is not None: + tbar = self._topDialog._extractWidgets()[0].bar + tlab = self._topDialog._extractWidgets()[0].label + print(tlab.pos(), tbar.pos()) + # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModal: @@ -236,3 +230,26 @@ class ProgressDialog(QtGui.QProgressDialog): if self.disabled: return 0 return QtGui.QProgressDialog.minimum(self) + + +class ProgressWidget(QtGui.QWidget): + 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 From 7c1a6ecb1afcf4f1012c367380d7195732368380 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:01:51 -0700 Subject: [PATCH 3/6] Prevent dialog from moving label/bar widgets on resize when nested --- examples/ProgressDialog.py | 4 ++-- pyqtgraph/widgets/ProgressDialog.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/ProgressDialog.py b/examples/ProgressDialog.py index 08cffa7e..141d2bb4 100644 --- a/examples/ProgressDialog.py +++ b/examples/ProgressDialog.py @@ -13,11 +13,11 @@ app = QtGui.QApplication([]) def runStage(i): - """Waste time for 3 seconds while incrementing a progress bar. + """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.03) + time.sleep(0.02) dlg += 1 if dlg.wasCanceled(): print("Canceled stage %s" % i) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 4964771d..de3c6dc4 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -183,6 +183,12 @@ class ProgressDialog(QtGui.QProgressDialog): 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 From 384975dd464b0daa6d93dc26ec8859c24941ca79 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:03:08 -0700 Subject: [PATCH 4/6] Cleanup --- pyqtgraph/widgets/ProgressDialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index de3c6dc4..e62a6551 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -200,7 +200,6 @@ class ProgressDialog(QtGui.QProgressDialog): if self._topDialog is not None: tbar = self._topDialog._extractWidgets()[0].bar tlab = self._topDialog._extractWidgets()[0].label - print(tlab.pos(), tbar.pos()) # Qt docs say this should happen automatically, but that doesn't seem # to be the case. From d2942c7acadef3910311c21b5e72cb3cbd854aa3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:06:05 -0700 Subject: [PATCH 5/6] Fix: obey nested option --- pyqtgraph/widgets/ProgressDialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index e62a6551..6bda4b95 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -45,6 +45,7 @@ class ProgressDialog(QtGui.QProgressDialog): 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) @@ -77,7 +78,7 @@ class ProgressDialog(QtGui.QProgressDialog): if self.busyCursor: QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) - if len(ProgressDialog.allDialogs) > 0: + if self.nested and len(ProgressDialog.allDialogs) > 0: topDialog = ProgressDialog.allDialogs[0] topDialog._addSubDialog(self) self._topDialog = topDialog From e2c991851031653ea53ab0600e1409f4f093663b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:11:44 -0700 Subject: [PATCH 6/6] docs cleanup --- pyqtgraph/widgets/ProgressDialog.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 6bda4b95..ae1826bb 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -6,7 +6,10 @@ __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:: @@ -35,8 +38,7 @@ class ProgressDialog(QtGui.QProgressDialog): 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 (if - any). + any pre-existing progress dialogs that also allow nesting. ============== ================================================================ """ # attributes used for nesting dialogs @@ -198,10 +200,6 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) - if self._topDialog is not None: - tbar = self._topDialog._extractWidgets()[0].bar - tlab = self._topDialog._extractWidgets()[0].label - # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModal: @@ -239,6 +237,9 @@ class ProgressDialog(QtGui.QProgressDialog): 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