diff --git a/bec_widgets/widgets/toggle/__init__.py b/bec_widgets/widgets/toggle/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/widgets/toggle/register_toggle_switch.py b/bec_widgets/widgets/toggle/register_toggle_switch.py new file mode 100644 index 00000000..fceca2de --- /dev/null +++ b/bec_widgets/widgets/toggle/register_toggle_switch.py @@ -0,0 +1,15 @@ +def main(): # pragma: no cover + from qtpy import PYSIDE6 + + if not PYSIDE6: + print("PYSIDE6 is not available in the environment. Cannot patch designer.") + return + from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + + from bec_widgets.widgets.toggle.toggle_switch_plugin import ToggleSwitchPlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(ToggleSwitchPlugin()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/bec_widgets/widgets/toggle/toggle.py b/bec_widgets/widgets/toggle/toggle.py new file mode 100644 index 00000000..400084d0 --- /dev/null +++ b/bec_widgets/widgets/toggle/toggle.py @@ -0,0 +1,149 @@ +import sys + +from qtpy.QtCore import Property, QEasingCurve, QPointF, QPropertyAnimation, Qt +from qtpy.QtGui import QColor, QPainter +from qtpy.QtWidgets import QApplication, QWidget + + +class ToggleSwitch(QWidget): + """ + A simple toggle. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self.setFixedSize(40, 21) + + self._thumb_pos = QPointF(3, 2) # Use QPointF for the thumb position + self._active_track_color = QColor(33, 150, 243) + self._active_thumb_color = QColor(255, 255, 255) + self._inactive_track_color = QColor(200, 200, 200) + self._inactive_thumb_color = QColor(255, 255, 255) + + self._checked = False + self._track_color = self.inactive_track_color + self._thumb_color = self.inactive_thumb_color + + self._animation = QPropertyAnimation(self, b"thumb_pos") + self._animation.setDuration(200) + self._animation.setEasingCurve(QEasingCurve.Type.OutBack) + self.setProperty("checked", True) + + @Property(bool) + def checked(self): + """ + The checked state of the toggle switch. + """ + return self._checked + + @checked.setter + def checked(self, state): + self._checked = state + self.update_colors() + self.set_thumb_pos_to_state() + + @Property(QPointF) + def thumb_pos(self): + return self._thumb_pos + + @thumb_pos.setter + def thumb_pos(self, pos): + self._thumb_pos = pos + self.update() + + @Property(QColor) + def active_track_color(self): + return self._active_track_color + + @active_track_color.setter + def active_track_color(self, color): + self._active_track_color = color + self.update_colors() + self.update() + + @Property(QColor) + def active_thumb_color(self): + return self._active_thumb_color + + @active_thumb_color.setter + def active_thumb_color(self, color): + self._active_thumb_color = color + self.update_colors() + self.update() + + @Property(QColor) + def inactive_track_color(self): + return self._inactive_track_color + + @inactive_track_color.setter + def inactive_track_color(self, color): + self._inactive_track_color = color + self.update_colors() + self.update() + + @Property(QColor) + def inactive_thumb_color(self): + return self._inactive_thumb_color + + @inactive_thumb_color.setter + def inactive_thumb_color(self, color): + self._inactive_thumb_color = color + self.update_colors() + self.update() + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + # Draw track + painter.setBrush(self._track_color) + painter.setPen(Qt.NoPen) + painter.drawRoundedRect( + 0, 0, self.width(), self.height(), self.height() / 2, self.height() / 2 + ) + + # Draw thumb + painter.setBrush(self._thumb_color) + diameter = int(self.height() * 0.8) + painter.drawEllipse(int(self._thumb_pos.x()), int(self._thumb_pos.y()), diameter, diameter) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self._checked = not self._checked + self.update_colors() + self.animate_thumb() + + def update_colors(self): + + self._thumb_color = self.active_thumb_color if self._checked else self.inactive_thumb_color + self._track_color = self.active_track_color if self._checked else self.inactive_track_color + + def get_thumb_pos(self, checked): + return QPointF(self.width() - self.height() + 3, 2) if checked else QPointF(3, 2) + + def set_thumb_pos_to_state(self): + # this is to avoid that linter complains about the thumb_pos setter + self.setProperty("thumb_pos", self.get_thumb_pos(self._checked)) + self.update_colors() + + def animate_thumb(self): + start_pos = self.thumb_pos + end_pos = self.get_thumb_pos(self._checked) + + self._animation.stop() + self._animation.setStartValue(start_pos) + self._animation.setEndValue(end_pos) + self._animation.start() + + def sizeHint(self): + return self.minimumSizeHint() + + def minimumSizeHint(self): + return self.size() + + +if __name__ == "__main__": # pragma: no cover + app = QApplication(sys.argv) + window = ToggleSwitch() + window.show() + sys.exit(app.exec()) diff --git a/bec_widgets/widgets/toggle/toggle_switch.pyproject b/bec_widgets/widgets/toggle/toggle_switch.pyproject new file mode 100644 index 00000000..dde67800 --- /dev/null +++ b/bec_widgets/widgets/toggle/toggle_switch.pyproject @@ -0,0 +1 @@ +{'files': ['toggle.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/toggle/toggle_switch_plugin.py b/bec_widgets/widgets/toggle/toggle_switch_plugin.py new file mode 100644 index 00000000..e476393d --- /dev/null +++ b/bec_widgets/widgets/toggle/toggle_switch_plugin.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from qtpy.QtDesigner import QDesignerCustomWidgetInterface +from qtpy.QtGui import QIcon + +from bec_widgets.widgets.toggle.toggle import ToggleSwitch + +DOM_XML = """ + + + + +""" + + +class ToggleSwitchPlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + t = ToggleSwitch(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "" + + def icon(self): + return QIcon() + + def includeFile(self): + return "toggle_switch" + + def initialize(self, form_editor): + self._form_editor = form_editor + + def isContainer(self): + return False + + def isInitialized(self): + return self._form_editor is not None + + def name(self): + return "ToggleSwitch" + + def toolTip(self): + return "ToggleSwitch" + + def whatsThis(self): + return self.toolTip() diff --git a/tests/unit_tests/test_toggle.py b/tests/unit_tests/test_toggle.py new file mode 100644 index 00000000..d840a6d3 --- /dev/null +++ b/tests/unit_tests/test_toggle.py @@ -0,0 +1,38 @@ +import pytest +from qtpy.QtCore import QPointF, Qt + +from bec_widgets.widgets.toggle.toggle import ToggleSwitch + + +@pytest.fixture +def toggle(qtbot): + widget = ToggleSwitch() + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + + +def test_toggle(toggle): + toggle.checked = False + assert toggle.checked is False + + assert toggle.thumb_pos == QPointF(3, 2) + + toggle.checked = True + assert toggle.checked is True + + assert toggle.thumb_pos == QPointF(22, 2) + + +def test_toggle_click(qtbot, toggle): + init_state = toggle.checked + + qtbot.mouseClick(toggle, Qt.LeftButton) + toggle.paintEvent(None) + assert toggle.checked is not init_state + + init_state = toggle.checked + + qtbot.mouseClick(toggle, Qt.LeftButton) + toggle.paintEvent(None) + assert toggle.checked is not init_state