From 61806a69b813ccd91db6cd51be5870ca61368819 Mon Sep 17 00:00:00 2001 From: Maic Siemering Date: Fri, 28 Mar 2025 14:46:24 +0100 Subject: [PATCH 1/3] gui: add restricted input --- arcade/examples/gui/exp_restricted_input.py | 34 ++++++++ arcade/gui/experimental/restricted_input.py | 92 +++++++++++++++++++++ tests/unit/gui/test_exp_restricted_input.py | 51 ++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 arcade/examples/gui/exp_restricted_input.py create mode 100644 arcade/gui/experimental/restricted_input.py create mode 100644 tests/unit/gui/test_exp_restricted_input.py diff --git a/arcade/examples/gui/exp_restricted_input.py b/arcade/examples/gui/exp_restricted_input.py new file mode 100644 index 0000000000..04c05ecd48 --- /dev/null +++ b/arcade/examples/gui/exp_restricted_input.py @@ -0,0 +1,34 @@ +"""Example of using experimental UIRestrictedInputText. + +If Arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.own_widgets +""" + +from __future__ import annotations + +import arcade +from arcade.gui import UIAnchorLayout, UIBoxLayout, UIView +from arcade.gui.experimental.restricted_input import UIIntInput + + +class MyView(UIView): + def __init__(self): + super().__init__() + self.background_color = arcade.uicolor.BLUE_BELIZE_HOLE + + root = self.ui.add(UIAnchorLayout()) + bars = root.add(UIBoxLayout(space_between=10)) + + # UIWidget based progress bar + self.input_field = UIIntInput(width=300, height=40, font_size=22) + bars.add(self.input_field) + + +def main(): + window = arcade.Window(antialiasing=False) + window.show_view(MyView()) + arcade.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/gui/experimental/restricted_input.py b/arcade/gui/experimental/restricted_input.py new file mode 100644 index 0000000000..c3d228160a --- /dev/null +++ b/arcade/gui/experimental/restricted_input.py @@ -0,0 +1,92 @@ +""" +This is an experimental implementation of a restricted input field. +If the implementation is successful, the feature will be merged into the existing UIInputText class. +""" + +from typing import Optional + + +from arcade.gui import UIEvent, UIInputText + + +class UIRestrictedInput(UIInputText): + """ + A text input field that restricts the input to a certain type. + + This class is meant to be subclassed to create custom input fields + that restrict the input by providing a custom validation method. + + Invalid inputs are dropped. + """ + + @property + def text(self): + """Text of the input field.""" + return self.doc.text + + @text.setter + def text(self, text: str): + if not self.validate(text): + # if the text is invalid, do not update the text + return + + # we can not call super().text = text here: https://bugs.python.org/issue14965 + UIInputText.text.__set__(self, text) # type: ignore + + def on_event(self, event: UIEvent) -> Optional[bool]: + # check if text changed during event handling, + # if so we need to validate the new text + old_text = self.text + pos = self.caret.position + + result = super().on_event(event) + if not self.validate(self.text): + self.text = old_text + self.caret.position = pos + + return result + + def validate(self, text) -> bool: + """Override this method to add custom validation logic. + + Be aware that an empty string should always be valid. + """ + return True + + +class UIIntInput(UIRestrictedInput): + def validate(self, text) -> bool: + if text == "": + return True + + try: + int(text) + return True + except ValueError: + return False + + +class UIFloatInput(UIRestrictedInput): + def validate(self, text) -> bool: + if text == "": + return True + + try: + float(text) + return True + except ValueError: + return False + + +class UIRegexInput(UIRestrictedInput): + def __init__(self, *args, pattern: str = r".*", **kwargs): + super().__init__() + self.pattern = pattern + + def validate(self, text: str) -> bool: + if text == "": + return True + + import re + + return re.match(self.pattern, text) is not None diff --git a/tests/unit/gui/test_exp_restricted_input.py b/tests/unit/gui/test_exp_restricted_input.py new file mode 100644 index 0000000000..468368197b --- /dev/null +++ b/tests/unit/gui/test_exp_restricted_input.py @@ -0,0 +1,51 @@ +from arcade.gui.experimental import UIPasswordInput +from arcade.gui.experimental.restricted_input import UIRestrictedInput, UIIntInput, UIRegexInput + + +def test_restricted_input_ignore_invalid_input(ui): + class FailingInput(UIRestrictedInput): + def validate(self, text) -> bool: + return text == "" + + fi = ui.add(FailingInput()) + + # WHEN + ui.click(fi.center_x, fi.center_y) + for l in "abcdef-.,1234567890": + ui.type_text(l) + + assert fi.text == "" + + +def test_int_input_accepts_only_digits(ui): + fi = ui.add(UIIntInput()) + + # WHEN + ui.click(fi.center_x, fi.center_y) + for l in "abcdef-.,1234567890": + ui.type_text(l) + + assert fi.text == "1234567890" + + +def test_float_input_accepts_only_float(ui): + fi = ui.add(UIIntInput()) + + # WHEN + ui.click(fi.center_x, fi.center_y) + for l in "abcdef-.,1234567890": + ui.type_text(l) + + assert fi.text == "1234567890" + + +def test_regex_input_accepts_only_matching_patterns(ui): + fi = ui.add(UIRegexInput(pattern="^[0-9]+$")) + + # WHEN + ui.click(fi.center_x, fi.center_y) + for l in "abcdef-.,1234567890": + ui.type_text(l) + + assert fi.text == "1234567890" + From 5513aa669f43c784606b2f74fe187558c2c13b3c Mon Sep 17 00:00:00 2001 From: Maic Siemering Date: Fri, 28 Mar 2025 14:50:37 +0100 Subject: [PATCH 2/3] fix example missing execution string --- arcade/examples/gui/exp_restricted_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/examples/gui/exp_restricted_input.py b/arcade/examples/gui/exp_restricted_input.py index 04c05ecd48..1611cc798f 100644 --- a/arcade/examples/gui/exp_restricted_input.py +++ b/arcade/examples/gui/exp_restricted_input.py @@ -1,7 +1,7 @@ """Example of using experimental UIRestrictedInputText. If Arcade and Python are properly installed, you can run this example with: -python -m arcade.examples.gui.own_widgets +python -m arcade.examples.gui.exp_restricted_input """ from __future__ import annotations From 2276c9e3683c3ccd0b79f92b7b37d2f7cc9f7166 Mon Sep 17 00:00:00 2001 From: Maic Siemering Date: Fri, 28 Mar 2025 14:55:32 +0100 Subject: [PATCH 3/3] fix import format --- arcade/gui/experimental/restricted_input.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arcade/gui/experimental/restricted_input.py b/arcade/gui/experimental/restricted_input.py index c3d228160a..1d7a41494c 100644 --- a/arcade/gui/experimental/restricted_input.py +++ b/arcade/gui/experimental/restricted_input.py @@ -5,7 +5,6 @@ from typing import Optional - from arcade.gui import UIEvent, UIInputText