Version

Quick search

Table Of Contents

Source code for kivy.uix.behaviors.focus

'''
Focus Behavior
==============

The :class:`~kivy.uix.behaviors.FocusBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
keyboard focus behavior. When combined with other
FocusBehavior widgets it allows one to cycle focus among them by pressing
tab. In addition, upon gaining focus, the instance will automatically
receive keyboard input.

Focus, very different from selection, is intimately tied with the keyboard;
each keyboard can focus on zero or one widgets, and each widget can only
have the focus of one keyboard. However, multiple keyboards can focus
simultaneously on different widgets. When escape is hit, the widget having
the focus of that keyboard will de-focus.

Managing focus
--------------

In essence, focus is implemented as a doubly linked list, where each
node holds a (weak) reference to the instance before it and after it,
as visualized when cycling through the nodes using tab (forward) or
shift+tab (backward). If a previous or next widget is not specified,
:attr:`focus_next` and :attr:`focus_previous` defaults to `None`. This
means that the :attr:`~kivy.uix.widget.Widget.children` list and
:attr:`parents <kivy.uix.widget.Widget.parent>` are
walked to find the next focusable widget, unless :attr:`focus_next` or
:attr:`focus_previous` is set to the `StopIteration` class, in which case
focus stops there.

For example, to cycle focus between :class:`~kivy.uix.button.Button`
elements of a :class:`~kivy.uix.gridlayout.GridLayout`::

    class FocusButton(FocusBehavior, Button):
      pass

    grid = GridLayout(cols=4)
    for i in range(40):
        grid.add_widget(FocusButton(text=str(i)))
    # clicking on a widget will activate focus, and tab can now be used
    # to cycle through

When using a software keyboard, typical on mobile and touch devices, the
keyboard display behavior is determined by the
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
this property to ensure the focused widget is not covered or obscured by the
keyboard.

Initializing focus
------------------

Widgets needs to be visible before they can receive the focus. This means that
setting their *focus* property to True before they are visible will have no
effect. To initialize focus, you can use the 'on_parent' event::

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

    class MyTextInput(TextInput):
        def on_parent(self, widget, parent):
            self.focus = True

    class SampleApp(App):
        def build(self):
            return MyTextInput()

    SampleApp().run()

If you are using a :class:`~kivy.uix.popup`, you can use the 'on_open' event.

For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.

.. warning::

    This code is still experimental, and its API is subject to change in a
    future version.
'''

__all__ = ('FocusBehavior', )

from kivy.properties import OptionProperty, ObjectProperty, BooleanProperty, \
    AliasProperty
from kivy.config import Config
from kivy.base import EventLoop

# When we are generating documentation, Config doesn't exist
_is_desktop = False
_keyboard_mode = 'system'
if Config:
    _is_desktop = Config.getboolean('kivy', 'desktop')
    _keyboard_mode = Config.get('kivy', 'keyboard_mode')


