4

I'm trying to make an input text that only accept float values. In addition, the value entered must be between two values.

I created a class that contain a 'validate' method. If the value is not between two values, a Popup is showed.

But I have a problem. The method is only called when the user hit 'Enter'. I tried call the method when the text is changed, but it is annoying for the user, because the Popup appears all the time while the user is entering the data.

There is another approach to do something like this?

Python file:

class BoundedInput(BoxLayout):
    value = NumericProperty()

    def validate(self, min_value, max_value):
        status = min_value <= self.value <= max_value
        if not status:
            message = f'Value must be between {min_value} and {max_value}'
            popup = Popup(title='Warning', content=Label(text=message),
                            size_hint=(None, None), size=(300, 200))
            popup.open()

Kv file:

<NumericInput@TextInput>:
    input_filter: 'float'
    multiline: False

<BoundedInput>:
    orientation: 'horizontal'
    Label:
        text: 'Value'
    NumericInput:
        text: str(root.value)
        on_text_validate:
            root.value = float(self.text)
            root.validate(5, 100)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Eduardo
  • 687
  • 1
  • 6
  • 24

2 Answers2

4

A suitable approach could be filtering in addition to floating this also within the range for it we create a class that inherits TextInput and overwrite the insert_text method:

from kivy.app import App
from kivy.base import Builder
from kivy.properties import NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput


Builder.load_string("""
<BoundedLayout>:
    orientation: 'horizontal'
    Label:
        text: 'Value'
    NumericInput:
        min_value : 5
        max_value : 100
        hint_text : 'Enter values between {} and {}'.format(self.min_value, self.max_value)
""")

class NumericInput(TextInput):
    min_value = NumericProperty()
    max_value = NumericProperty()
    def __init__(self, *args, **kwargs):
        TextInput.__init__(self, *args, **kwargs)
        self.input_filter = 'float'
        self.multiline = False

    def insert_text(self, string, from_undo=False):
        new_text = self.text + string
        if new_text != "":
            if self.min_value <= float(new_text) <= self.max_value:
                TextInput.insert_text(self, string, from_undo=from_undo)

class BoundedLayout(BoxLayout):
    pass

class MyApp(App):
    def build(self):
        return BoundedLayout()

if __name__ == '__main__':
    MyApp().run()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • That's a good approach. The only thing is the users doesn't know why they cannot input some values. I added a 'hint_text' in the input_text to notify the user the min and max values. Thanks! – Eduardo Jan 22 '18 at 15:07
  • @Eduardo Sure, that complements my solution. – eyllanesc Jan 22 '18 at 15:09
  • @Eduardo I already added it, if my answer helped you, you could mark it as correct. :D – eyllanesc Jan 22 '18 at 15:12
  • I founded an problem, I can't type "100" for example from the beginning. I'll investigate how can I solve it. – Eduardo Jan 22 '18 at 15:13
  • Correct me if I'm wrong, but doesn't this validation assume that the substring is being appended? What if the new text is inserted in the middle of the existing text, i.e. 3.90 -> 39.90 would check if 3.909 were in range ... – Daniel B. Nov 24 '18 at 02:57
3

You could use Bubble, and leave it hanging as long the input is wrong.
You can even edit the lable to tell what is wrong.
Or change the text color if validated or not.
I made an example with both implemented.

from kivy.app import App
from kivy.uix.textinput import TextInput
from kivy.uix.bubble import Bubble
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import BooleanProperty

KV = """

<ValidateLabel>:
    size_hint: (None, None)
    size: (280, 60)
    Label:
        id: label
        text: "Must be a float"


<MyInput>:
    foreground_color: (0,1,0,1) if root.validated else (1,0,0,1)


FloatInput:

"""


class MyInput(TextInput):
    validated = BooleanProperty(False)


class FloatInput(FloatLayout):
    bubble_showed = True

    def __init__(self, **kwargs):
        super(FloatInput, self).__init__(**kwargs)
        self.input = MyInput()
        self.input.bind(text=self.validate)
        self.add_widget(self.input)
        self.bubble = ValidateLabel()
        self.add_widget(self.bubble)

    def validate(self, input, value, min_value=15., max_value=25.):
        self.bubble.ids.label.text = "Number must be between {} and {}".format(min_value, max_value)
        try:
            print(min_value, max_value)
            status = float(min_value) <= float(value) <= float(max_value)
        except Exception as e:
            status = False
            self.bubble.ids.label.text = "Input must be a number"

        if not status:
            if not self.bubble_showed:
                self.input.validated = False
                self.add_widget(self.bubble)
                self.bubble_showed = True
        else:
            print("bubble removed")
            self.input.validated = True
            self.remove_widget(self.bubble)
            self.bubble_showed = False


class ValidateLabel(Bubble):
    validated = False


class TestApp(App):

    def build(self):
        return Builder.load_string(KV)


TestApp().run()

Output:

Number to small or big: Not validated

Number is okey: Validated

Input value is not a number: Not validated text input

el3ien
  • 5,362
  • 1
  • 17
  • 33
  • That is a good approach. The user is propely notified. I'll try it and I'll come back with comments. – Eduardo Jan 22 '18 at 15:24
  • @Eduardo yes these are at least two ways that in my opinion goes well together. The green color indicates that it is okey, no need for bubble then. Good luck. – el3ien Jan 23 '18 at 20:20