Source code for edifice.base_components.spin_input

from __future__ import annotations

import typing as tp

from edifice.qt import QT_VERSION

if QT_VERSION == "PyQt6" and not tp.TYPE_CHECKING:
    from PyQt6.QtGui import QValidator
    from PyQt6.QtWidgets import QSpinBox
else:
    from PySide6.QtGui import QValidator
    from PySide6.QtWidgets import QSpinBox

from .base_components import CommandType, Element, QtWidgetElement, _ensure_future, _WidgetTree


class EdSpinBox(QSpinBox):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._textFromValue: tp.Callable[[int], str] | None = None
        self._valueFromText: (
            tp.Callable[[str], int | tp.Literal[QValidator.State.Intermediate, QValidator.State.Invalid]] | None
        ) = None

    def textFromValue(self, val: int) -> str:
        if self._textFromValue is not None:
            return self._textFromValue(val)
        return super().textFromValue(val)

    def valueFromText(self, text: str) -> int:
        if self._valueFromText is not None:
            match self._valueFromText(text):
                case QValidator.State.Intermediate:
                    return 0  # unreachable case
                case QValidator.State.Invalid:
                    return 0  # unreachable case
                case val:
                    return val
        else:
            return super().valueFromText(text)

    def validate(self, input: str, pos: int) -> object:  # noqa: A002
        if self._valueFromText is not None:
            match self._valueFromText(input):
                case QValidator.State.Intermediate:
                    return QValidator.State.Intermediate
                case QValidator.State.Invalid:
                    return QValidator.State.Invalid
                case _:
                    return QValidator.State.Acceptable
        else:
            return super().validate(input, pos)


[docs] class SpinInput(QtWidgetElement[EdSpinBox]): """Widget for a :code:`int` input value with up/down buttons. Allows the user to choose a value by clicking the up/down buttons or pressing up/down on the keyboard to increase/decrease the value currently displayed. The user can also type the value in manually. * Underlying Qt Widget `QSpinBox <https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QSpinBox.html>`_ Args: value: Value of the text input. min_value: Minimum value of the text input. max_value: Maximum value of the text input. on_change: Callback for when the value changes. The callback is passed the changed value. value_to_text: Function to convert the value to a text. If not provided, the default text conversion is used. text_to_value: Function to convert the text to a value. If not provided, the default text conversion is used. The function should return one of * :code:`int` value if the text is valid. * :code:`QValidator.State.Intermediate` if the text might be valid with more input. * :code:`QValidator.State.Invalid` if the text is invalid. See `QValidator.State <https://doc.qt.io/qtforpython-6/PySide6/QtGui/QValidator.html#PySide6.QtGui.QValidator.State>`_. (At the time of this writing, the PySide6.QValidator.State Enum cannot be correctly typechecked as a Literal by Pyright for some reason.) single_step: The value step size for the up/down buttons. enable_mouse_scroll: Whether mouse scroll events should be able to change the value. """ # TODO Note that you can set an optional Completer, giving the dropdown for completion.
[docs] def __init__( self, value: int = 0, min_value: int = 0, max_value: int = 100, on_change: tp.Callable[[int], None | tp.Awaitable[None]] | None = None, value_to_text: tp.Callable[[int], str] | None = None, text_to_value: tp.Callable[ [str], int | tp.Literal[QValidator.State.Intermediate, QValidator.State.Invalid], ] | None = None, single_step: int = 1, enable_mouse_scroll: bool = True, **kwargs, ): super().__init__(**kwargs) self._register_props( { "value": value, "min_value": min_value, "max_value": max_value, "on_change": on_change, "value_to_text": value_to_text, "text_to_value": text_to_value, "single_step": single_step, "enable_mouse_scroll": enable_mouse_scroll, }, )
def _initialize(self): self.underlying = EdSpinBox() self.underlying.setObjectName(str(id(self))) self.underlying.valueChanged.connect(self._on_change_handler) if "enable_mouse_scroll" in self.props and not self.props.enable_mouse_scroll: self.underlying.wheelEvent = lambda event: event.ignore() def _on_change_handler(self, value: int): if self.props.on_change is not None: _ensure_future(self.props.on_change)(value) def _set_value(self, value: int): assert self.underlying is not None self.underlying.blockSignals(True) self.underlying.setValue(value) self.underlying.blockSignals(False) def _set_min_value(self, value: int): assert self.underlying is not None self.underlying.blockSignals(True) self.underlying.setMinimum(value) self.underlying.blockSignals(False) def _set_max_value(self, value: int): assert self.underlying is not None self.underlying.blockSignals(True) self.underlying.setMaximum(value) self.underlying.blockSignals(False) def _qt_update_commands( self, widget_trees: dict[Element, _WidgetTree], newprops, ): if self.underlying is None: self._initialize() assert self.underlying is not None commands = super()._qt_update_commands_super(widget_trees, newprops, self.underlying) if "value_to_text" in newprops: commands.append(CommandType(setattr, self.underlying, "_textFromValue", newprops.value_to_text)) if "text_to_value" in newprops: commands.append(CommandType(setattr, self.underlying, "_valueFromText", newprops.text_to_value)) if "min_value" in newprops: commands.append(CommandType(self._set_min_value, newprops.min_value)) if "max_value" in newprops: commands.append(CommandType(self._set_max_value, newprops.max_value)) if "value" in newprops: commands.append(CommandType(self._set_value, newprops.value)) if "single_step" in newprops: commands.append(CommandType(self.underlying.setSingleStep, newprops.single_step)) return commands