File: ProgressDialog.py

package info (click to toggle)
python-pyqtgraph 0.13.1-4
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 6,520 kB
  • sloc: python: 52,773; makefile: 115; ansic: 40; sh: 2
file content (268 lines) | stat: -rw-r--r-- 9,627 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
from time import perf_counter

from ..Qt import QtCore, QtGui, QtWidgets

__all__ = ['ProgressDialog']


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 = []
    
    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