diff --git a/arcade/examples/gui/exp_restricted_input.py b/arcade/examples/gui/exp_restricted_input.py new file mode 100644 index 0000000000..1611cc798f --- /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.exp_restricted_input +""" + +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..1d7a41494c --- /dev/null +++ b/arcade/gui/experimental/restricted_input.py @@ -0,0 +1,91 @@ +""" +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" +