Repository: kivy/kivy

Issue:
Feature request: Sticky TextInput (for virtual keyboard)
_Use case:_ I have a real-time search scroll list that is updated as I type into the VKeyboard. When I scroll through the list, the keyboard dissipaters because text-input loses focus.

It would be ideal if TextInput has an option to be 'sticky' and not hide the keybaord when it loses focus, and only release it when escape is pressed.

I've worked around this by sub-classing the TextInput field and re-factoring it slightly to enable a `hide_on_lose_focus` property and `on_escape_pressed` property that enables the user to detect when the keyboard vanishes from the screen and force it to retain focus on the Text-input field even if the window tries to grab focus.

Happy to submit this as a PR if it's a viable solution?

Here's what I have at the moment:

``` python

class StickyTextInput(TextInput):
    """
    Get the keyboard to stay on screen and assign the correct operation to it
    """
    hide_on_lose_focus = BooleanProperty(True)  # Set this in KV lang to make the keyboard sticky
    escape_pressed = BooleanProperty(False)  # Bindable property to identify when the keyboard is not visible.
    _has_focus = BooleanProperty(False)  # Secondary focus check for sticky behaviour

    def __init__(self,  **kwargs):
        super(StickyTextInput, self).__init__(**kwargs)

    def on_focus(self, instance, value, *largs):
        win = self._win
        if not win:
            self._win = win = EventLoop.window
        if not win:
            # we got argument, it could be the previous schedule
            # cancel focus.
            if len(largs):
                Logger.warning('Textinput: ' +
                    'Cannot focus the element, unable to get root window')
                return
            else:
                Clock.schedule_once(partial(self.on_focus, self, value), 0)
            return

        editable = ((not self.readonly) or
                    (platform() in ('win', 'linux', 'macosx') and
                    self._keyboard_mode == 'system'))

        if value and editable:
            if not self._has_focus:
                # We only need to do this if the keyboard is gone
                self._bind_keyboard(win)

        else:
            if self._keyboard:
                if self.hide_on_lose_focus:
                    self._release_keyboard()

    def _bind_keyboard(self, win):
        self.escape_pressed = False
        keyboard = win.request_keyboard(self._keyboard_released, self)
        self._keyboard = keyboard
        keyboard.bind(
            on_key_down=self._keyboard_on_key_down,
            on_key_up=self._keyboard_on_key_up)
        Clock.schedule_interval(self._do_blink_cursor, 1 / 2.)
        self._has_focus = True

    def _release_keyboard(self):
        self._has_focus = False
        self.focus = False
        keyboard = self._keyboard
        keyboard.unbind(
            on_key_down=self._keyboard_on_key_down,
            on_key_up=self._keyboard_on_key_up)
        win = self._win
        keyboard.release()

        self.cancel_selection()
        Clock.unschedule(self._do_blink_cursor)
        self._hide_cut_copy_paste(win)
        self._win = None

    def _keyboard_on_key_down(self, window, keycode, text, modifiers):
        self._hide_cut_copy_paste()
        is_osx = sys.platform == 'darwin'
        # Keycodes on OSX:
        ctrl, cmd = 64, 1024
        key, key_str = keycode

        if text and not key in (self.interesting_keys.keys() + [27]):
            # This allows *either* ctrl *or* cmd, but not both.
            if modifiers == ['ctrl'] or (is_osx and modifiers == ['meta']):
                if key == ord('x'):  # cut selection
                    self._cut(self.selection_text)
                elif key == ord('c'):  # copy selection
                    self._copy(self.selection_text)
                elif key == ord('v'):  # paste selection
                    self._paste()
                elif key == ord('a'):  # select all
                    self.select_all()
                elif key == ord('z'):  # undo
                    self.do_undo()
                elif key == ord('r'):  # redo
                    self.do_redo()
            else:
                if self._selection:
                    self.delete_selection()
                self.insert_text(text)
            #self._recalc_size()
            return

        if key == 27:  # escape
            # Handle the focus check and set escape_pressed
            self.focus = False
            self.escape_pressed = True
            self._release_keyboard()
            return True
        elif key == 9:  # tab
            self.insert_text('\t')
            return True

        k = self.interesting_keys.get(key)
        if k:
            key = (None, None, k, 1)
            self._key_down(key)

```



Discussion:
I have had the opposite issue that clicking back on the text input doesn't
close the keyboard.  If there isn't enough space in the UI to click off of
the keyboard, it's confusing to get back to not-keyboard.  In my case, I
need the text input touches to also lower the keyboard again.

Both of these behaviors are important =D

On Tue, Nov 26, 2013 at 5:25 AM, Martin Buhr notifications@github.comwrote:

