Skip to content

Commit 7c09d2d

Browse files
Merge pull request #19 from jnsebgosselin/add_processstatusbar
PR: Add process status bar widget
2 parents 49fb410 + 4988820 commit 7c09d2d

File tree

3 files changed

+393
-0
lines changed

3 files changed

+393
-0
lines changed

qtapputils/colors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
RED = '#CC0000'
1414
YELLOW = '#ffa500'
1515
YELLOWLIGHT = '#fcf7b6'
16+
BLUE = '#004C99'
1617

1718
# Copied from matplotlib.colors.CSS4_COLORS.
1819
CSS4_COLORS = {

qtapputils/widgets/statusbar.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# -*- coding: utf-8 -*-
2+
# -----------------------------------------------------------------------------
3+
# Copyright © QtAppUtils Project Contributors
4+
# https://github.com/jnsebgosselin/qtapputils
5+
#
6+
# This file is part of QtAppUtils.
7+
# Licensed under the terms of the MIT License.
8+
# -----------------------------------------------------------------------------
9+
10+
# ---- Third party imports
11+
from qtpy.QtCore import QSize, Qt
12+
from qtpy.QtGui import QIcon
13+
from qtpy.QtWidgets import QGridLayout, QLabel, QWidget, QTextBrowser
14+
import qtawesome as qta
15+
16+
17+
# ---- Local imports
18+
from qtapputils.qthelpers import create_waitspinner
19+
from qtapputils.colors import RED, GREEN, BLUE
20+
21+
22+
class ProcessStatusBar(QWidget):
23+
"""
24+
A status bar that shows the progression status and results of a process.
25+
26+
This class was taken from the Sardes project.
27+
See sardes/widgets/statusbar.py at https://github.com/geo-stack/sardes
28+
"""
29+
HIDDEN = 0
30+
IN_PROGRESS = 1
31+
PROCESS_SUCCEEDED = 2
32+
PROCESS_FAILED = 3
33+
NEED_UPDATE = 4
34+
35+
def __init__(self, parent=None, iconsize=24, ndots=11,
36+
orientation=Qt.Horizontal, spacing=None,
37+
contents_margin: list = None,
38+
hsize_policy='minimum', vsize_policy='minimum',
39+
text_valign='center', icon_valign='center'):
40+
"""
41+
A process status bar including an icon and a label.
42+
43+
Parameters
44+
----------
45+
parent : QWidget, optional
46+
The parent of the progress status bar. The default is None.
47+
iconsize : int, optional
48+
The size of the icon. The default is 24.
49+
ndots : int, optional
50+
Number of dots to use for the spinner icon . The default is 11.
51+
orientation : int, optional
52+
Orientation of the progress status bar. The default is
53+
Qt.Horizontal.
54+
spacing : in, optional
55+
Spacing between the icon and the label. Default to 0 if
56+
orientation is horizontal and to 5 if vertical.
57+
contents_margin : list[int], optional
58+
A list of four integers corresponding to the left, top, right, and
59+
bottom contents margin. The default is 0 on all sides.
60+
hsize_policy : str, optional
61+
An attribute describing horizontal resizing policy. Valid
62+
values are 'minimum' or 'expanding'. This option is ignored
63+
when the orientation of the progress bar is horizontal.
64+
vsize_policy : str, optional
65+
An attribute describing vertical resizing policy. Valid
66+
values are 'minimum', 'expanding', or 'minimum_expanding'.
67+
text_valign : str, optional
68+
The vertical alignment of the text. De default is 'center'.
69+
Valid values are 'top', 'bottom', or 'center'.
70+
icon_valign : str, optional
71+
The vertical alignment of the icon. De default is 'center'.
72+
Valid values are 'top', 'bottom', or 'center'.
73+
"""
74+
super().__init__(parent)
75+
self._status = self.HIDDEN
76+
self._iconsize = iconsize
77+
78+
VALIGN_DICT = {
79+
'center': Qt.AlignVCenter,
80+
'top': Qt.AlignTop,
81+
'bottom': Qt.AlignBottom
82+
}
83+
84+
class LabelBrowser(QTextBrowser):
85+
def text(self):
86+
return self.toPlainText()
87+
88+
def minimumSizeHint(self):
89+
return QLabel().minimumSizeHint()
90+
91+
def sizeHint(self):
92+
return QLabel().sizeHint()
93+
94+
text_valign = VALIGN_DICT[text_valign]
95+
self._label = LabelBrowser()
96+
if orientation == Qt.Horizontal:
97+
self._label.setAlignment(Qt.AlignLeft | text_valign)
98+
else:
99+
self._label.setAlignment(Qt.AlignCenter | text_valign)
100+
self._label.setLineWrapMode(LabelBrowser.WidgetWidth)
101+
self._label.setTextInteractionFlags(
102+
Qt.TextSelectableByMouse | Qt.TextBrowserInteraction)
103+
self._label.setOpenExternalLinks(True)
104+
self._label.setFocusPolicy(Qt.NoFocus)
105+
self._label.setFrameStyle(0)
106+
self._label.setStyleSheet("background-color:transparent;")
107+
108+
self._spinner = create_waitspinner(iconsize, ndots, self)
109+
110+
# Setup status icons.
111+
self._icons = {
112+
'failed': QLabel(),
113+
'success': QLabel(),
114+
'update': QLabel()
115+
}
116+
self.hide_icons()
117+
118+
self.set_icon(
119+
'failed', qta.icon('mdi.alert-circle-outline', color=RED))
120+
self.set_icon(
121+
'success', qta.icon('mdi.check-circle-outline', color=GREEN))
122+
self.set_icon(
123+
'update', qta.icon('mdi.update', color=BLUE))
124+
125+
# Setup layout.
126+
layout = QGridLayout(self)
127+
if contents_margin is None:
128+
contents_margin = [0, 0, 0, 0]
129+
layout.setContentsMargins(*contents_margin)
130+
131+
icon_valign = VALIGN_DICT[icon_valign]
132+
if orientation == Qt.Horizontal:
133+
alignment = Qt.AlignLeft | icon_valign
134+
else:
135+
alignment = Qt.AlignCenter | icon_valign
136+
layout.addWidget(self._spinner, 1, 1, alignment)
137+
for widget in self._icons.values():
138+
layout.addWidget(widget, 1, 1, alignment)
139+
if orientation == Qt.Horizontal:
140+
layout.setColumnMinimumWidth(2, 5)
141+
layout.addWidget(self._label, 1, 3)
142+
if vsize_policy == 'minimum':
143+
layout.setRowStretch(0, 100)
144+
layout.setRowStretch(3, 100)
145+
elif vsize_policy == 'expanding':
146+
layout.setRowStretch(1, 100)
147+
# We ignore 'hsize_policy' when orientation is horizontal.
148+
layout.setColumnStretch(3, 100)
149+
layout.setSpacing(spacing or 0)
150+
else:
151+
layout.addWidget(self._label, 2, 1)
152+
if vsize_policy == 'minimum':
153+
layout.setRowStretch(0, 100)
154+
layout.setRowStretch(4, 100)
155+
elif vsize_policy == 'expanding':
156+
layout.setRowStretch(2, 100)
157+
elif vsize_policy == 'minimum_expanding':
158+
layout.setRowStretch(0, 50)
159+
layout.setRowStretch(2, 100)
160+
layout.setRowStretch(4, 50)
161+
if hsize_policy == 'minimum':
162+
layout.setColumnStretch(0, 100)
163+
layout.setColumnStretch(2, 100)
164+
elif hsize_policy == 'expanding':
165+
layout.setColumnStretch(1, 100)
166+
layout.setSpacing(spacing or 5)
167+
168+
def show_icon(self, icon_name):
169+
"""Show icon named 'icon_name' and hide all other icons."""
170+
self._spinner.hide()
171+
self._spinner.stop()
172+
for name, icon in self._icons.items():
173+
if name == icon_name:
174+
icon.show()
175+
else:
176+
icon.hide()
177+
178+
def hide_icons(self):
179+
"""Hide all icons."""
180+
for icon in self._icons.values():
181+
icon.hide()
182+
183+
def set_icon(self, name: str, icon: QIcon):
184+
"""Set the icon named 'name'."""
185+
self._icons[name].setPixmap(
186+
icon.pixmap(QSize(self._iconsize, self._iconsize))
187+
)
188+
189+
@property
190+
def status(self):
191+
return self._status
192+
193+
def set_label(self, text):
194+
"""Set the text that is displayed next to the spinner."""
195+
self._label.setText(text)
196+
197+
def show_update_icon(self, message=None):
198+
"""Stop and hide the spinner and show an update icon instead."""
199+
self._status = self.NEED_UPDATE
200+
self.show_icon('update')
201+
if message is not None:
202+
self.set_label(message)
203+
204+
def show_fail_icon(self, message=None):
205+
"""Stop and hide the spinner and show a failed icon instead."""
206+
self._status = self.PROCESS_FAILED
207+
self.show_icon('failed')
208+
if message is not None:
209+
self.set_label(message)
210+
211+
def show_sucess_icon(self, message=None):
212+
"""Stop and hide the spinner and show a success icon instead."""
213+
self._status = self.PROCESS_SUCCEEDED
214+
self.show_icon('success')
215+
if message is not None:
216+
self.set_label(message)
217+
218+
def show(self, message=None):
219+
"""Extend Qt method to start the waiting spinner."""
220+
self._status = self.IN_PROGRESS
221+
self._spinner.show()
222+
self.hide_icons()
223+
super().show()
224+
self._spinner.start()
225+
if message is not None:
226+
self.set_label(message)
227+
228+
def hide(self):
229+
"""Extend Qt hide to stop waiting spinner."""
230+
self._status = self.HIDDEN
231+
super().hide()
232+
self._spinner.stop()
233+
234+
235+
if __name__ == '__main__':
236+
import sys
237+
from qtpy.QtWidgets import QWidget
238+
from qtapputils.qthelpers import create_qapplication
239+
240+
app = create_qapplication()
241+
242+
widget = QWidget()
243+
layout = QGridLayout(widget)
244+
245+
pbar = ProcessStatusBar()
246+
pbar.show('This is a demo...')
247+
248+
pbar2 = ProcessStatusBar()
249+
pbar2.show()
250+
pbar2.show_sucess_icon('Success icon demo.')
251+
252+
pbar3 = ProcessStatusBar()
253+
pbar3.show()
254+
pbar3.show_fail_icon('Fail icon demo.')
255+
256+
pbar4 = ProcessStatusBar()
257+
pbar4.show()
258+
pbar4.show_update_icon('Update icon demo.')
259+
260+
layout.addWidget(pbar)
261+
layout.addWidget(pbar2)
262+
layout.addWidget(pbar3)
263+
layout.addWidget(pbar4)
264+
265+
widget.show()
266+
267+
sys.exit(app.exec_())

0 commit comments

Comments
 (0)