diff --git a/bec_widgets/widgets/utility/spinbox/__init__.py b/bec_widgets/widgets/utility/spinbox/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/widgets/utility/spinbox/bec_spin_box.pyproject b/bec_widgets/widgets/utility/spinbox/bec_spin_box.pyproject new file mode 100644 index 00000000..0fa4ab1b --- /dev/null +++ b/bec_widgets/widgets/utility/spinbox/bec_spin_box.pyproject @@ -0,0 +1 @@ +{'files': ['decimal_spinbox.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/utility/spinbox/bec_spin_box_plugin.py b/bec_widgets/widgets/utility/spinbox/bec_spin_box_plugin.py new file mode 100644 index 00000000..0276ec5d --- /dev/null +++ b/bec_widgets/widgets/utility/spinbox/bec_spin_box_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 bec_widgets.utils.bec_designer import designer_material_icon +from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox + +DOM_XML = """ + + + + +""" + + +class BECSpinBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + t = BECSpinBox(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "" + + def icon(self): + return designer_material_icon(BECSpinBox.ICON_NAME) + + def includeFile(self): + return "bec_spin_box" + + 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 "BECSpinBox" + + def toolTip(self): + return "BECSpinBox" + + def whatsThis(self): + return self.toolTip() diff --git a/bec_widgets/widgets/utility/spinbox/decimal_spinbox.py b/bec_widgets/widgets/utility/spinbox/decimal_spinbox.py new file mode 100644 index 00000000..9990f3cb --- /dev/null +++ b/bec_widgets/widgets/utility/spinbox/decimal_spinbox.py @@ -0,0 +1,83 @@ +import sys + +from bec_qthemes import material_icon +from qtpy.QtGui import Qt +from qtpy.QtWidgets import ( + QApplication, + QDoubleSpinBox, + QInputDialog, + QSizePolicy, + QToolButton, + QWidget, +) + +from bec_widgets.utils import ConnectionConfig +from bec_widgets.utils.bec_widget import BECWidget + + +class BECSpinBox(BECWidget, QDoubleSpinBox): + PLUGIN = True + RPC = False + ICON_NAME = "123" + + def __init__( + self, + parent: QWidget | None = None, + config: ConnectionConfig | None = None, + client=None, + gui_id: str | None = None, + ) -> None: + if config is None: + config = ConnectionConfig(widget_class=self.__class__.__name__) + super().__init__(client=client, gui_id=gui_id, config=config) + QDoubleSpinBox.__init__(self, parent=parent) + + self.setObjectName("BECSpinBox") + # Make the widget as compact as possible horizontally. + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + self.setAlignment(Qt.AlignHCenter) + + # Configure default QDoubleSpinBox settings. + self.setRange(-2147483647, 2147483647) + self.setDecimals(2) + + # Create an embedded settings button. + self.setting_button = QToolButton(self) + self.setting_button.setIcon(material_icon("settings")) + self.setting_button.setToolTip("Set number of decimals") + self.setting_button.setCursor(Qt.PointingHandCursor) + self.setting_button.setFocusPolicy(Qt.NoFocus) + self.setting_button.setStyleSheet("QToolButton { border: none; padding: 0px; }") + + self.setting_button.clicked.connect(self.change_decimals) + + self._button_size = 12 + self._arrow_width = 20 + + def resizeEvent(self, event): + super().resizeEvent(event) + arrow_width = self._arrow_width + + # Position the settings button inside the spin box, to the left of the arrow buttons. + x = self.width() - arrow_width - self._button_size - 2 # 2px margin + y = (self.height() - self._button_size) // 2 + self.setting_button.setFixedSize(self._button_size, self._button_size) + self.setting_button.move(x, y) + + def change_decimals(self): + """ + Change the number of decimals in the spin box. + """ + current = self.decimals() + new_decimals, ok = QInputDialog.getInt( + self, "Set Decimals", "Number of decimals:", current, 0, 10, 1 + ) + if ok: + self.setDecimals(new_decimals) + + +if __name__ == "__main__": # pragma: no cover + app = QApplication(sys.argv) + window = BECSpinBox() + window.show() + sys.exit(app.exec()) diff --git a/bec_widgets/widgets/utility/spinbox/register_bec_spin_box.py b/bec_widgets/widgets/utility/spinbox/register_bec_spin_box.py new file mode 100644 index 00000000..af43c1e7 --- /dev/null +++ b/bec_widgets/widgets/utility/spinbox/register_bec_spin_box.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.utility.spinbox.bec_spin_box_plugin import BECSpinBoxPlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(BECSpinBoxPlugin()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/tests/unit_tests/test_decimal_spin_box.py b/tests/unit_tests/test_decimal_spin_box.py new file mode 100644 index 00000000..3ca34ef3 --- /dev/null +++ b/tests/unit_tests/test_decimal_spin_box.py @@ -0,0 +1,68 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import +import pytest +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QInputDialog + +from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox + + +@pytest.fixture +def spinbox_fixture(qtbot): + widget = BECSpinBox() + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + + +def test_spinbox_initial_values(spinbox_fixture): + """ + Test the default properties of the BECSpinBox. + """ + spinbox = spinbox_fixture + assert spinbox.decimals() == 2 + assert spinbox.minimum() == -2147483647 + assert spinbox.maximum() == 2147483647 + assert spinbox.setting_button is not None + + +def test_change_decimals_ui(spinbox_fixture, monkeypatch, qtbot): + """ + Test that clicking on the setting button triggers the QInputDialog to change decimals. + We'll simulate a user entering a new decimals value in the dialog. + """ + spinbox = spinbox_fixture + + def mock_get_int(*args, **kwargs): + return (5, True) + + monkeypatch.setattr(QInputDialog, "getInt", mock_get_int) + assert spinbox.decimals() == 2 + + qtbot.mouseClick(spinbox.setting_button, Qt.LeftButton) + assert spinbox.decimals() == 5 + + +def test_change_decimals_cancel(spinbox_fixture, monkeypatch, qtbot): + """ + Test that if the user cancels the decimals dialog, the decimals do not change. + """ + spinbox = spinbox_fixture + + def mock_get_int(*args, **kwargs): + return (0, False) + + monkeypatch.setattr(QInputDialog, "getInt", mock_get_int) + + old_decimals = spinbox.decimals() + qtbot.mouseClick(spinbox.setting_button, Qt.LeftButton) + assert spinbox.decimals() == old_decimals + + +def test_spinbox_value_change(spinbox_fixture): + """ + Test that the spinbox accepts user input and updates its value accordingly. + """ + spinbox = spinbox_fixture + assert spinbox.value() == 0.0 + spinbox.setValue(123.456) + assert spinbox.value() == 123.46