> _Use case:_ I have a real-time search scroll list that is updated as I
> type into the VKeyboard. When I scroll through the list, the keyboard
> dissipaters because text-input loses focus.
> 
> It would be ideal if TextInput has an option to be 'sticky' and not hide
> the keybaord when it loses focus, and only release it when escape is
> pressed.
> 
> I've worked around this by sub-classing the TextInput field and
> re-factoring it slightly to enable a hide_on_lose_focus property and
> on_escape_pressed property that enables the user to detect when the
> keyboard vanishes from the screen and force it to retain focus on the
> Text-input field even if the window tries to grab focus.
> 
> Happy to submit this as a PR if it's a viable solution?
> 
> Here's what I have at the moment:
> 
> class StickyTextInput(TextInput):
>     """    Get the keyboard to stay on screen and assign the correct operation to it    """
>     hide_on_lose_focus = BooleanProperty(True)  # Set this in KV lang to make the keyboard sticky
>     escape_pressed = BooleanProperty(False)  # Bindable property to identify when the keyboard is not visible.
>     _has_focus = BooleanProperty(False)  # Secondary focus check for sticky behaviour
> 
> ```
> def __init__(self,  **kwargs):
>     super(StickyTextInput, self).__init__(**kwargs)
> 
> def on_focus(self, instance, value, *largs):
>     win = self._win
>     if not win:
>         self._win = win = EventLoop.window
>     if not win:
>         # we got argument, it could be the previous schedule
>         # cancel focus.
>         if len(largs):
>             Logger.warning('Textinput: ' +
>                 'Cannot focus the element, unable to get root window')
>             return
>         else:
>             Clock.schedule_once(partial(self.on_focus, self, value), 0)
>         return
> 
>     editable = ((not self.readonly) or
>                 (platform() in ('win', 'linux', 'macosx') and
>                 self._keyboard_mode == 'system'))
> 
>     if value and editable:
>         if not self._has_focus:
>             # We only need to do this if the keyboard is gone
>             self._bind_keyboard(win)
> 
>     else:
>         if self._keyboard:
>             if self.hide_on_lose_focus:
>                 self._release_keyboard()
> 
> def _bind_keyboard(self, win):
>     self.escape_pressed = False
>     keyboard = win.request_keyboard(self._keyboard_released, self)
>     self._keyboard = keyboard
>     keyboard.bind(
>         on_key_down=self._keyboard_on_key_down,
>         on_key_up=self._keyboard_on_key_up)
>     Clock.schedule_interval(self._do_blink_cursor, 1 / 2.)
>     self._has_focus = True
> 
> def _release_keyboard(self):
>     self._has_focus = False
>     self.focus = False
>     keyboard = self._keyboard
>     keyboard.unbind(
>         on_key_down=self._keyboard_on_key_down,
>         on_key_up=self._keyboard_on_key_up)
>     win = self._win
>     keyboard.release()
> 
>     self.cancel_selection()
>     Clock.unschedule(self._do_blink_cursor)
>     self._hide_cut_copy_paste(win)
>     self._win = None
> 
> def _keyboard_on_key_down(self, window, keycode, text, modifiers):
>     self._hide_cut_copy_paste()
>     is_osx = sys.platform == 'darwin'
>     # Keycodes on OSX:
>     ctrl, cmd = 64, 1024
>     key, key_str = keycode
> 
>     if text and not key in (self.interesting_keys.keys() + [27]):
>         # This allows *either* ctrl *or* cmd, but not both.
>         if modifiers == ['ctrl'] or (is_osx and modifiers == ['meta']):
>             if key == ord('x'):  # cut selection
>                 self._cut(self.selection_text)
>             elif key == ord('c'):  # copy selection
>                 self._copy(self.selection_text)
>             elif key == ord('v'):  # paste selection
>                 self._paste()
>             elif key == ord('a'):  # select all
>                 self.select_all()
>             elif key == ord('z'):  # undo
>                 self.do_undo()
>             elif key == ord('r'):  # redo
>                 self.do_redo()
>         else:
>             if self._selection:
>                 self.delete_selection()
>             self.insert_text(text)
>         #self._recalc_size()
>         return
> 
>     if key == 27:  # escape
>         # Handle the focus check and set escape_pressed
>         self.focus = False
>         self.escape_pressed = True
>         self._release_keyboard()
>         return True
>     elif key == 9:  # tab
>         self.insert_text('\t')
>         return True
> 
>     k = self.interesting_keys.get(key)
>     if k:
>         key = (None, None, k, 1)
>         self._key_down(key)
> ```
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/kivy/kivy/issues/1640
> .