[docs]class FocusBehavior(object): '''Provides keyboard focus behavior. When combined with other FocusBehavior widgets it allows one to cycle focus among them by pressing tab. Please see the :mod:`focus behavior module documentation <kivy.uix.behaviors.focus>` for more information. .. versionadded:: 1.9.0 ''' _requested_keyboard = False _keyboard = ObjectProperty(None, allownone=True) _keyboards = {} ignored_touch = [] '''A list of touches that should not be used to defocus. After on_touch_up, every touch that is not in :attr:`ignored_touch` will defocus all the focused widgets if the config keyboard mode is not multi. Touches on focusable widgets that were used to focus are automatically added here. Example usage:: class Unfocusable(Widget): def on_touch_down(self, touch): if self.collide_point(*touch.pos): FocusBehavior.ignored_touch.append(touch) Notice that you need to access this as a class, not an instance variable. ''' def _set_keyboard(self, value): focus = self.focus keyboard = self._keyboard keyboards = FocusBehavior._keyboards if keyboard: self.focus = False # this'll unbind if self._keyboard: # remove assigned keyboard from dict del keyboards[keyboard] if value and value not in keyboards: keyboards[value] = None self._keyboard = value self.focus = focus def _get_keyboard(self): return self._keyboard keyboard = AliasProperty(_get_keyboard, _set_keyboard, bind=('_keyboard', )) '''The keyboard to bind to (or bound to the widget) when focused. When None, a keyboard is requested and released whenever the widget comes into and out of focus. If not None, it must be a keyboard, which gets bound and unbound from the widget whenever it's in or out of focus. It is useful only when more than one keyboard is available, so it is recommended to be set to None when only one keyboard is available. If more than one keyboard is available, whenever an instance gets focused a new keyboard will be requested if None. Unless the other instances lose focus (e.g. if tab was used), a new keyboard will appear. When this is undesired, the keyboard property can be used. For example, if there are two users with two keyboards, then each keyboard can be assigned to different groups of instances of FocusBehavior, ensuring that within each group, only one FocusBehavior will have focus, and will receive input from the correct keyboard. See `keyboard_mode` in :mod:`~kivy.config` for more information on the keyboard modes. **Keyboard and focus behavior** When using the keyboard, there are some important default behaviors you should keep in mind. * When Config's `keyboard_mode` is multi, each new touch is considered a touch by a different user and will set the focus (if clicked on a focusable) with a new keyboard. Already focused elements will not lose their focus (even if an unfocusable widget is touched). * If the keyboard property is set, that keyboard will be used when the instance gets focused. If widgets with different keyboards are linked through :attr:`focus_next` and :attr:`focus_previous`, then as they are tabbed through, different keyboards will become active. Therefore, typically it's undesirable to link instances which are assigned different keyboards. * When a widget has focus, setting its keyboard to None will remove its keyboard, but the widget will then immediately try to get another keyboard. In order to remove its keyboard, rather set its :attr:`focus` to False. * When using a software keyboard, typical on mobile and touch devices, the keyboard display behavior is determined by the :attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use this property to ensure the focused widget is not covered or obscured. :attr:`keyboard` is an :class:`~kivy.properties.AliasProperty` and defaults to None. .. warning: When assigning a keyboard, the keyboard must not be released while it is still assigned to an instance. Similarly, the keyboard created by the instance on focus and assigned to :attr:`keyboard` if None, will be released by the instance when the instance loses focus. Therefore, it is not safe to assign this keyboard to another instance's :attr:`keyboard`. ''' is_focusable = BooleanProperty(_is_desktop) '''Whether the instance can become focused. If focused, it'll lose focus when set to False. :attr:`is_focusable` is a :class:`~kivy.properties.BooleanProperty` and defaults to True on a desktop (i.e. `desktop` is True in :mod:`~kivy.config`), False otherwise. ''' focus = BooleanProperty(False) '''Whether the instance currently has focus. Setting it to True will bind to and/or request the keyboard, and input will be forwarded to the instance. Setting it to False will unbind and/or release the keyboard. For a given keyboard, only one widget can have its focus, so focusing one will automatically unfocus the other instance holding its focus. When using a software keyboard, please refer to the :attr:`~kivy.core.window.WindowBase.softinput_mode` property to determine how the keyboard display is handled. :attr:`focus` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' focused = focus '''An alias of :attr:`focus`. :attr:`focused` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. .. warning:: :attr:`focused` is an alias of :attr:`focus` and will be removed in 2.0.0. ''' keyboard_suggestions = BooleanProperty(True) '''If True provides auto suggestions on top of keyboard. This will only work if :attr:`input_type` is set to `text`, `url`, `mail` or `address`. .. warning:: On Android, `keyboard_suggestions` relies on `InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS` to work, but some keyboards just ignore this flag. If you want to disable suggestions at all on Android, you can set `input_type` to `null`, which will request the input method to run in a limited "generate key events" mode. .. versionadded:: 2.1.0 :attr:`keyboard_suggestions` is a :class:`~kivy.properties.BooleanProperty` and defaults to True ''' def _set_on_focus_next(self, instance, value): '''If changing focus, ensure your code does not create an infinite loop. eg: ```python widget.focus_next = widget widget.focus_previous = widget ``` ''' next_ = self._old_focus_next if next_ is value: # prevent infinite loop return if isinstance(next_, FocusBehavior): next_.focus_previous = None self._old_focus_next = value if value is None or value is StopIteration: return if not isinstance(value, FocusBehavior): raise ValueError('focus_next accepts only objects based on' ' FocusBehavior, or the `StopIteration` class.') value.focus_previous = self focus_next = ObjectProperty(None, allownone=True) '''The :class:`FocusBehavior` instance to acquire focus when tab is pressed and this instance has focus, if not `None` or `StopIteration`. When tab is pressed, focus cycles through all the :class:`FocusBehavior` widgets that are linked through :attr:`focus_next` and are focusable. If :attr:`focus_next` is `None`, it instead walks the children lists to find the next focusable widget. Finally, if :attr:`focus_next` is the `StopIteration` class, focus won't move forward, but end here. .. note: Setting :attr:`focus_next` automatically sets :attr:`focus_previous` of the other instance to point to this instance, if not None or `StopIteration`. Similarly, if it wasn't None or `StopIteration`, it also sets the :attr:`focus_previous` property of the instance previously in :attr:`focus_next` to `None`. Therefore, it is only required to set one of the :attr:`focus_previous` or :attr:`focus_next` links since the other side will be set automatically. :attr:`focus_next` is an :class:`~kivy.properties.ObjectProperty` and defaults to `None`. ''' def _set_on_focus_previous(self, instance, value): prev = self._old_focus_previous if prev is value: return if isinstance(prev, FocusBehavior): prev.focus_next = None self._old_focus_previous = value if value is None or value is StopIteration: return if not isinstance(value, FocusBehavior): raise ValueError('focus_previous accepts only objects based' 'on FocusBehavior, or the `StopIteration` class.') value.focus_next = self focus_previous = ObjectProperty(None, allownone=True) '''The :class:`FocusBehavior` instance to acquire focus when shift+tab is pressed on this instance, if not None or `StopIteration`. When shift+tab is pressed, focus cycles through all the :class:`FocusBehavior` widgets that are linked through :attr:`focus_previous` and are focusable. If :attr:`focus_previous` is `None`, it instead walks the children tree to find the previous focusable widget. Finally, if :attr:`focus_previous` is the `StopIteration` class, focus won't move backward, but end here. .. note: Setting :attr:`focus_previous` automatically sets :attr:`focus_next` of the other instance to point to this instance, if not None or `StopIteration`. Similarly, if it wasn't None or `StopIteration`, it also sets the :attr:`focus_next` property of the instance previously in :attr:`focus_previous` to `None`. Therefore, it is only required to set one of the :attr:`focus_previous` or :attr:`focus_next` links since the other side will be set automatically. :attr:`focus_previous` is an :class:`~kivy.properties.ObjectProperty` and defaults to `None`. ''' keyboard_mode = OptionProperty('auto', options=('auto', 'managed')) '''Determines how the keyboard visibility should be managed. 'auto' will result in the standard behavior of showing/hiding on focus. 'managed' requires setting the keyboard visibility manually, or calling the helper functions :meth:`show_keyboard` and :meth:`hide_keyboard`. :attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and defaults to 'auto'. Can be one of 'auto' or 'managed'. ''' input_type = OptionProperty('null', options=('null', 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address')) '''The kind of input keyboard to request. .. versionadded:: 1.8.0 .. versionchanged:: 2.1.0 Changed default value from `text` to `null`. Added `null` to options. .. warning:: As the default value has been changed, you may need to adjust `input_type` in your code. :attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and defaults to 'null'. Can be one of 'null', 'text', 'number', 'url', 'mail', 'datetime', 'tel' or 'address'. ''' unfocus_on_touch = BooleanProperty(_keyboard_mode not in ('multi', 'systemandmulti')) '''Whether a instance should lose focus when clicked outside the instance. When a user clicks on a widget that is focus aware and shares the same keyboard as this widget (which in the case with only one keyboard), then as the other widgets gain focus, this widget loses focus. In addition to that, if this property is `True`, clicking on any widget other than this widget, will remove focus from this widget. :attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` and defaults to `False` if the `keyboard_mode` in :attr:`~kivy.config.Config` is `'multi'` or `'systemandmulti'`, otherwise it defaults to `True`. ''' def __init__(self, **kwargs): self._old_focus_next = None self._old_focus_previous = None super(FocusBehavior, self).__init__(**kwargs) self._keyboard_mode = _keyboard_mode fbind = self.fbind fbind('focus', self._on_focus) fbind('disabled', self._on_focusable) fbind('is_focusable', self._on_focusable) fbind('focus_next', self._set_on_focus_next) fbind('focus_previous', self._set_on_focus_previous) def _on_focusable(self, instance, value): if self.disabled or not self.is_focusable: self.focus = False def _on_focus(self, instance, value, *largs): if self.keyboard_mode == 'auto': if value: self._bind_keyboard() else: self._unbind_keyboard() def _ensure_keyboard(self): if self._keyboard is None: self._requested_keyboard = True keyboard = self._keyboard = EventLoop.window.request_keyboard( self._keyboard_released, self, input_type=self.input_type, keyboard_suggestions=self.keyboard_suggestions, ) keyboards = FocusBehavior._keyboards if keyboard not in keyboards: keyboards[keyboard] = None def _bind_keyboard(self): self._ensure_keyboard() keyboard = self._keyboard if not keyboard or self.disabled or not self.is_focusable: self.focus = False return keyboards = FocusBehavior._keyboards old_focus = keyboards[keyboard] # keyboard should be in dict if old_focus: old_focus.focus = False # keyboard shouldn't have been released here, see keyboard warning keyboards[keyboard] = self keyboard.bind(on_key_down=self.keyboard_on_key_down, on_key_up=self.keyboard_on_key_up, on_textinput=self.keyboard_on_textinput) def _unbind_keyboard(self): keyboard = self._keyboard if keyboard: keyboard.unbind(on_key_down=self.keyboard_on_key_down, on_key_up=self.keyboard_on_key_up, on_textinput=self.keyboard_on_textinput) if self._requested_keyboard: keyboard.release() self._keyboard = None self._requested_keyboard = False del FocusBehavior._keyboards[keyboard] else: FocusBehavior._keyboards[keyboard] = None def keyboard_on_textinput(self, window, text): pass def _keyboard_released(self): self.focus = False def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if (not self.disabled and self.is_focusable and ('button' not in touch.profile or not touch.button.startswith('scroll'))): self.focus = True FocusBehavior.ignored_touch.append(touch) return super(FocusBehavior, self).on_touch_down(touch) @staticmethod def _handle_post_on_touch_up(touch): ''' Called by window after each touch has finished. ''' touches = FocusBehavior.ignored_touch if touch in touches: touches.remove(touch) return if 'button' in touch.profile and touch.button in\ ('scrollup', 'scrolldown', 'scrollleft', 'scrollright'): return for focusable in list(FocusBehavior._keyboards.values()): if focusable is None or not focusable.unfocus_on_touch: continue focusable.focus = False def _get_focus_next(self, focus_dir): current = self walk_tree = 'walk' if focus_dir == 'focus_next' else 'walk_reverse' while 1: # if we hit a focusable, walk through focus_xxx while getattr(current, focus_dir) is not None: current = getattr(current, focus_dir) if current is self or current is StopIteration: return None # make sure we don't loop forever if current.is_focusable and not current.disabled: return current # hit unfocusable, walk widget tree itr = getattr(current, walk_tree)(loopback=True) if focus_dir == 'focus_next': next(itr) # current is returned first when walking forward for current in itr: if isinstance(current, FocusBehavior): break # why did we stop if isinstance(current, FocusBehavior): if current is self: return None if current.is_focusable and not current.disabled: return current else: return None
[docs] def get_focus_next(self): '''Returns the next focusable widget using either :attr:`focus_next` or the :attr:`children` similar to the order when tabbing forwards with the ``tab`` key. ''' return self._get_focus_next('focus_next')
[docs] def get_focus_previous(self): '''Returns the previous focusable widget using either :attr:`focus_previous` or the :attr:`children` similar to the order when the ``tab`` + ``shift`` keys are triggered together. ''' return self._get_focus_next('focus_previous')
[docs] def keyboard_on_key_down(self, window, keycode, text, modifiers): '''The method bound to the keyboard when the instance has focus. When the instance becomes focused, this method is bound to the keyboard and will be called for every input press. The parameters are the same as :meth:`kivy.core.window.WindowBase.on_key_down`. When overwriting the method in the derived widget, super should be called to enable tab cycling. If the derived widget wishes to use tab for its own purposes, it can call super after it has processed the character (if it does not wish to consume the tab). Similar to other keyboard functions, it should return True if the key was consumed. ''' if keycode[1] == 'tab': # deal with cycle modifiers = set(modifiers) if {'ctrl', 'alt', 'meta', 'super', 'compose'} & modifiers: return False if 'shift' in modifiers: next = self.get_focus_previous() else: next = self.get_focus_next() if next: self.focus = False next.focus = True return True return False
[docs] def keyboard_on_key_up(self, window, keycode): '''The method bound to the keyboard when the instance has focus. When the instance becomes focused, this method is bound to the keyboard and will be called for every input release. The parameters are the same as :meth:`kivy.core.window.WindowBase.on_key_up`. When overwriting the method in the derived widget, super should be called to enable de-focusing on escape. If the derived widget wishes to use escape for its own purposes, it can call super after it has processed the character (if it does not wish to consume the escape). See :meth:`keyboard_on_key_down` ''' if keycode[1] == 'escape': self.focus = False return True return False
[docs] def show_keyboard(self): ''' Convenience function to show the keyboard in managed mode. ''' if self.keyboard_mode == 'managed': self._bind_keyboard()
[docs] def hide_keyboard(self): ''' Convenience function to hide the keyboard in managed mode. ''' if self.keyboard_mode == 'managed': self._unbind_keyboard()