community icon indicating copy to clipboard operation
community copied to clipboard

Keyboard binding to on_textinput closes as soon as any TextInput widget is focussed and unfocused in Kivy

Open CoreTaxxe opened this issue 2 years ago • 11 comments
trafficstars

Software Versions

  • Python: 3.10
  • OS: Windows 11
  • Kivy: master
  • Kivy installation method: pip git

Describe the bug When binding to "on_textinput" to a Keyboard instance the keyboard gets closed as soon as you focus/click on any textinput and click out of it again. It should be noted, that the callback bound to "on_textinput" still receives inputs as long as you 1. haven't clicked any textinput 2. are still in the textinput you clicked. Just as soon as you leave the textinput the callbacks cease. The keyboard dispatches "keyboard_closed" as soon as you click into a textinput but still dispatches everything.

The callback works again as soon as you enter any textinput again.

Expected behavior One of two things should happen

  1. The keyboard never closes or always receives on_textinput callbacks.
  2. The keyboard completely closes and instantly (not after you left the textinput) stops all dispatches not just the "on_textinput"

Code and Logs and screenshots

from kivy.app import App
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput


def text_input(keyboard, text):
    print(f"TI : {keyboard}, {text}")


class Root(FloatLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        keyboard = Window.request_keyboard(self.keyboard_closed, self, 'text')
        keyboard.bind(on_textinput=text_input)
        self.add_widget(TextInput(size_hint=(1,0.2)))

    def keyboard_closed(self):
        print(f"keyboard closed for {self}")


class MApp(App):
    def build(self):
        return Root()

MApp().run()

CoreTaxxe avatar Feb 16 '23 22:02 CoreTaxxe

For anyone interested you can kid of avoid this by setting Config.set("kivy", "keyboard_mode", "systemandmulti") BUT then it opens a virtual keyboard for the textinputs which makes it unusable for me. This doesn't however solve the inconsistency with the even unbinding.

CoreTaxxe avatar Feb 16 '23 22:02 CoreTaxxe

So the issue is caused by some odd SDL behaviour. Basically SDL_TEXTINPUT is enabled by default on Desktop platforms but disabled on mobile platforms. Now on desktop whenever anyone (here FocusBehaviour) requests a keyboard SDL_StartTextInput is called which on mobile brings up the onscreen keyboard but has no effect on Desktop (unless SDL_TEXTINPUT was disabled prior). Now if the anyone (FocusBehaviour here again) "releases" the keyboard SDL_StopTextInput is called which hides the keyboard on mobile and tells SDL to stop receiving/dispatching SDL_TEXTINPUT. (All this is inside kivy/core/window/_window_sdl2.pyx).

My proposed fix would be (I will open a PR soonish after some testing)

THIS IS THE CURRENT CODE

def hide_keyboard(self):
    if SDL_IsTextInputActive():
        SDL_StopTextInput()

This is my proposed change

def hide_keyboard(self):
    if (platform == 'android' or platform == 'ios') and SDL_IsTextInputActive():
        SDL_StopTextInput()

I am not sure if this fixes the issue for MacOS since I don't know if the issue exists there in the first place.

Additionally, I assumed that it's better to fix it partly for other systems but keep functionality consistent with others.

Edit; Fixed platform check.

CoreTaxxe avatar Jun 21 '23 18:06 CoreTaxxe

How about https://kivy.org/doc/stable/api-kivy.uix.behaviors.html#kivy.uix.behaviors.FocusBehavior.keyboard_mode ?

TextInput:
    keyboard_mode: 'managed'

Cause here, you're actually requesting the keyboard on your own, (and you expect to receive events even outside of theTextInput)

#8292 seems hacky as in auto mode we expect to not receive events anymore. (even on Desktop platforms)

misl6 avatar Jun 24 '23 07:06 misl6

Thought of that initially as well. Doesn't work, however.

https://github.com/kivy/kivy/pull/8292 seems hacky as in auto mode we expect to not receive events anymore. (even on Desktop platforms)

I kinda disagree, cause on Desktop you always want to get the keyboard inputs for SDL_TEXTINPUT and even if you don't they are on by default so you have to disable them anyways manually. So in auto mode we need to call SDL_StopTextInput otherwise its getting dispatched until you enter the text input. (Note: Unless the user bound to Window.on_text_input not stopping textinput has no effect on anything else since focus behaviour manages callback binding byitself and disconnects from SDL_TEXTINPUT).

I also believe that this should be the preferred way of doing SDL event callbacks as managing them privately (e.g. in your own class) instead of for everyone (by disabling it on the SDL layer.) causes less confusion and issues. But I am not the one to decide that so if you disagree that is totally fine with me.

I can see why one could call the fix hacky but in the end, it's not really violating any existing principles. Maybe it could make use of an additional statement for the unlikely case that a user wants to disable text input on Desktop for some reason.

keyboard_mode:'managed' doesn't really work. Cause then you need to manually call show_keyboard and hide_keyboard which is not only very laborious but also causes the error again since hide_keyboard calls release_keyboard which then calls hide_keyboard as defined in _window_sdl2.pyx.

Tldr; keyboard_mode : 'managed' requires to manually toggle keyboard which not only is laborious but also doesn't prevent the error from happening.

Edit; added example


from kivy.app import App
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput


def text_input(keyboard, text):
    print(f"TI : {keyboard}, {text}")


class Root(FloatLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.bind(on_textinput=text_input)
        t = TextInput(size_hint=(1,0.2), keyboard_mode="managed")

        def p(w,v):
            if v:
                t.show_keyboard()
            else:
                t.hide_keyboard()
        
        t.bind(focus=p)
        self.add_widget(t)

    def keyboard_closed(self):
        print(f"keyboard closed for {self}")


class MApp(App):
    def build(self):
        return Root()

MApp().run()

We need to call show_keyboard to start receiving input and hide_keyboard to stop receiving inputs which causes the bug

CoreTaxxe avatar Jun 24 '23 15:06 CoreTaxxe

@misl6 Do you mind running the checks - cause if they fail then I have to rethink this eitherway

CoreTaxxe avatar Jul 01 '23 12:07 CoreTaxxe

@CoreTaxxe: Trying to see what is required to move this issue along. What does "running the checks" mean here?

Julian-O avatar Oct 31 '23 04:10 Julian-O

I meant the PR git checks that are run before merging. (For the linked PR). Since misl6 and I didn't agree on the topic I at least wanted to know if my implementation would work.

CoreTaxxe avatar Nov 01 '23 01:11 CoreTaxxe

@CorreTaxxe: oh, right. In GitHub, if you go to your fork you can select Actions and inherit them from the original, and then the checks are run against each of your commits.

Julian-O avatar Nov 01 '23 06:11 Julian-O

Oh cool good to know Thanks!

CoreTaxxe avatar Nov 01 '23 12:11 CoreTaxxe

@misl6 I implemented it a bit differently now with a config entry defaulting to false that controls this behaviour for desktop platforms

CoreTaxxe avatar Feb 10 '24 13:02 CoreTaxxe