3

I am trying to create a simple TextInput for dates that limits input to numbers and auto-populates the the forward slashes for a (mm/dd/yy) format. I was successful in creating a filter that does this by redefining insert_text(), except when the user backspaces, I want to automatically remove the slashes as well. But I don't know how to detect when the user backspaces in the text input so that I can fire an event to erase the slashes if necessary.

Here's a snippet that explains what I want to do, but TextInput does not have an "on_key_up" attribute. Is there a way to add one? Or a better way to go about this?

# .kv file

<DateInput>
    on_key_up: self.check_for_backspace(keycode) # not a true attribute

# .py file

class DateInput(TextInput):

    # checks if last character is a slash and removes it after backspace keystroke.  Not sure this would work.
    def check_for_backspace(self, keycode):
        if keycode[1] == 'backspace' and self.text[-1:] == '/':
            self.text = self.text[:-1]

    #filter for date formatting which works well aside from backspacing
    pat = re.compile('[^0-9]')
    def insert_text(self, substring, from_undo=False):
        pat = self.pat
        if len(substring) > 1:
            substring = re.sub(pat, '', (self.text + substring))
            self.text = ''
            slen = len(substring)
            if slen == 2:
                s = substring[:2] + '/'
            elif slen == 3:
                s = substring[:2] + '/' + substring[2:]
            elif slen == 4:
                s = substring[:2] + '/' + substring[2:] + '/'
            else:
                s = substring[:2] + '/' + substring[2:4] + '/' + substring[4:8]
        elif len(self.text) > 9:
            s = ''
        elif len(self.text) == 1:
            s = re.sub(pat, '', substring)
            if s != '':
                s = s + '/'
        elif len(self.text) == 4:
            s = re.sub(pat, '', substring)
            if s != '':
                s = s + '/'
        else:
            s = re.sub(pat, '', substring)
        return super(DateInput, self).insert_text(s, from_undo=from_undo)

4 Answers4

0

Since version 1.9.0 TextInput is inherent to FocusBehavior so if you want to detect when you press backspace you must use the keyboard_on_key_down() method or keyboard_on_key_up() method:

from kivy.app import App
from kivy.uix.textinput import TextInput

class DateInput(TextInput):
    def keyboard_on_key_down(self, window, keycode, text, modifiers):
        if keycode[1] == "backspace":
            print("print backspace down", keycode)
        TextInput.keyboard_on_key_down(self, window, keycode, text, modifiers)

    def keyboard_on_key_up(self, window, keycode, text, modifiers):
        if keycode[1] == "backspace":
            print("print backspace up", keycode)
        TextInput.keyboard_on_key_down(self, window, keycode, text, modifiers)


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

if __name__ == '__main__':
    MyApp().run()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • 1
    Awesome! This is super helpful. I will switch that out with keyboard_on_key_up(self, window, keycode) in order to prevent running code before the user is finished backspacing. Thank you! – brother_friend_ Jun 30 '18 at 22:05
0

Here's the full solution for anyone wanting a auto-formatted Date Input:

class DateInput(TextInput):
    def keyboard_on_key_up(self, window, keycode):
        if keycode[1] == "backspace" and len(self.text) >= 1:
            if self.text[-1] == "/":
                self.text = self.text[:-1]
            else:
                pass
        else:
            pass
        TextInput.keyboard_on_key_up(self, window, keycode)

    pat = re.compile('[^0-9]')
    def insert_text(self, substring, from_undo=False):
        pat = self.pat
        if len(substring) > 1:
            substring = re.sub(pat, '', (self.text + substring))
            self.text = ''
            slen = len(substring)
            if slen == 2:
                s = substring[:2] + '/'
            elif slen == 3:
                s = substring[:2] + '/' + substring[2:]
            elif slen == 4:
                s = substring[:2] + '/' + substring[2:] + '/'
            else:
                s = substring[:2] + '/' + substring[2:4] + '/' + substring[4:8]
        elif len(self.text) > 9:
            s = ''
        elif len(self.text) == 2:
            s = re.sub(pat, '', substring)
            if s != '':
                s = '/' + s
        elif len(self.text) == 5:
            s = re.sub(pat, '', substring)
            if s != '':
                s = '/' + s
        else:
            s = re.sub(pat, '', substring)
        return super(DateInput, self).insert_text(s, from_undo=from_undo)
0

Solution

Override the do_backspace() method in TextInput. If the text is '/' then return True, indicating that we have consumed the backspace and don’t want it to propagate any further.

Finally, if the text is not '/', we call the original event using super(…) and return the result. This allows the do_backspace event propagation to continue as it would normally have occurred.

Please refer to example for details.

Text Input » do_backspace() method

do_backspace(from_undo=False, mode='bkspc')

Do backspace operation from the current cursor position. This action might do several things:

  • removing the current selection if available.
  • removing the previous char and move the cursor back.
  • do nothing, if we are at the start.

Example

main.py

from kivy.app import App
from kivy.uix.textinput import TextInput
import re


class DateInput(TextInput):

    def do_backspace(self, from_undo=False, mode='bkspc'):
        print(from_undo, mode)
        if len(self.text) >= 1:
            if self.text[-1] == "/":
                self.text = self.text[:-1]
                return True    # we have consumed the backspace and don’t want it to propagate any further
        return super(DateInput, self).do_backspace(from_undo, mode)

    #filter for date formatting which works well aside from backspacing
    pat = re.compile('[^0-9]')

    def insert_text(self, substring, from_undo=False):
        pat = self.pat
        if len(substring) > 1:
            substring = re.sub(pat, '', (self.text + substring))
            self.text = ''
            slen = len(substring)
            if slen == 2:
                s = substring[:2] + '/'
            elif slen == 3:
                s = substring[:2] + '/' + substring[2:]
            elif slen == 4:
                s = substring[:2] + '/' + substring[2:] + '/'
            else:
                s = substring[:2] + '/' + substring[2:4] + '/' + substring[4:8]
        elif len(self.text) > 9:
            s = ''
        elif len(self.text) == 1:
            s = re.sub(pat, '', substring)
            if s != '':
                s = s + '/'
        elif len(self.text) == 4:
            s = re.sub(pat, '', substring)
            if s != '':
                s = s + '/'
        else:
            s = re.sub(pat, '', substring)
        return super(DateInput, self).insert_text(s, from_undo=from_undo)


class TestApp(App):

    def build(self):
        return DateInput()


if __name__ == "__main__":
    TestApp().run()

Output

Img01 Img02

ikolim
  • 15,721
  • 2
  • 19
  • 29
0

Here is my solution:

class DateInput(TextInput):
    pat = re.compile("[^0-9]")

    def insert_text(self, substring, from_undo=False):
        pat = self.pat
        s = re.sub(pat, "", substring)
        if len(self.text) >= 10:
            return super(DateInput, self).insert_text("", from_undo=from_undo)
        elif ((len(self.text) > 2) and (self.cursor_index() == 2)) or (
            (len(self.text) > 5) and (self.cursor_index() == 5)
        ):
            return super(DateInput, self).insert_text("/", from_undo=from_undo)
        elif (len(self.text) == 2) or (len(self.text) == 5):
            return super(DateInput, self).insert_text("/" + s, from_undo=from_undo)
        else:
            return super(DateInput, self).insert_text(s, from_undo=from_undo)