Resolving patch:
diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py
--- a/kivy/uix/textinput.py
+++ b/kivy/uix/textinput.py
@@ -1137,7 +1137,7 @@ def _reload_remove_observer(wr):
         if wr in _textinput_list:
             _textinput_list.remove(wr)
 
-    def on_focus(self, instance, value, *largs):
+    def _set_window(self, *largs):
         win = self._win
         if not win:
             self._win = win = EventLoop.window
@@ -1152,36 +1152,56 @@ def on_focus(self, instance, value, *largs):
                 Clock.schedule_once(partial(self.on_focus, self, value), 0)
             return
 
+    def on_focus(self, instance, value, *largs):
+        self._set_window(*largs)
+
         self._editable = editable = (not (self.readonly or self.disabled) or
                     (platform in ('win', 'linux', 'macosx') and
                     self._keyboard_mode == 'system'))
 
         if value:
-            keyboard = win.request_keyboard(
-                self._keyboard_released, self, input_type=self.input_type)
-            self._keyboard = keyboard
-            if editable:
-                keyboard.bind(
-                    on_key_down=self._keyboard_on_key_down,
-                    on_key_up=self._keyboard_on_key_up)
-                Clock.schedule_interval(self._do_blink_cursor, 1 / 2.)
-            else:
-                # in non-editable mode, we still want shortcut (as copy)
-                keyboard.bind(
-                    on_key_down=self._keyboard_on_key_down)
+            if self.keyboard_mode != 'managed':
+                self._bind_keyboard()
         else:
-            if self._keyboard:
-                keyboard = self._keyboard
-                keyboard.unbind(
-                    on_key_down=self._keyboard_on_key_down,
-                    on_key_up=self._keyboard_on_key_up)
-                keyboard.release()
-                self._keyboard = None
-            self.cancel_selection()
-            Clock.unschedule(self._do_blink_cursor)
-            self._hide_cut_copy_paste(win)
-            self._hide_handles(win)
-            self._win = None
+            if self.keyboard_mode != 'managed':
+                self._unbind_keyboard()
+
+    def _unbind_keyboard(self):
+        self._set_window()
+        win = self._win
+        if self._keyboard:
+            keyboard = self._keyboard
+            keyboard.unbind(
+                on_key_down=self._keyboard_on_key_down,
+                on_key_up=self._keyboard_on_key_up)
+            keyboard.release()
+            self._keyboard = None
+
+        self.cancel_selection()
+        Clock.unschedule(self._do_blink_cursor)
+        self._hide_cut_copy_paste(win)
+        self._hide_handles(win)
+        self._win = None
+
+    def _bind_keyboard(self):
+        self._set_window()
+        win = self._win
+        self._editable = editable = (not (self.readonly or self.disabled) or
+                    (platform in ('win', 'linux', 'macosx') and
+                    self._keyboard_mode == 'system'))
+
+        keyboard = win.request_keyboard(
+            self._keyboard_released, self, input_type=self.input_type)
+        self._keyboard = keyboard
+        if editable:
+            keyboard.bind(
+                on_key_down=self._keyboard_on_key_down,
+                on_key_up=self._keyboard_on_key_up)
+            Clock.schedule_interval(self._do_blink_cursor, 1 / 2.)
+        else:
+            # in non-editable mode, we still want shortcut (as copy)
+            keyboard.bind(
+                on_key_down=self._keyboard_on_key_down)
 
     def on_readonly(self, instance, value):
         if not value:
@@ -2432,6 +2452,33 @@ def on_handle_image_right(self, instance, value):
         if self._handle_right:
             self._handle_right.source = value
 
+    keyboard_mode = OptionProperty('auto', options=('auto', 'managed'))
+    '''How the keyboard visibility should be managed (auto will have standard
+    behaviour to show/hide on focus, managed requires setting keyboard_visible
+    manually, or calling the helper functions ``show_keyboard()``
+    and ``hide_keyboard()``
+
+    .. versionadded:: 1.8.0
+
+    :data:`keyboard_mode` is a :class:`~kivy.properties.OptionsProperty`,
+    default to 'auto'. Can be one of 'auto' or 'managed'.
+    '''
+
+    def show_keyboard(self):
+        """
+        Convenience function to show the keyboard in managed mode
+        """
+        if self.keyboard_mode == "managed":
+            self._bind_keyboard()
+
+    def hide_keyboard(self):
+        """
+        Convenience function to hide the keyboard in managed mode
+        """
+        if self.keyboard_mode == "managed":
+            self._unbind_keyboard()
+
+
 if __name__ == '__main__':
     from kivy.app import App
     from kivy.uix.boxlayout import BoxLayout


Test patch: