Skip to content

Commit b00ef27

Browse files
committed
Add toggle widget.
1 parent a2ebf42 commit b00ef27

File tree

6 files changed

+287
-6
lines changed

6 files changed

+287
-6
lines changed

qtwidgets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from .palette import PaletteGrid, PaletteHorizontal, PaletteVertical
1111
# from scrubber
1212
# from stopwatch
13+
from .toggle import Toggle, AnimatedToggle

qtwidgets/stopwatch/demo_pyside2.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
from PySide2 import QtCore, QtGui, QtWidgets
2-
from palette import Palette
2+
from toggle import Toggle, AnimatedToggle
33

44

55
class Window(QtWidgets.QMainWindow):
66

77
def __init__(self):
88
super().__init__()
99

10-
palette = Palette('paired12')
11-
palette.selected.connect(self.show_selected_color)
12-
self.setCentralWidget(palette)
10+
toggle_1 = AnimatedToggle()
11+
toggle_2 = AnimatedToggle(
12+
checked_color="#FFB000",
13+
pulse_checked_color="#44FFB000"
14+
)
1315

14-
def show_selected_color(self, c):
15-
print("Selected: {}".format(c))
16+
container = QtWidgets.QWidget()
17+
layout = QtWidgets.QVBoxLayout()
18+
layout.addWidget(toggle_1)
19+
layout.addWidget(toggle_2)
20+
container.setLayout(layout)
21+
22+
self.setCentralWidget(container)
1623

1724

1825
app = QtWidgets.QApplication([])

qtwidgets/toggle/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .toggle import Toggle, AnimatedToggle

qtwidgets/toggle/demo_pyqt5.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import PyQt5
2+
from PyQt5 import QtCore, QtGui, QtWidgets
3+
from toggle import Toggle, AnimatedToggle
4+
5+
6+
class Window(QtWidgets.QMainWindow):
7+
8+
def __init__(self):
9+
super().__init__()
10+
11+
toggle_1 = Toggle()
12+
toggle_2 = AnimatedToggle(
13+
checked_color="#FFB000",
14+
pulse_checked_color="#44FFB000"
15+
)
16+
17+
container = QtWidgets.QWidget()
18+
layout = QtWidgets.QVBoxLayout()
19+
layout.addWidget(toggle_1)
20+
layout.addWidget(toggle_2)
21+
container.setLayout(layout)
22+
23+
self.setCentralWidget(container)
24+
25+
26+
app = QtWidgets.QApplication([])
27+
w = Window()
28+
w.show()
29+
app.exec_()
30+
31+
32+
33+

qtwidgets/toggle/demo_pyside2.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import PySide2
2+
from PySide2 import QtCore, QtGui, QtWidgets
3+
from toggle import Toggle, AnimatedToggle
4+
5+
6+
class Window(QtWidgets.QMainWindow):
7+
8+
def __init__(self):
9+
super().__init__()
10+
11+
toggle_1 = Toggle()
12+
toggle_2 = AnimatedToggle(
13+
checked_color="#FFB000",
14+
pulse_checked_color="#44FFB000"
15+
)
16+
17+
container = QtWidgets.QWidget()
18+
layout = QtWidgets.QVBoxLayout()
19+
layout.addWidget(toggle_1)
20+
layout.addWidget(toggle_2)
21+
container.setLayout(layout)
22+
23+
self.setCentralWidget(container)
24+
25+
26+
app = QtWidgets.QApplication([])
27+
w = Window()
28+
w.show()
29+
app.exec_()
30+
31+
32+
33+

