Coverage for src / ezqt_widgets / widgets / misc / toggle_switch.py: 83.19%
111 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-31 10:03 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-31 10:03 +0000
1# ///////////////////////////////////////////////////////////////
2# TOGGLE_SWITCH - Toggle Switch Widget
3# Project: ezqt_widgets
4# ///////////////////////////////////////////////////////////////
6"""
7Toggle switch widget module.
9Provides a modern toggle switch widget with animated sliding circle for
10PySide6 applications.
11"""
13from __future__ import annotations
15# ///////////////////////////////////////////////////////////////
16# IMPORTS
17# ///////////////////////////////////////////////////////////////
18# Standard library imports
19from typing import Any
21# Third-party imports
22from PySide6.QtCore import (
23 Property,
24 QEasingCurve,
25 QPropertyAnimation,
26 QRect,
27 QSize,
28 Qt,
29 Signal,
30)
31from PySide6.QtGui import QBrush, QColor, QMouseEvent, QPainter, QPaintEvent, QPen
32from PySide6.QtWidgets import QSizePolicy, QWidget
34# Local imports
35from ...types import WidgetParent
37# ///////////////////////////////////////////////////////////////
38# CLASSES
39# ///////////////////////////////////////////////////////////////
42class ToggleSwitch(QWidget):
43 """Modern toggle switch widget with animated sliding circle.
45 Features:
46 - Smooth animation when toggling
47 - Customizable colors for on/off states
48 - Configurable size and border radius
49 - Click to toggle functionality
50 - Property-based access to state
51 - Signal emitted on state change
53 Args:
54 parent: The parent widget (default: None).
55 checked: Initial state of the toggle (default: False).
56 width: Width of the toggle switch (default: 50).
57 height: Height of the toggle switch (default: 24).
58 animation: Whether to animate the toggle (default: True).
59 *args: Additional arguments passed to QWidget.
60 **kwargs: Additional keyword arguments passed to QWidget.
62 Signals:
63 toggled(bool): Emitted when the toggle state changes.
65 Example:
66 >>> from ezqt_widgets import ToggleSwitch
67 >>> switch = ToggleSwitch(checked=False, width=50, height=24)
68 >>> switch.toggled.connect(lambda state: print(f"On: {state}"))
69 >>> switch.checked = True
70 >>> switch.show()
71 """
73 toggled = Signal(bool)
75 # ///////////////////////////////////////////////////////////////
76 # INIT
77 # ///////////////////////////////////////////////////////////////
79 def __init__(
80 self,
81 parent: WidgetParent = None,
82 checked: bool = False,
83 width: int = 50,
84 height: int = 24,
85 animation: bool = True,
86 *args: Any,
87 **kwargs: Any,
88 ) -> None:
89 """Initialize the toggle switch."""
90 super().__init__(parent, *args, **kwargs)
92 # Initialize properties
93 self._checked: bool = checked
94 self._width: int = width
95 self._height: int = height
96 self._animation: bool = animation
97 self._circle_radius: int = (height - 4) // 2 # Circle radius with 2px margin
98 self._animation_duration: int = 200
100 # Colors
101 self._bg_color_off: QColor = QColor(44, 49, 58) # Default dark theme
102 self._bg_color_on: QColor = QColor(150, 205, 50) # Default accent color
103 self._circle_color: QColor = QColor(255, 255, 255)
104 self._border_color: QColor = QColor(52, 59, 72)
106 # Initialize position
107 self._circle_position: int = self._get_circle_position()
109 # Setup animation
110 self._setup_animation()
112 # Setup widget
113 self._setup_widget()
115 # ------------------------------------------------
116 # PRIVATE METHODS
117 # ------------------------------------------------
119 def _setup_animation(self) -> None:
120 """Setup the animation system."""
121 self._animation_obj = QPropertyAnimation(self, b"circle_position")
122 self._animation_obj.setDuration(self._animation_duration)
123 self._animation_obj.setEasingCurve(QEasingCurve.Type.InOutQuart)
125 def _setup_widget(self) -> None:
126 """Setup the widget properties."""
127 self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
128 self.setFixedSize(self._width, self._height)
129 self.setCursor(Qt.CursorShape.PointingHandCursor)
131 def _get_circle_position(self) -> int:
132 """Calculate circle position based on state.
134 Returns:
135 The circle position in pixels.
136 """
137 if self._checked:
138 return self._width - self._height + 2 # Right position
139 else:
140 return 2 # Left position
142 def _get_circle_position_property(self) -> int:
143 """Property getter for animation.
145 Returns:
146 The current circle position.
147 """
148 return self._circle_position
150 def _set_circle_position_property(self, position: int) -> None:
151 """Property setter for animation.
153 Args:
154 position: The new circle position.
155 """
156 self._circle_position = position
157 self.update()
159 # Property for animation
160 circle_position = Property(
161 int, _get_circle_position_property, _set_circle_position_property
162 )
164 # ///////////////////////////////////////////////////////////////
165 # PROPERTIES
166 # ///////////////////////////////////////////////////////////////
168 @property
169 def checked(self) -> bool:
170 """Get the toggle state.
172 Returns:
173 True if checked, False otherwise.
174 """
175 return self._checked
177 @checked.setter
178 def checked(self, value: bool) -> None:
179 """Set the toggle state.
181 Args:
182 value: The new toggle state.
183 """
184 if value != self._checked: 184 ↛ exitline 184 didn't return from function 'checked' because the condition on line 184 was always true
185 self._checked = bool(value)
186 if self._animation: 186 ↛ 189line 186 didn't jump to line 189 because the condition on line 186 was always true
187 self._animate_circle()
188 else:
189 self._circle_position = self._get_circle_position()
190 self.update()
191 self.toggled.emit(self._checked)
193 @property
194 def width(self) -> int:
195 """Get the width of the toggle.
197 Returns:
198 The current width in pixels.
199 """
200 return self._width
202 @width.setter
203 def width(self, value: int) -> None:
204 """Set the width of the toggle.
206 Args:
207 value: The new width in pixels.
208 """
209 self._width = max(20, int(value))
210 self._circle_radius = (self._height - 4) // 2
211 self.setFixedSize(self._width, self._height)
212 self._circle_position = self._get_circle_position()
213 self.update()
215 @property
216 def height(self) -> int:
217 """Get the height of the toggle.
219 Returns:
220 The current height in pixels.
221 """
222 return self._height
224 @height.setter
225 def height(self, value: int) -> None:
226 """Set the height of the toggle.
228 Args:
229 value: The new height in pixels.
230 """
231 self._height = max(12, int(value))
232 self._circle_radius = (self._height - 4) // 2
233 self.setFixedSize(self._width, self._height)
234 self._circle_position = self._get_circle_position()
235 self.update()
237 @property
238 def animation(self) -> bool:
239 """Get whether animation is enabled.
241 Returns:
242 True if animation is enabled, False otherwise.
243 """
244 return self._animation
246 @animation.setter
247 def animation(self, value: bool) -> None:
248 """Set whether animation is enabled.
250 Args:
251 value: Whether to enable animation.
252 """
253 self._animation = bool(value)
255 # ///////////////////////////////////////////////////////////////
256 # PUBLIC METHODS
257 # ///////////////////////////////////////////////////////////////
259 def toggle(self) -> None:
260 """Toggle the switch state."""
261 self.checked = not self._checked
263 # ------------------------------------------------
264 # PRIVATE METHODS
265 # ------------------------------------------------
267 def _animate_circle(self) -> None:
268 """Animate the circle movement."""
269 target_position = self._get_circle_position()
270 self._animation_obj.setStartValue(self._circle_position)
271 self._animation_obj.setEndValue(target_position)
272 self._animation_obj.start()
274 # ///////////////////////////////////////////////////////////////
275 # EVENT HANDLERS
276 # ///////////////////////////////////////////////////////////////
278 def mousePressEvent(self, event: QMouseEvent) -> None:
279 """Handle mouse press events.
281 Args:
282 event: The mouse event.
283 """
284 if event.button() == Qt.MouseButton.LeftButton: 284 ↛ exitline 284 didn't return from function 'mousePressEvent' because the condition on line 284 was always true
285 self.toggle()
287 def paintEvent(self, _event: QPaintEvent) -> None:
288 """Custom paint event to draw the toggle switch.
290 Args:
291 _event: The paint event (unused but required by signature).
292 """
293 painter = QPainter(self)
294 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
296 # Draw background
297 bg_color = self._bg_color_on if self._checked else self._bg_color_off
298 painter.setPen(QPen(self._border_color, 1))
299 painter.setBrush(QBrush(bg_color))
300 painter.drawRoundedRect(
301 0,
302 0,
303 self._width,
304 self._height,
305 self._height // 2,
306 self._height // 2,
307 )
309 # Draw circle
310 circle_x = self._circle_position
311 circle_y = (self._height - self._circle_radius * 2) // 2
312 circle_rect = QRect(
313 circle_x, circle_y, self._circle_radius * 2, self._circle_radius * 2
314 )
316 painter.setPen(Qt.PenStyle.NoPen)
317 painter.setBrush(QBrush(self._circle_color))
318 painter.drawEllipse(
319 circle_x, circle_y, circle_rect.width(), circle_rect.height()
320 )
322 # ///////////////////////////////////////////////////////////////
323 # OVERRIDE METHODS
324 # ///////////////////////////////////////////////////////////////
326 def sizeHint(self) -> QSize:
327 """Return the recommended size for the widget.
329 Returns:
330 The recommended size.
331 """
332 return QSize(self._width, self._height)
334 def minimumSizeHint(self) -> QSize:
335 """Return the minimum size for the widget.
337 Returns:
338 The minimum size hint.
339 """
340 return QSize(self._width, self._height)
342 # ///////////////////////////////////////////////////////////////
343 # STYLE METHODS
344 # ///////////////////////////////////////////////////////////////
346 def refreshStyle(self) -> None:
347 """Refresh the widget style.
349 Useful after dynamic stylesheet changes.
350 """
351 self.style().unpolish(self)
352 self.style().polish(self)
353 self.update()
356# ///////////////////////////////////////////////////////////////
357# PUBLIC API
358# ///////////////////////////////////////////////////////////////
360__all__ = ["ToggleSwitch"]