diff --git a/examples/demo/kivycatalog/main.py b/examples/demo/kivycatalog/main.py index 2ee3fb7d6..e6e180f21 100755 --- a/examples/demo/kivycatalog/main.py +++ b/examples/demo/kivycatalog/main.py @@ -57,7 +57,7 @@ for class_name in CONTAINER_CLASSES: class KivyRenderTextInput(CodeInput): - def _keyboard_on_key_down(self, window, keycode, text, modifiers): + def keyboard_on_key_down(self, window, keycode, text, modifiers): is_osx = sys.platform == 'darwin' # Keycodes on OSX: ctrl, cmd = 64, 1024 @@ -70,7 +70,7 @@ class KivyRenderTextInput(CodeInput): self.catalog.change_kv(True) return - super(KivyRenderTextInput, self)._keyboard_on_key_down( + super(KivyRenderTextInput, self).keyboard_on_key_down( window, keycode, text, modifiers) diff --git a/kivy/core/window/__init__.py b/kivy/core/window/__init__.py index ba35daad7..ac3c30d0d 100755 --- a/kivy/core/window/__init__.py +++ b/kivy/core/window/__init__.py @@ -24,6 +24,7 @@ from kivy.properties import ListProperty, ObjectProperty, AliasProperty, \ NumericProperty, OptionProperty, StringProperty, BooleanProperty from kivy.utils import platform, reify from kivy.context import get_current_context +from kivy.uix.behaviors import FocusBehavior # late import VKeyboard = None @@ -851,6 +852,7 @@ class WindowBase(EventDispatcher): self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) + FocusBehavior._handle_post_on_touch_up(me) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. diff --git a/kivy/core/window/window_sdl2.py b/kivy/core/window/window_sdl2.py index 24981a241..c0c29a80f 100644 --- a/kivy/core/window/window_sdl2.py +++ b/kivy/core/window/window_sdl2.py @@ -344,6 +344,7 @@ class WindowSDL(WindowBase): elif action in ('keydown', 'keyup'): mod, key, scancode, kstr = args + print key if mod in self._meta_keys: try: kstr = unichr(key) diff --git a/kivy/uix/behaviors.py b/kivy/uix/behaviors.py index 83ce4d7e2..84b50e12e 100644 --- a/kivy/uix/behaviors.py +++ b/kivy/uix/behaviors.py @@ -37,10 +37,12 @@ import string # When we are generating documentation, Config doesn't exist _scroll_timeout = _scroll_distance = 0 _is_desktop = False +_keyboard_mode = 'system' if Config: _scroll_timeout = Config.getint('widgets', 'scroll_timeout') _scroll_distance = Config.getint('widgets', 'scroll_distance') _is_desktop = Config.getboolean('kivy', 'desktop') + _keyboard_mode = Config.get('kivy', 'keyboard_mode') class ButtonBehavior(object): @@ -492,11 +494,28 @@ class FocusBehavior(object): future version. ''' - _win = None + _focus_win = None _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 class, not instance variable. + ''' + def _set_keyboard(self, value): focused = self.focused keyboard = self._keyboard @@ -535,6 +554,13 @@ class FocusBehavior(object): :attr:`keyboard` is a :class:`~kivy.properties.AliasProperty`, defaults to None. + .. note:: + + When Config's `keyboard_mode` is multi, each new touch is considered + a touch by a different user and will focus (if clicked on a + focusable) with a new keyboard. Already focused elements will not lose + their focus (even if clicked on a unfocusable). + .. note: If the keyboard property is set, that keyboard will be used when the @@ -668,15 +694,52 @@ class FocusBehavior(object): defaults to `None`. ''' + 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()``. + + :attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and + defaults to 'auto'. Can be one of 'auto' or 'managed'. + ''' + + input_type = OptionProperty('text', options=('text', 'number', 'url', + 'mail', 'datetime', 'tel', + 'address')) + '''The kind of input keyboard to request. + + .. versionadded:: 1.8.0 + + :attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and + defaults to 'text'. Can be one of 'text', 'number', 'url', 'mail', + 'datetime', 'tel', '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 the this widget (which in the case of only one keyboard, are + all focus aware widgets), then as the other widgets gains 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 form this + widget. + + :attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty`, + 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 self.bind(focused=self._on_focused, disabled=self._on_focusable, is_focusable=self._on_focusable, - # don't be at mercy of child calling super - on_touch_down=self._focus_on_touch_down, focus_next=self._set_on_focus_next, focus_previous=self._set_on_focus_previous) @@ -685,23 +748,25 @@ class FocusBehavior(object): self.focused = False def _on_focused(self, instance, value, *largs): - if value: - self._bind_keyboard() - else: - self._unbind_keyboard() + if self.keyboard_mode == 'auto': + if value: + self._bind_keyboard() + else: + self._unbind_keyboard() def _ensure_keyboard(self): if self._keyboard is None: - win = self._win + win = self._focus_win if not win: - self._win = win = EventLoop.window + self._focus_win = win = EventLoop.window if not win: Logger.warning('FocusBehavior: ' 'Cannot focus the element, unable to get root window') return self._requested_keyboard = True keyboard = self._keyboard =\ - win.request_keyboard(self._keyboard_released, self) + win.request_keyboard(self._keyboard_released, self, + input_type=self.input_type) keyboards = FocusBehavior._keyboards if keyboard not in keyboards: keyboards[keyboard] = None @@ -738,12 +803,28 @@ class FocusBehavior(object): def _keyboard_released(self): self.focused = False - def _focus_on_touch_down(self, instance, touch): - if not self.disabled and self.is_focusable and\ - self.collide_point(*touch.pos) and ('button' not in touch.profile - or not touch.button.startswith('scroll')): + 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.focused = True - return False + 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 + for focusable in FocusBehavior._keyboards.values(): + if focusable is None or not focusable.unfocus_on_touch: + continue + focusable.focused = False def _get_focus_next(self, focus_dir): current = self @@ -755,7 +836,7 @@ class FocusBehavior(object): 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: + if current.is_focusable and not current.disabled: return current # hit unfocusable, walk widget tree @@ -769,7 +850,7 @@ class FocusBehavior(object): if isinstance(current, FocusBehavior): if current is self: return None - if current.is_focusable: + if current.is_focusable and not current.disabled: return current else: return None @@ -823,6 +904,20 @@ class FocusBehavior(object): return True return False + 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() + class CompoundSelectionBehavior(object): '''Selection behavior implements the logic behind keyboard and touch diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py index c07f869fc..f6ec218f7 100644 --- a/kivy/uix/textinput.py +++ b/kivy/uix/textinput.py @@ -151,6 +151,7 @@ from kivy.compat import PY2 from kivy.logger import Logger from kivy.metrics import inch from kivy.utils import boundary, platform +from kivy.uix.behaviors import FocusBehavior from kivy.core.text import Label from kivy.graphics import Color, Rectangle @@ -221,6 +222,8 @@ class Selector(ButtonBehavior, Image): touch.push() touch.apply_transform_2d(self.to_widget) self._touch_diff = self.top - touch.y + if self.collide_point(*touch.pos): + FocusBehavior.ignored_touch.append(touch) return super(Selector, self).on_touch_down(touch) finally: touch.pop() @@ -244,6 +247,11 @@ class TextInputCutCopyPaste(Bubble): super(TextInputCutCopyPaste, self).__init__(**kwargs) Clock.schedule_interval(self._check_parent, .5) + def on_touch_down(self, touch): + if self.collide_point(*touch.pos): + FocusBehavior.ignored_touch.append(touch) + return super(TextInputCutCopyPaste, self).on_touch_down(touch) + def on_textinput(self, instance, value): global Clipboard if value and not Clipboard and not _is_desktop: @@ -314,7 +322,7 @@ class TextInputCutCopyPaste(Bubble): anim.start(self) -class TextInput(Widget): +class TextInput(FocusBehavior, Widget): '''TextInput class. See module documentation for more information. :Events: @@ -346,6 +354,18 @@ class TextInput(Widget): on the next clock cycle using :meth:`~kivy.clock.ClockBase.schedule_once`. + .. note:: + :attr:`~kivy.uix.behaviors.FocusBehavior.keyboard_mode`, + :meth:`~kivy.uix.behaviors.FocusBehavior.show_keyboard`, + :meth:`~kivy.uix.behaviors.FocusBehavior.hide_keyboard`, + and :attr:`~kivy.uix.behaviors.FocusBehavior.input_type`, + have been removed from :class:`TextInput` since they are now inherited + from :class:`~kivy.uix.behaviors.FocusBehavior`. + + .. versionchanged:: 1.8.1 + :class:`TextInput` now inherits from + :class:`~kivy.uix.behaviors.FocusBehavior`. + .. versionchanged:: 1.7.0 `on_double_tap`, `on_triple_tap` and `on_quad_touch` events added. ''' @@ -354,6 +374,7 @@ class TextInput(Widget): 'on_quad_touch') def __init__(self, **kwargs): + self.is_focusable = kwargs.get('is_focusable', True) self._win = None self._cursor_blink_time = Clock.get_time() self._cursor = [0, 0] @@ -375,7 +396,6 @@ class TextInput(Widget): self._hint_text_rects = [] self._label_cached = None self._line_options = None - self._keyboard = None self._keyboard_mode = Config.get('kivy', 'keyboard_mode') self._command_mode = False self._command = '' @@ -402,6 +422,13 @@ class TextInput(Widget): self.bind(font_size=self._trigger_refresh_line_options, font_name=self._trigger_refresh_line_options) + def set_focused(instance, value): + self.focused = value + + def handle_readonly(instance, value): + if value and (not _is_desktop or not self.allow_copy): + self.is_focusable = False + self.bind(padding=self._update_text_options, tab_width=self._update_text_options, font_size=self._update_text_options, @@ -409,7 +436,9 @@ class TextInput(Widget): size=self._update_text_options, password=self._update_text_options) - self.bind(pos=self._trigger_update_graphics) + self.bind(pos=self._trigger_update_graphics, focus=set_focused, + readonly=handle_readonly) + handle_readonly(self, self.readonly) self._trigger_position_handles = Clock.create_trigger( self._position_handles) @@ -424,10 +453,6 @@ class TextInput(Widget): # when the gl context is reloaded, trigger the text rendering again. _textinput_list.append(ref(self, TextInput._reload_remove_observer)) - def on_disabled(self, instance, value): - if value: - self.focus = False - def on_text_validate(self): pass @@ -961,14 +986,9 @@ class TextInput(Widget): touch_pos = touch.pos if not self.collide_point(*touch_pos): - if self._keyboard_mode == 'multi': - if self.readonly: - self.focus = False - else: - self.focus = False return False - if not self.focus: - self.focus = True + if super(TextInput, self).on_touch_down(touch): + return True # Check for scroll wheel if 'button' in touch.profile and touch.button.startswith('scroll'): @@ -1298,76 +1318,29 @@ class TextInput(Widget): if wr in _textinput_list: _textinput_list.remove(wr) - def _set_window(self, *largs): + def on_focused(self, instance, value, *largs): + self.focus = value + 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: - #XXX where do `value` comes from? - Clock.schedule_once(partial(self.on_focus, self, largs), 0) + Logger.warning('Textinput: unable to get root window') return - - def on_focus(self, instance, value, *largs): - self._set_window(*largs) + self.cancel_selection() + self._hide_cut_copy_paste(win) if value: - if self.keyboard_mode != 'managed': - self._bind_keyboard() + if (not (self.readonly or self.disabled) or _is_desktop and + self._keyboard_mode == 'system'): + Clock.schedule_interval(self._do_blink_cursor, 1 / 2.) + self._editable = True + else: + self._editable = False else: - 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 - _is_desktop and self._keyboard_mode == 'system') - - if not _is_desktop and not editable: - return - - 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: - self.focus = False + Clock.unschedule(self._do_blink_cursor) + self._hide_handles(win) + self._win = None def _ensure_clipboard(self): global Clipboard @@ -1414,12 +1387,6 @@ class TextInput(Widget): self.delete_selection() self.insert_text(data) - def _keyboard_released(self): - # Callback called when the real keyboard is taken by someone else - # called by the window if the keyboard is taken by somebody else - # FIXME: handle virtual keyboard. - self.focus = False - def _get_text_width(self, text, tab_width, _label_cached): # Return the width of a text, according to the current line options kw = self._get_line_options() @@ -1962,7 +1929,7 @@ class TextInput(Widget): if self._selection: self._update_selection(True) - def _keyboard_on_key_down(self, window, keycode, text, modifiers): + def keyboard_on_key_down(self, window, keycode, text, modifiers): # Keycodes on OSX: ctrl, cmd = 64, 1024 key, key_str = keycode @@ -1973,13 +1940,17 @@ class TextInput(Widget): _is_osx and modifiers == ['meta'])) is_interesting_key = key in (list(self.interesting_keys.keys()) + [27]) + if not self.write_tab and super(TextInput, + self).keyboard_on_key_down(window, keycode, text, modifiers): + return True + if not self._editable: # duplicated but faster testing for non-editable keys if text and not is_interesting_key: if is_shortcut and key == ord('c'): self.copy() elif key == 27: - self.focus = False + self.focused = False return True if text and not is_interesting_key: @@ -2060,7 +2031,7 @@ class TextInput(Widget): self._hide_handles(win) if key == 27: # escape - self.focus = False + self.focused = False return True elif key == 9: # tab self.insert_text(u'\t') @@ -2071,7 +2042,7 @@ class TextInput(Widget): key = (None, None, k, 1) self._key_down(key) - def _keyboard_on_key_up(self, window, keycode): + def keyboard_on_key_up(self, window, keycode): key, key_str = keycode k = self.interesting_keys.get(key) if k: @@ -2496,6 +2467,12 @@ class TextInput(Widget): show selection when TextInput is focused, you should delay (use Clock.schedule) the call to the functions for selecting text (select_all, select_text). + + ..versionchanged:: 1.8.1 + :class:`TextInput` now inherits from + :class:`~kivy.uix.behaviors.FocusBehavior` and :attr:`focus` is now + an alias for :attr:`focused`. Setting either one will also set the + other. ''' def _get_text(self, encode=True): @@ -2629,18 +2606,6 @@ class TextInput(Widget): defaults to 0. ''' - input_type = OptionProperty('text', options=('text', 'number', 'url', - 'mail', 'datetime', 'tel', - 'address')) - '''The kind of input, keyboard to request - - .. versionadded:: 1.8.0 - - :attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and - defaults to 'text'. Can be one of 'text', 'number', 'url', 'mail', - 'datetime', 'tel', 'address'. - ''' - input_filter = ObjectProperty(None, allownone=True) ''' Filters the input according to the specified mode, if not None. If None, no filtering is applied. @@ -2700,32 +2665,17 @@ class TextInput(Widget): 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()``. + write_tab = BooleanProperty(True) + '''Whether the tab key should move focus to the next widget or if it should + enter a tab in the :class:`TextInput`. If `True` a tab will be written, + otherwise, focus will move to the next widget. - .. versionadded:: 1.8.0 + .. versionadded:: 1.8.1 - :attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and - defaults to 'auto'. Can be one of 'auto' or 'managed'. + :attr:`write_tab` is a :class:`~kivy.properties.BooleanProperty` and + defaults to `True`. ''' - 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