qtwidgets/toggle/toggle.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import sys
2+
3+
if 'PyQt5' in sys.modules:
4+
from PyQt5.QtCore import (
5+
Qt, QSize, QPoint, QPointF, QRectF,
6+
QEasingCurve, QPropertyAnimation, QSequentialAnimationGroup,
7+
pyqtSlot, pyqtProperty)
8+
from PyQt5.QtWidgets import QCheckBox
9+
from PyQt5.QtGui import QColor, QBrush, QPaintEvent, QPen, QPainter
10+
11+
from PyQt5.QtCore import pyqtSlot as Slot, pyqtProperty as Property
12+
13+
else:
14+
from PySide2.QtCore import (
15+
Qt, QSize, QPoint, QPointF, QRectF,
16+
QEasingCurve, QPropertyAnimation, QSequentialAnimationGroup,
17+
Slot, Property)
18+
19+
from PySide2.QtWidgets import QCheckBox
20+
from PySide2.QtGui import QColor, QBrush, QPaintEvent, QPen, QPainter
21+
22+
23+
24+
class Toggle(QCheckBox):
25+
26+
_transparent_pen = QPen(Qt.transparent)
27+
_light_grey_pen = QPen(Qt.lightGray)
28+
29+
def __init__(self,
30+
parent=None,
31+
bar_color=Qt.gray,
32+
checked_color="#00B0FF",
33+
handle_color=Qt.white,
34+
):
35+
super().__init__(parent)
36+
37+
# Save our properties on the object via self, so we can access them later
38+
# in the paintEvent.
39+
self._bar_brush = QBrush(bar_color)
40+
self._bar_checked_brush = QBrush(QColor(checked_color).lighter())
41+
42+
self._handle_brush = QBrush(handle_color)
43+
self._handle_checked_brush = QBrush(QColor(checked_color))
44+
45+
# Setup the rest of the widget.
46+
47+
self.setContentsMargins(8, 0, 8, 0)
48+
self._handle_position = 0
49+
50+
self.stateChanged.connect(self.handle_state_change)
51+
52+
def sizeHint(self):
53+
return QSize(58, 45)
54+
55+
def hitButton(self, pos: QPoint):
56+
return self.contentsRect().contains(pos)
57+
58+
def paintEvent(self, e: QPaintEvent):
59+
60+
contRect = self.contentsRect()
61+
handleRadius = round(0.24 * contRect.height())
62+
63+
p = QPainter(self)
64+
p.setRenderHint(QPainter.Antialiasing)
65+
66+
p.setPen(self._transparent_pen)
67+
barRect = QRectF(
68+
0, 0,
69+
contRect.width() - handleRadius, 0.40 * contRect.height()
70+
)
71+
barRect.moveCenter(contRect.center())
72+
rounding = barRect.height() / 2
73+
74+
# the handle will move along this line
75+
trailLength = contRect.width() - 2 * handleRadius
76+
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
77+
78+
if self.isChecked():
79+
p.setBrush(self._bar_checked_brush)
80+
p.drawRoundedRect(barRect, rounding, rounding)
81+
p.setBrush(self._handle_checked_brush)
82+
83+
else:
84+
p.setBrush(self._bar_brush)
85+
p.drawRoundedRect(barRect, rounding, rounding)
86+
p.setPen(self._light_grey_pen)
87+
p.setBrush(self._handle_brush)
88+
89+
p.drawEllipse(
90+
QPointF(xPos, barRect.center().y()),
91+
handleRadius, handleRadius)
92+
93+
p.end()
94+
95+
@Slot(int)
96+
def handle_state_change(self, value):
97+
self._handle_position = 1 if value else 0
98+
99+
@Property(float)
100+
def handle_position(self):
101+
return self._handle_position
102+
103+
@handle_position.setter
104+
def handle_position(self, pos):
105+
"""change the property
106+
we need to trigger QWidget.update() method, either by:
107+
1- calling it here [ what we're doing ].
108+
2- connecting the QPropertyAnimation.valueChanged() signal to it.
109+
"""
110+
self._handle_position = pos
111+
self.update()
112+
113+
@Property(float)
114+
def pulse_radius(self):
115+
return self._pulse_radius
116+
117+
@pulse_radius.setter
118+
def pulse_radius(self, pos):
119+
self._pulse_radius = pos
120+
self.update()
121+
122+
123+
124+
class AnimatedToggle(Toggle):
125+
126+
_transparent_pen = QPen(Qt.transparent)
127+
_light_grey_pen = QPen(Qt.lightGray)
128+
129+
def __init__(self, *args, pulse_unchecked_color="#44999999",
130+
pulse_checked_color="#4400B0EE", **kwargs):
131+
132+
self._pulse_radius = 0
133+
134+
super().__init__(*args, **kwargs)
135+
136+
self.animation = QPropertyAnimation(self, b"handle_position", self)
137+
self.animation.setEasingCurve(QEasingCurve.InOutCubic)
138+
self.animation.setDuration(200) # time in ms
139+
140+
self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
141+
self.pulse_anim.setDuration(350) # time in ms
142+
self.pulse_anim.setStartValue(10)
143+
self.pulse_anim.setEndValue(20)
144+
145+
self.animations_group = QSequentialAnimationGroup()
146+
self.animations_group.addAnimation(self.animation)
147+
self.animations_group.addAnimation(self.pulse_anim)
148+
149+
self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
150+
self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))
151+
152+
153+
154+
@Slot(int)
155+
def handle_state_change(self, value):
156+
self.animations_group.stop()
157+
if value:
158+
self.animation.setEndValue(1)
159+
else:
160+
self.animation.setEndValue(0)
161+
self.animations_group.start()
162+
163+
def paintEvent(self, e: QPaintEvent):
164+
165+
contRect = self.contentsRect()
166+
handleRadius = round(0.24 * contRect.height())
167+
168+
p = QPainter(self)
169+
p.setRenderHint(QPainter.Antialiasing)
170+
171+
p.setPen(self._transparent_pen)
172+
barRect = QRectF(
173+
0, 0,
174+
contRect.width() - handleRadius, 0.40 * contRect.height()
175+
)
176+
barRect.moveCenter(contRect.center())
177+
rounding = barRect.height() / 2
178+
179+
# the handle will move along this line
180+
trailLength = contRect.width() - 2 * handleRadius
181+
182+
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
183+
184+
if self.pulse_anim.state() == QPropertyAnimation.Running:
185+
p.setBrush(
186+
self._pulse_checked_animation if
187+
self.isChecked() else self._pulse_unchecked_animation)
188+
p.drawEllipse(QPointF(xPos, barRect.center().y()),
189+
self._pulse_radius, self._pulse_radius)
190+
191+
if self.isChecked():
192+
p.setBrush(self._bar_checked_brush)
193+
p.drawRoundedRect(barRect, rounding, rounding)
194+
p.setBrush(self._handle_checked_brush)
195+
196+
else:
197+
p.setBrush(self._bar_brush)
198+
p.drawRoundedRect(barRect, rounding, rounding)
199+
p.setPen(self._light_grey_pen)
200+
p.setBrush(self._handle_brush)
201+
202+
p.drawEllipse(
203+
QPointF(xPos, barRect.center().y()),
204+
handleRadius, handleRadius)
205+
206+
p.end()

0 commit comments

Comments
 (0)