first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -0,0 +1,95 @@
'''
Behaviors
=========
.. versionadded:: 1.8.0
Behavior mixin classes
----------------------
This module implements behaviors that can be
`mixed in <https://en.wikipedia.org/wiki/Mixin>`_
with existing base widgets. The idea behind these classes is to encapsulate
properties and events associated with certain types of widgets.
Isolating these properties and events in a mixin class allows you to define
your own implementation for standard kivy widgets that can act as drop-in
replacements. This means you can re-style and re-define widgets as desired
without breaking compatibility: as long as they implement the behaviors
correctly, they can simply replace the standard widgets.
Adding behaviors
----------------
Say you want to add :class:`~kivy.uix.button.Button` capabilities to an
:class:`~kivy.uix.image.Image`, you could do::
class IconButton(ButtonBehavior, Image):
pass
This would give you an :class:`~kivy.uix.image.Image` with the events and
properties inherited from :class:`ButtonBehavior`. For example, the *on_press*
and *on_release* events would be fired when appropriate::
class IconButton(ButtonBehavior, Image):
def on_press(self):
print("on_press")
Or in kv:
.. code-block:: kv
IconButton:
on_press: print('on_press')
Naturally, you could also bind to any property changes the behavior class
offers:
.. code-block:: python
def state_changed(*args):
print('state changed')
button = IconButton()
button.bind(state=state_changed)
.. note::
The behavior class must always be _before_ the widget class. If you don't
specify the inheritance in this order, the behavior will not work because
the behavior methods are overwritten by the class method listed first.
Similarly, if you combine a behavior class with a class which
requires the use of the methods also defined by the behavior class, the
resulting class may not function properly. For example, when combining the
:class:`ButtonBehavior` with a :class:`~kivy.uix.slider.Slider`, both of
which use the :meth:`~kivy.uix.widget.Widget.on_touch_up` method,
the resulting class may not work properly.
.. versionchanged:: 1.9.1
The individual behavior classes, previously in one big `behaviors.py`
file, has been split into a single file for each class under the
:mod:`~kivy.uix.behaviors` module. All the behaviors are still imported
in the :mod:`~kivy.uix.behaviors` module so they are accessible as before
(e.g. both `from kivy.uix.behaviors import ButtonBehavior` and
`from kivy.uix.behaviors.button import ButtonBehavior` work).
'''
__all__ = ('ButtonBehavior', 'ToggleButtonBehavior', 'DragBehavior',
'FocusBehavior', 'CompoundSelectionBehavior',
'CodeNavigationBehavior', 'EmacsBehavior', 'CoverBehavior',
'TouchRippleBehavior', 'TouchRippleButtonBehavior')
from kivy.uix.behaviors.button import ButtonBehavior
from kivy.uix.behaviors.togglebutton import ToggleButtonBehavior
from kivy.uix.behaviors.drag import DragBehavior
from kivy.uix.behaviors.focus import FocusBehavior
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
from kivy.uix.behaviors.codenavigation import CodeNavigationBehavior
from kivy.uix.behaviors.emacs import EmacsBehavior
from kivy.uix.behaviors.cover import CoverBehavior
from kivy.uix.behaviors.touchripple import TouchRippleBehavior
from kivy.uix.behaviors.touchripple import TouchRippleButtonBehavior

View file

@ -0,0 +1,212 @@
'''
Button Behavior
===============
The :class:`~kivy.uix.behaviors.button.ButtonBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
:class:`~kivy.uix.button.Button` behavior. You can combine this class with
other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
alternative buttons that preserve Kivy button behavior.
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.
Example
-------
The following example adds button behavior to an image to make a checkbox that
behaves like a button::
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
class MyButton(ButtonBehavior, Image):
def __init__(self, **kwargs):
super(MyButton, self).__init__(**kwargs)
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
def on_press(self):
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
def on_release(self):
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
class SampleApp(App):
def build(self):
return MyButton()
SampleApp().run()
See :class:`~kivy.uix.behaviors.ButtonBehavior` for details.
'''
__all__ = ('ButtonBehavior', )
from kivy.clock import Clock
from kivy.config import Config
from kivy.properties import OptionProperty, ObjectProperty, \
BooleanProperty, NumericProperty
from time import time
class ButtonBehavior(object):
'''
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
:class:`~kivy.uix.button.Button` behavior. Please see the
:mod:`button behaviors module <kivy.uix.behaviors.button>` documentation
for more information.
:Events:
`on_press`
Fired when the button is pressed.
`on_release`
Fired when the button is released (i.e. the touch/click that
pressed the button goes away).
'''
state = OptionProperty('normal', options=('normal', 'down'))
'''The state of the button, must be one of 'normal' or 'down'.
The state is 'down' only when the button is currently touched/clicked,
otherwise its 'normal'.
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
to 'normal'.
'''
last_touch = ObjectProperty(None)
'''Contains the last relevant touch received by the Button. This can
be used in `on_press` or `on_release` in order to know which touch
dispatched the event.
.. versionadded:: 1.8.0
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
defaults to `None`.
'''
min_state_time = NumericProperty(0)
'''The minimum period of time which the widget must remain in the
`'down'` state.
.. versionadded:: 1.9.1
:attr:`min_state_time` is a float and defaults to 0.035. This value is
taken from :class:`~kivy.config.Config`.
'''
always_release = BooleanProperty(False)
'''This determines whether or not the widget fires an `on_release` event if
the touch_up is outside the widget.
.. versionadded:: 1.9.0
.. versionchanged:: 1.10.0
The default value is now False.
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
defaults to `False`.
'''
def __init__(self, **kwargs):
self.register_event_type('on_press')
self.register_event_type('on_release')
if 'min_state_time' not in kwargs:
self.min_state_time = float(Config.get('graphics',
'min_state_time'))
super(ButtonBehavior, self).__init__(**kwargs)
self.__state_event = None
self.__touch_time = None
self.fbind('state', self.cancel_event)
def _do_press(self):
self.state = 'down'
def _do_release(self, *args):
self.state = 'normal'
def cancel_event(self, *args):
if self.__state_event:
self.__state_event.cancel()
self.__state_event = None
def on_touch_down(self, touch):
if super(ButtonBehavior, self).on_touch_down(touch):
return True
if touch.is_mouse_scrolling:
return False
if not self.collide_point(touch.x, touch.y):
return False
if self in touch.ud:
return False
touch.grab(self)
touch.ud[self] = True
self.last_touch = touch
self.__touch_time = time()
self._do_press()
self.dispatch('on_press')
return True
def on_touch_move(self, touch):
if touch.grab_current is self:
return True
if super(ButtonBehavior, self).on_touch_move(touch):
return True
return self in touch.ud
def on_touch_up(self, touch):
if touch.grab_current is not self:
return super(ButtonBehavior, self).on_touch_up(touch)
assert self in touch.ud
touch.ungrab(self)
self.last_touch = touch
if (not self.always_release and
not self.collide_point(*touch.pos)):
self._do_release()
return
touchtime = time() - self.__touch_time
if touchtime < self.min_state_time:
self.__state_event = Clock.schedule_once(
self._do_release, self.min_state_time - touchtime)
else:
self._do_release()
self.dispatch('on_release')
return True
def on_press(self):
pass
def on_release(self):
pass
def trigger_action(self, duration=0.1):
'''Trigger whatever action(s) have been bound to the button by calling
both the on_press and on_release callbacks.
This is similar to a quick button press without using any touch events,
but note that like most kivy code, this is not guaranteed to be safe to
call from external threads. If needed use
:class:`Clock <kivy.clock.Clock>` to safely schedule this function and
the resulting callbacks to be called from the main thread.
Duration is the length of the press in seconds. Pass 0 if you want
the action to happen instantly.
.. versionadded:: 1.8.0
'''
self._do_press()
self.dispatch('on_press')
def trigger_release(dt):
self._do_release()
self.dispatch('on_release')
if not duration:
trigger_release(0)
else:
Clock.schedule_once(trigger_release, duration)

View file

@ -0,0 +1,167 @@
'''
Code Navigation Behavior
========================
The :class:`~kivy.uix.bahviors.CodeNavigationBehavior` modifies navigation
behavior in the :class:`~kivy.uix.textinput.TextInput`, making it work like an
IDE instead of a word processor.
Using this mixin gives the TextInput the ability to recognize whitespace,
punctuation and case variations (e.g. CamelCase) when moving over text. It
is currently used by the :class:`~kivy.uix.codeinput.CodeInput` widget.
'''
__all__ = ('CodeNavigationBehavior', )
from kivy.event import EventDispatcher
import string
class CodeNavigationBehavior(EventDispatcher):
'''Code navigation behavior. Modifies the navigation behavior in TextInput
to work like an IDE instead of a word processor. Please see the
:mod:`code navigation behaviors module <kivy.uix.behaviors.codenavigation>`
documentation for more information.
.. versionadded:: 1.9.1
'''
def _move_cursor_word_left(self, index=None):
pos = index or self.cursor_index()
pos -= 1
if pos == 0:
return 0, 0
col, row = self.get_cursor_from_index(pos)
lines = self._lines
ucase = string.ascii_uppercase
lcase = string.ascii_lowercase
ws = string.whitespace
punct = string.punctuation
mode = 'normal'
rline = lines[row]
c = rline[col] if len(rline) > col else '\n'
if c in ws:
mode = 'ws'
elif c == '_':
mode = 'us'
elif c in punct:
mode = 'punct'
elif c not in ucase:
mode = 'camel'
while True:
if col == -1:
if row == 0:
return 0, 0
row -= 1
rline = lines[row]
col = len(rline)
lc = c
c = rline[col] if len(rline) > col else '\n'
if c == '\n':
if lc not in ws:
col += 1
break
if mode in ('normal', 'camel') and c in ws:
col += 1
break
if mode in ('normal', 'camel') and c in punct:
col += 1
break
if mode == 'camel' and c in ucase:
break
if mode == 'punct' and (c == '_' or c not in punct):
col += 1
break
if mode == 'us' and c != '_' and (c in punct or c in ws):
col += 1
break
if mode == 'us' and c != '_':
mode = ('normal' if c in ucase
else 'ws' if c in ws
else 'camel')
elif mode == 'ws' and c not in ws:
mode = ('normal' if c in ucase
else 'us' if c == '_'
else 'punct' if c in punct
else 'camel')
col -= 1
if col > len(rline):
if row == len(lines) - 1:
return row, len(lines[row])
row += 1
col = 0
return col, row
def _move_cursor_word_right(self, index=None):
pos = index or self.cursor_index()
col, row = self.get_cursor_from_index(pos)
lines = self._lines
mrow = len(lines) - 1
if row == mrow and col == len(lines[row]):
return col, row
ucase = string.ascii_uppercase
lcase = string.ascii_lowercase
ws = string.whitespace
punct = string.punctuation
mode = 'normal'
rline = lines[row]
c = rline[col] if len(rline) > col else '\n'
if c in ws:
mode = 'ws'
elif c == '_':
mode = 'us'
elif c in punct:
mode = 'punct'
elif c in lcase:
mode = 'camel'
while True:
if mode in ('normal', 'camel', 'punct') and c in ws:
mode = 'ws'
elif mode in ('normal', 'camel') and c == '_':
mode = 'us'
elif mode == 'normal' and c not in ucase:
mode = 'camel'
if mode == 'us':
if c in ws:
mode = 'ws'
elif c != '_':
break
if mode == 'ws' and c not in ws:
break
if mode == 'camel' and c in ucase:
break
if mode == 'punct' and (c == '_' or c not in punct):
break
if mode != 'punct' and c != '_' and c in punct:
break
col += 1
if col > len(rline):
if row == mrow:
return len(rline), mrow
row += 1
rline = lines[row]
col = 0
c = rline[col] if len(rline) > col else '\n'
if c == '\n':
break
return col, row

View file

@ -0,0 +1,689 @@
'''
Compound Selection Behavior
===========================
The :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class implements the logic
behind keyboard and touch selection of selectable widgets managed by the
derived widget. For example, it can be combined with a
:class:`~kivy.uix.gridlayout.GridLayout` to add selection to the layout.
Compound selection concepts
---------------------------
At its core, it keeps a dynamic list of widgets that can be selected.
Then, as the touches and keyboard input are passed in, it selects one or
more of the widgets based on these inputs. For example, it uses the mouse
scroll and keyboard up/down buttons to scroll through the list of widgets.
Multiselection can also be achieved using the keyboard shift and ctrl keys.
Finally, in addition to the up/down type keyboard inputs, compound selection
can also accept letters from the keyboard to be used to select nodes with
associated strings that start with those letters, similar to how files
are selected by a file browser.
Selection mechanics
-------------------
When the controller needs to select a node, it calls :meth:`select_node` and
:meth:`deselect_node`. Therefore, they must be overwritten in order alter
node selection. By default, the class doesn't listen for keyboard or
touch events, so the derived widget must call
:meth:`select_with_touch`, :meth:`select_with_key_down`, and
:meth:`select_with_key_up` on events that it wants to pass on for selection
purposes.
Example
-------
To add selection to a grid layout which will contain
:class:`~kivy.uix.Button` widgets. For each button added to the layout, you
need to bind the :attr:`~kivy.uix.widget.Widget.on_touch_down` of the button
to :meth:`select_with_touch` to pass on the touch events::
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.core.window import Window
from kivy.app import App
class SelectableGrid(FocusBehavior, CompoundSelectionBehavior, GridLayout):
def keyboard_on_key_down(self, window, keycode, text, modifiers):
"""Based on FocusBehavior that provides automatic keyboard
access, key presses will be used to select children.
"""
if super(SelectableGrid, self).keyboard_on_key_down(
window, keycode, text, modifiers):
return True
if self.select_with_key_down(window, keycode, text, modifiers):
return True
return False
def keyboard_on_key_up(self, window, keycode):
"""Based on FocusBehavior that provides automatic keyboard
access, key release will be used to select children.
"""
if super(SelectableGrid, self).keyboard_on_key_up(window, keycode):
return True
if self.select_with_key_up(window, keycode):
return True
return False
def add_widget(self, widget, *args, **kwargs):
""" Override the adding of widgets so we can bind and catch their
*on_touch_down* events. """
widget.bind(on_touch_down=self.button_touch_down,
on_touch_up=self.button_touch_up)
return super(SelectableGrid, self)\
.add_widget(widget, *args, **kwargs)
def button_touch_down(self, button, touch):
""" Use collision detection to select buttons when the touch occurs
within their area. """
if button.collide_point(*touch.pos):
self.select_with_touch(button, touch)
def button_touch_up(self, button, touch):
""" Use collision detection to de-select buttons when the touch
occurs outside their area and *touch_multiselect* is not True. """
if not (button.collide_point(*touch.pos) or
self.touch_multiselect):
self.deselect_node(button)
def select_node(self, node):
node.background_color = (1, 0, 0, 1)
return super(SelectableGrid, self).select_node(node)
def deselect_node(self, node):
node.background_color = (1, 1, 1, 1)
super(SelectableGrid, self).deselect_node(node)
def on_selected_nodes(self, grid, nodes):
print("Selected nodes = {0}".format(nodes))
class TestApp(App):
def build(self):
grid = SelectableGrid(cols=3, rows=2, touch_multiselect=True,
multiselect=True)
for i in range(0, 6):
grid.add_widget(Button(text="Button {0}".format(i)))
return grid
TestApp().run()
.. warning::
This code is still experimental, and its API is subject to change in a
future version.
'''
__all__ = ('CompoundSelectionBehavior', )
from time import time
from os import environ
from kivy.properties import NumericProperty, BooleanProperty, ListProperty
if 'KIVY_DOC' not in environ:
from kivy.config import Config
_is_desktop = Config.getboolean('kivy', 'desktop')
else:
_is_desktop = False
class CompoundSelectionBehavior(object):
'''The Selection behavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
implements the logic behind keyboard and touch
selection of selectable widgets managed by the derived widget. Please see
the :mod:`compound selection behaviors module
<kivy.uix.behaviors.compoundselection>` documentation
for more information.
.. versionadded:: 1.9.0
'''
selected_nodes = ListProperty([])
'''The list of selected nodes.
.. note::
Multiple nodes can be selected right after one another e.g. using the
keyboard. When listening to :attr:`selected_nodes`, one should be
aware of this.
:attr:`selected_nodes` is a :class:`~kivy.properties.ListProperty` and
defaults to the empty list, []. It is read-only and should not be modified.
'''
touch_multiselect = BooleanProperty(False)
'''A special touch mode which determines whether touch events, as
processed by :meth:`select_with_touch`, will add the currently touched
node to the selection, or if it will clear the selection before adding the
node. This allows the selection of multiple nodes by simply touching them.
This is different from :attr:`multiselect` because when it is True,
simply touching an unselected node will select it, even if ctrl is not
pressed. If it is False, however, ctrl must be pressed in order to
add to the selection when :attr:`multiselect` is True.
.. note::
:attr:`multiselect`, when False, will disable
:attr:`touch_multiselect`.
:attr:`touch_multiselect` is a :class:`~kivy.properties.BooleanProperty`
and defaults to False.
'''
multiselect = BooleanProperty(False)
'''Determines whether multiple nodes can be selected. If enabled, keyboard
shift and ctrl selection, optionally combined with touch, for example, will
be able to select multiple widgets in the normally expected manner.
This dominates :attr:`touch_multiselect` when False.
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and
defaults to False.
'''
touch_deselect_last = BooleanProperty(not _is_desktop)
'''Determines whether the last selected node can be deselected when
:attr:`multiselect` or :attr:`touch_multiselect` is False.
.. versionadded:: 1.10.0
:attr:`touch_deselect_last` is a :class:`~kivy.properties.BooleanProperty`
and defaults to True on mobile, False on desktop platforms.
'''
keyboard_select = BooleanProperty(True)
'''Determines whether the keyboard can be used for selection. If False,
keyboard inputs will be ignored.
:attr:`keyboard_select` is a :class:`~kivy.properties.BooleanProperty`
and defaults to True.
'''
page_count = NumericProperty(10)
'''Determines by how much the selected node is moved up or down, relative
to the position of the last selected node, when pageup (or pagedown) is
pressed.
:attr:`page_count` is a :class:`~kivy.properties.NumericProperty` and
defaults to 10.
'''
up_count = NumericProperty(1)
'''Determines by how much the selected node is moved up or down, relative
to the position of the last selected node, when the up (or down) arrow on
the keyboard is pressed.
:attr:`up_count` is a :class:`~kivy.properties.NumericProperty` and
defaults to 1.
'''
right_count = NumericProperty(1)
'''Determines by how much the selected node is moved up or down, relative
to the position of the last selected node, when the right (or left) arrow
on the keyboard is pressed.
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
defaults to 1.
'''
scroll_count = NumericProperty(0)
'''Determines by how much the selected node is moved up or down, relative
to the position of the last selected node, when the mouse scroll wheel is
scrolled.
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
defaults to 0.
'''
nodes_order_reversed = BooleanProperty(True)
''' (Internal) Indicates whether the order of the nodes as displayed top-
down is reversed compared to their order in :meth:`get_selectable_nodes`
(e.g. how the children property is reversed compared to how
it's displayed).
'''
text_entry_timeout = NumericProperty(1.)
'''When typing characters in rapid succession (i.e. the time difference
since the last character is less than :attr:`text_entry_timeout`), the
keys get concatenated and the combined text is passed as the key argument
of :meth:`goto_node`.
.. versionadded:: 1.10.0
'''
_anchor = None # the last anchor node selected (e.g. shift relative node)
# the idx may be out of sync
_anchor_idx = 0 # cache indexes in case list hasn't changed
_last_selected_node = None # the absolute last node selected
_last_node_idx = 0
_ctrl_down = False # if it's pressed - for e.g. shift selection
_shift_down = False
# holds str used to find node, e.g. if word is typed. passed to goto_node
_word_filter = ''
_last_key_time = 0 # time since last press, for finding whole strs in node
_key_list = [] # keys that are already pressed, to not press continuously
_offset_counts = {} # cache of counts for faster access
def __init__(self, **kwargs):
super(CompoundSelectionBehavior, self).__init__(**kwargs)
self._key_list = []
def ensure_single_select(*l):
if (not self.multiselect) and len(self.selected_nodes) > 1:
self.clear_selection()
update_counts = self._update_counts
update_counts()
fbind = self.fbind
fbind('multiselect', ensure_single_select)
fbind('page_count', update_counts)
fbind('up_count', update_counts)
fbind('right_count', update_counts)
fbind('scroll_count', update_counts)
def select_with_touch(self, node, touch=None):
'''(internal) Processes a touch on the node. This should be called by
the derived widget when a node is touched and is to be used for
selection. Depending on the keyboard keys pressed and the
configuration, it could select or deslect this and other nodes in the
selectable nodes list, :meth:`get_selectable_nodes`.
:Parameters:
`node`
The node that received the touch. Can be None for a scroll
type touch.
`touch`
Optionally, the touch. Defaults to None.
:Returns:
bool, True if the touch was used, False otherwise.
'''
multi = self.multiselect
multiselect = multi and (self._ctrl_down or self.touch_multiselect)
range_select = multi and self._shift_down
if touch and 'button' in touch.profile and touch.button in\
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
node_src, idx_src = self._resolve_last_node()
node, idx = self.goto_node(touch.button, node_src, idx_src)
if node == node_src:
return False
if range_select:
self._select_range(multiselect, True, node, idx)
else:
if not multiselect:
self.clear_selection()
self.select_node(node)
return True
if node is None:
return False
if (node in self.selected_nodes and (not range_select)): # selected
if multiselect:
self.deselect_node(node)
else:
selected_node_count = len(self.selected_nodes)
self.clear_selection()
if not self.touch_deselect_last or selected_node_count > 1:
self.select_node(node)
elif range_select:
# keep anchor only if not multiselect (ctrl-type selection)
self._select_range(multiselect, not multiselect, node, 0)
else: # it's not selected at this point
if not multiselect:
self.clear_selection()
self.select_node(node)
return True
def select_with_key_down(self, keyboard, scancode, codepoint, modifiers,
**kwargs):
'''Processes a key press. This is called when a key press is to be used
for selection. Depending on the keyboard keys pressed and the
configuration, it could select or deselect nodes or node ranges
from the selectable nodes list, :meth:`get_selectable_nodes`.
The parameters are such that it could be bound directly to the
on_key_down event of a keyboard. Therefore, it is safe to be called
repeatedly when the key is held down as is done by the keyboard.
:Returns:
bool, True if the keypress was used, False otherwise.
'''
if not self.keyboard_select:
return False
keys = self._key_list
multi = self.multiselect
node_src, idx_src = self._resolve_last_node()
text = scancode[1]
if text == 'shift':
self._shift_down = True
elif text in ('ctrl', 'lctrl', 'rctrl'):
self._ctrl_down = True
elif (multi and 'ctrl' in modifiers and text in ('a', 'A') and
text not in keys):
sister_nodes = self.get_selectable_nodes()
select = self.select_node
for node in sister_nodes:
select(node)
keys.append(text)
else:
s = text
if len(text) > 1:
d = {'divide': '/', 'mul': '*', 'substract': '-', 'add': '+',
'decimal': '.'}
if text.startswith('numpad'):
s = text[6:]
if len(s) > 1:
if s in d:
s = d[s]
else:
s = None
else:
s = None
if s is not None:
if s not in keys: # don't keep adding while holding down
if time() - self._last_key_time <= self.text_entry_timeout:
self._word_filter += s
else:
self._word_filter = s
keys.append(s)
self._last_key_time = time()
node, idx = self.goto_node(self._word_filter, node_src,
idx_src)
else:
self._word_filter = ''
node, idx = self.goto_node(text, node_src, idx_src)
if node == node_src:
return False
multiselect = multi and 'ctrl' in modifiers
if multi and 'shift' in modifiers:
self._select_range(multiselect, True, node, idx)
else:
if not multiselect:
self.clear_selection()
self.select_node(node)
return True
self._word_filter = ''
return False
def select_with_key_up(self, keyboard, scancode, **kwargs):
'''(internal) Processes a key release. This must be called by the
derived widget when a key that :meth:`select_with_key_down` returned
True is released.
The parameters are such that it could be bound directly to the
on_key_up event of a keyboard.
:Returns:
bool, True if the key release was used, False otherwise.
'''
if scancode[1] == 'shift':
self._shift_down = False
elif scancode[1] in ('ctrl', 'lctrl', 'rctrl'):
self._ctrl_down = False
else:
try:
self._key_list.remove(scancode[1])
return True
except ValueError:
return False
return True
def _update_counts(self, *largs):
# doesn't invert indices here
pc = self.page_count
uc = self.up_count
rc = self.right_count
sc = self.scroll_count
self._offset_counts = {'pageup': -pc, 'pagedown': pc, 'up': -uc,
'down': uc, 'right': rc, 'left': -rc, 'scrollup': sc,
'scrolldown': -sc, 'scrollright': -sc, 'scrollleft': sc}
def _resolve_last_node(self):
# for offset selection, we have a anchor, and we select everything
# between anchor and added offset relative to last node
sister_nodes = self.get_selectable_nodes()
if not len(sister_nodes):
return None, 0
last_node = self._last_selected_node
last_idx = self._last_node_idx
end = len(sister_nodes) - 1
if last_node is None:
last_node = self._anchor
last_idx = self._anchor_idx
if last_node is None:
return sister_nodes[end], end
if last_idx > end or sister_nodes[last_idx] != last_node:
try:
return last_node, self.get_index_of_node(last_node,
sister_nodes)
except ValueError:
return sister_nodes[end], end
return last_node, last_idx
def _select_range(self, multiselect, keep_anchor, node, idx):
'''Selects a range between self._anchor and node or idx.
If multiselect is True, it will be added to the selection, otherwise
it will unselect everything before selecting the range. This is only
called if self.multiselect is True.
If keep anchor is False, the anchor is moved to node. This should
always be True for keyboard selection.
'''
select = self.select_node
sister_nodes = self.get_selectable_nodes()
end = len(sister_nodes) - 1
last_node = self._anchor
last_idx = self._anchor_idx
if last_node is None:
last_idx = end
last_node = sister_nodes[end]
else:
if last_idx > end or sister_nodes[last_idx] != last_node:
try:
last_idx = self.get_index_of_node(last_node, sister_nodes)
except ValueError:
# list changed - cannot do select across them
return
if idx > end or sister_nodes[idx] != node:
try: # just in case
idx = self.get_index_of_node(node, sister_nodes)
except ValueError:
return
if last_idx > idx:
last_idx, idx = idx, last_idx
if not multiselect:
self.clear_selection()
for item in sister_nodes[last_idx:idx + 1]:
select(item)
if keep_anchor:
self._anchor = last_node
self._anchor_idx = last_idx
else:
self._anchor = node # in case idx was reversed, reset
self._anchor_idx = idx
self._last_selected_node = node
self._last_node_idx = idx
def clear_selection(self):
''' Deselects all the currently selected nodes.
'''
# keep the anchor and last selected node
deselect = self.deselect_node
nodes = self.selected_nodes
# empty beforehand so lookup in deselect will be fast
for node in nodes[:]:
deselect(node)
def get_selectable_nodes(self):
'''(internal) Returns a list of the nodes that can be selected. It can
be overwritten by the derived widget to return the correct list.
This list is used to determine which nodes to select with group
selection. E.g. the last element in the list will be selected when
home is pressed, pagedown will move (or add to, if shift is held) the
selection from the current position by negative :attr:`page_count`
nodes starting from the position of the currently selected node in
this list and so on. Still, nodes can be selected even if they are not
in this list.
.. note::
It is safe to dynamically change this list including removing,
adding, or re-arranging its elements. Nodes can be selected even
if they are not on this list. And selected nodes removed from the
list will remain selected until :meth:`deselect_node` is called.
.. warning::
Layouts display their children in the reverse order. That is, the
contents of :attr:`~kivy.uix.widget.Widget.children` is displayed
form right to left, bottom to top. Therefore, internally, the
indices of the elements returned by this function are reversed to
make it work by default for most layouts so that the final result
is consistent e.g. home, although it will select the last element
in this list visually, will select the first element when
counting from top to bottom and left to right. If this behavior is
not desired, a reversed list should be returned instead.
Defaults to returning :attr:`~kivy.uix.widget.Widget.children`.
'''
return self.children
def get_index_of_node(self, node, selectable_nodes):
'''(internal) Returns the index of the `node` within the
`selectable_nodes` returned by :meth:`get_selectable_nodes`.
'''
return selectable_nodes.index(node)
def goto_node(self, key, last_node, last_node_idx):
'''(internal) Used by the controller to get the node at the position
indicated by key. The key can be keyboard inputs, e.g. pageup,
or scroll inputs from the mouse scroll wheel, e.g. scrollup.
'last_node' is the last node selected and is used to find the resulting
node. For example, if the key is up, the returned node is one node
up from the last node.
It can be overwritten by the derived widget.
:Parameters:
`key`
str, the string used to find the desired node. It can be any
of the keyboard keys, as well as the mouse scrollup,
scrolldown, scrollright, and scrollleft strings. If letters
are typed in quick succession, the letters will be combined
before it's passed in as key and can be used to find nodes that
have an associated string that starts with those letters.
`last_node`
The last node that was selected.
`last_node_idx`
The cached index of the last node selected in the
:meth:`get_selectable_nodes` list. If the list hasn't changed
it saves having to look up the index of `last_node` in that
list.
:Returns:
tuple, the node targeted by key and its index in the
:meth:`get_selectable_nodes` list. Returning
`(last_node, last_node_idx)` indicates a node wasn't found.
'''
sister_nodes = self.get_selectable_nodes()
end = len(sister_nodes) - 1
counts = self._offset_counts
if end == -1:
return last_node, last_node_idx
if last_node_idx > end or sister_nodes[last_node_idx] != last_node:
try: # just in case
last_node_idx = self.get_index_of_node(last_node, sister_nodes)
except ValueError:
return last_node, last_node_idx
is_reversed = self.nodes_order_reversed
if key in counts:
count = -counts[key] if is_reversed else counts[key]
idx = max(min(count + last_node_idx, end), 0)
return sister_nodes[idx], idx
elif key == 'home':
if is_reversed:
return sister_nodes[end], end
return sister_nodes[0], 0
elif key == 'end':
if is_reversed:
return sister_nodes[0], 0
return sister_nodes[end], end
else:
return last_node, last_node_idx
def select_node(self, node):
''' Selects a node.
It is called by the controller when it selects a node and can be
called from the outside to select a node directly. The derived widget
should overwrite this method and change the node state to selected
when called.
:Parameters:
`node`
The node to be selected.
:Returns:
bool, True if the node was selected, False otherwise.
.. warning::
This method must be called by the derived widget using super if it
is overwritten.
'''
nodes = self.selected_nodes
if node in nodes:
return False
if (not self.multiselect) and len(nodes):
self.clear_selection()
if node not in nodes:
nodes.append(node)
self._anchor = node
self._last_selected_node = node
return True
def deselect_node(self, node):
''' Deselects a possibly selected node.
It is called by the controller when it deselects a node and can also
be called from the outside to deselect a node directly. The derived
widget should overwrite this method and change the node to its
unselected state when this is called
:Parameters:
`node`
The node to be deselected.
.. warning::
This method must be called by the derived widget using super if it
is overwritten.
'''
try:
self.selected_nodes.remove(node)
return True
except ValueError:
return False

View file

@ -0,0 +1,160 @@
'''
Cover Behavior
==============
The :class:`~kivy.uix.behaviors.cover.CoverBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ is intended for rendering
textures to full widget size keeping the aspect ratio of the original texture.
Use cases are i.e. rendering full size background images or video content in
a dynamic layout.
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.
Example
-------
The following examples add cover behavior to an image:
In python:
.. code-block:: python
from kivy.app import App
from kivy.uix.behaviors import CoverBehavior
from kivy.uix.image import Image
class CoverImage(CoverBehavior, Image):
def __init__(self, **kwargs):
super(CoverImage, self).__init__(**kwargs)
texture = self._coreimage.texture
self.reference_size = texture.size
self.texture = texture
class MainApp(App):
def build(self):
return CoverImage(source='image.jpg')
MainApp().run()
In Kivy Language:
.. code-block:: kv
CoverImage:
source: 'image.png'
<CoverImage@CoverBehavior+Image>:
reference_size: self.texture_size
See :class:`~kivy.uix.behaviors.cover.CoverBehavior` for details.
'''
__all__ = ('CoverBehavior', )
from decimal import Decimal
from kivy.lang import Builder
from kivy.properties import ListProperty
Builder.load_string("""
<-CoverBehavior>:
canvas.before:
StencilPush
Rectangle:
pos: self.pos
size: self.size
StencilUse
canvas:
Rectangle:
texture: self.texture
size: self.cover_size
pos: self.cover_pos
canvas.after:
StencilUnUse
Rectangle:
pos: self.pos
size: self.size
StencilPop
""")
class CoverBehavior(object):
'''The CoverBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
provides rendering a texture covering full widget size keeping aspect ratio
of the original texture.
.. versionadded:: 1.10.0
'''
reference_size = ListProperty([])
'''Reference size used for aspect ratio approximation calculation.
:attr:`reference_size` is a :class:`~kivy.properties.ListProperty` and
defaults to `[]`.
'''
cover_size = ListProperty([0, 0])
'''Size of the aspect ratio aware texture. Gets calculated in
``CoverBehavior.calculate_cover``.
:attr:`cover_size` is a :class:`~kivy.properties.ListProperty` and
defaults to `[0, 0]`.
'''
cover_pos = ListProperty([0, 0])
'''Position of the aspect ratio aware texture. Gets calculated in
``CoverBehavior.calculate_cover``.
:attr:`cover_pos` is a :class:`~kivy.properties.ListProperty` and
defaults to `[0, 0]`.
'''
def __init__(self, **kwargs):
super(CoverBehavior, self).__init__(**kwargs)
# bind covering
self.bind(
size=self.calculate_cover,
pos=self.calculate_cover
)
def _aspect_ratio_approximate(self, size):
# return a decimal approximation of an aspect ratio.
return Decimal('%.2f' % (float(size[0]) / size[1]))
def _scale_size(self, size, sizer):
# return scaled size based on sizer, where sizer (n, None) scales x
# to n and (None, n) scales y to n
size_new = list(sizer)
i = size_new.index(None)
j = i * -1 + 1
size_new[i] = (size_new[j] * size[i]) / size[j]
return tuple(size_new)
def calculate_cover(self, *args):
# return if no reference size yet
if not self.reference_size:
return
size = self.size
origin_appr = self._aspect_ratio_approximate(self.reference_size)
crop_appr = self._aspect_ratio_approximate(size)
# same aspect ratio
if origin_appr == crop_appr:
crop_size = self.size
offset = (0, 0)
# scale x
elif origin_appr < crop_appr:
crop_size = self._scale_size(self.reference_size, (size[0], None))
offset = (0, ((crop_size[1] - size[1]) / 2) * -1)
# scale y
else:
crop_size = self._scale_size(self.reference_size, (None, size[1]))
offset = (((crop_size[0] - size[0]) / 2) * -1, 0)
# set background size and position
self.cover_size = crop_size
self.cover_pos = offset

View file

@ -0,0 +1,234 @@
"""
Drag Behavior
=============
The :class:`~kivy.uix.behaviors.drag.DragBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides Drag behavior.
When combined with a widget, dragging in the rectangle defined by the
:attr:`~kivy.uix.behaviors.drag.DragBehavior.drag_rectangle` will drag the
widget.
Example
-------
The following example creates a draggable label::
from kivy.uix.label import Label
from kivy.app import App
from kivy.uix.behaviors import DragBehavior
from kivy.lang import Builder
# You could also put the following in your kv file...
kv = '''
<DragLabel>:
# Define the properties for the DragLabel
drag_rectangle: self.x, self.y, self.width, self.height
drag_timeout: 10000000
drag_distance: 0
FloatLayout:
# Define the root widget
DragLabel:
size_hint: 0.25, 0.2
text: 'Drag me'
'''
class DragLabel(DragBehavior, Label):
pass
class TestApp(App):
def build(self):
return Builder.load_string(kv)
TestApp().run()
"""
__all__ = ('DragBehavior', )
from kivy.clock import Clock
from kivy.properties import NumericProperty, ReferenceListProperty
from kivy.config import Config
from kivy.metrics import sp
from functools import partial
# When we are generating documentation, Config doesn't exist
_scroll_timeout = _scroll_distance = 0
if Config:
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
_scroll_distance = Config.getint('widgets', 'scroll_distance')
class DragBehavior(object):
'''
The DragBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_ provides
Drag behavior. When combined with a widget, dragging in the rectangle
defined by :attr:`drag_rectangle` will drag the widget. Please see
the :mod:`drag behaviors module <kivy.uix.behaviors.drag>` documentation
for more information.
.. versionadded:: 1.8.0
'''
drag_distance = NumericProperty(_scroll_distance)
'''Distance to move before dragging the :class:`DragBehavior`, in pixels.
As soon as the distance has been traveled, the :class:`DragBehavior` will
start to drag, and no touch event will be dispatched to the children.
It is advisable that you base this value on the dpi of your target device's
screen.
:attr:`drag_distance` is a :class:`~kivy.properties.NumericProperty` and
defaults to the `scroll_distance` as defined in the user
:class:`~kivy.config.Config` (20 pixels by default).
'''
drag_timeout = NumericProperty(_scroll_timeout)
'''Timeout allowed to trigger the :attr:`drag_distance`, in milliseconds.
If the user has not moved :attr:`drag_distance` within the timeout,
dragging will be disabled, and the touch event will be dispatched to the
children.
:attr:`drag_timeout` is a :class:`~kivy.properties.NumericProperty` and
defaults to the `scroll_timeout` as defined in the user
:class:`~kivy.config.Config` (55 milliseconds by default).
'''
drag_rect_x = NumericProperty(0)
'''X position of the axis aligned bounding rectangle where dragging
is allowed (in window coordinates).
:attr:`drag_rect_x` is a :class:`~kivy.properties.NumericProperty` and
defaults to 0.
'''
drag_rect_y = NumericProperty(0)
'''Y position of the axis aligned bounding rectangle where dragging
is allowed (in window coordinates).
:attr:`drag_rect_Y` is a :class:`~kivy.properties.NumericProperty` and
defaults to 0.
'''
drag_rect_width = NumericProperty(100)
'''Width of the axis aligned bounding rectangle where dragging is allowed.
:attr:`drag_rect_width` is a :class:`~kivy.properties.NumericProperty` and
defaults to 100.
'''
drag_rect_height = NumericProperty(100)
'''Height of the axis aligned bounding rectangle where dragging is allowed.
:attr:`drag_rect_height` is a :class:`~kivy.properties.NumericProperty` and
defaults to 100.
'''
drag_rectangle = ReferenceListProperty(drag_rect_x, drag_rect_y,
drag_rect_width, drag_rect_height)
'''Position and size of the axis aligned bounding rectangle where dragging
is allowed.
:attr:`drag_rectangle` is a :class:`~kivy.properties.ReferenceListProperty`
of (:attr:`drag_rect_x`, :attr:`drag_rect_y`, :attr:`drag_rect_width`,
:attr:`drag_rect_height`) properties.
'''
def __init__(self, **kwargs):
self._drag_touch = None
super(DragBehavior, self).__init__(**kwargs)
def _get_uid(self, prefix='sv'):
return '{0}.{1}'.format(prefix, self.uid)
def on_touch_down(self, touch):
xx, yy, w, h = self.drag_rectangle
x, y = touch.pos
if not self.collide_point(x, y):
touch.ud[self._get_uid('svavoid')] = True
return super(DragBehavior, self).on_touch_down(touch)
if self._drag_touch or ('button' in touch.profile and
touch.button.startswith('scroll')) or\
not ((xx < x <= xx + w) and (yy < y <= yy + h)):
return super(DragBehavior, self).on_touch_down(touch)
# no mouse scrolling, so the user is going to drag with this touch.
self._drag_touch = touch
uid = self._get_uid()
touch.grab(self)
touch.ud[uid] = {
'mode': 'unknown',
'dx': 0,
'dy': 0}
Clock.schedule_once(self._change_touch_mode,
self.drag_timeout / 1000.)
return True
def on_touch_move(self, touch):
if self._get_uid('svavoid') in touch.ud or\
self._drag_touch is not touch:
return super(DragBehavior, self).on_touch_move(touch) or\
self._get_uid() in touch.ud
if touch.grab_current is not self:
return True
uid = self._get_uid()
ud = touch.ud[uid]
mode = ud['mode']
if mode == 'unknown':
ud['dx'] += abs(touch.dx)
ud['dy'] += abs(touch.dy)
if ud['dx'] > sp(self.drag_distance):
mode = 'drag'
if ud['dy'] > sp(self.drag_distance):
mode = 'drag'
ud['mode'] = mode
if mode == 'drag':
self.x += touch.dx
self.y += touch.dy
return True
def on_touch_up(self, touch):
if self._get_uid('svavoid') in touch.ud:
return super(DragBehavior, self).on_touch_up(touch)
if self._drag_touch and self in [x() for x in touch.grab_list]:
touch.ungrab(self)
self._drag_touch = None
ud = touch.ud[self._get_uid()]
if ud['mode'] == 'unknown':
super(DragBehavior, self).on_touch_down(touch)
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
else:
if self._drag_touch is not touch:
super(DragBehavior, self).on_touch_up(touch)
return self._get_uid() in touch.ud
def _do_touch_up(self, touch, *largs):
super(DragBehavior, self).on_touch_up(touch)
# don't forget about grab event!
for x in touch.grab_list[:]:
touch.grab_list.remove(x)
x = x()
if not x:
continue
touch.grab_current = x
super(DragBehavior, self).on_touch_up(touch)
touch.grab_current = None
def _change_touch_mode(self, *largs):
if not self._drag_touch:
return
uid = self._get_uid()
touch = self._drag_touch
ud = touch.ud[uid]
if ud['mode'] != 'unknown':
return
touch.ungrab(self)
self._drag_touch = None
touch.push()
touch.apply_transform_2d(self.parent.to_widget)
super(DragBehavior, self).on_touch_down(touch)
touch.pop()
return

View file

@ -0,0 +1,140 @@
# -*- encoding: utf-8 -*-
'''
Emacs Behavior
==============
The :class:`~kivy.uix.behaviors.emacs.EmacsBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ allows you to add
`Emacs <https://www.gnu.org/software/emacs/>`_ keyboard shortcuts for basic
movement and editing to the :class:`~kivy.uix.textinput.TextInput` widget.
The shortcuts currently available are listed below:
Emacs shortcuts
---------------
=============== ========================================================
Shortcut Description
--------------- --------------------------------------------------------
Control + a Move cursor to the beginning of the line
Control + e Move cursor to the end of the line
Control + f Move cursor one character to the right
Control + b Move cursor one character to the left
Alt + f Move cursor to the end of the word to the right
Alt + b Move cursor to the start of the word to the left
Alt + Backspace Delete text left of the cursor to the beginning of word
Alt + d Delete text right of the cursor to the end of the word
Alt + w Copy selection
Control + w Cut selection
Control + y Paste selection
=============== ========================================================
.. warning::
If you have the :mod:`~kivy.modules.inspector` module enabled, the
shortcut for opening the inspector (Control + e) conflicts with the
Emacs shortcut to move to the end of the line (it will still move the
cursor to the end of the line, but the inspector will open as well).
'''
from kivy.properties import StringProperty
__all__ = ('EmacsBehavior', )
class EmacsBehavior(object):
'''
A `mixin <https://en.wikipedia.org/wiki/Mixin>`_ that enables Emacs-style
keyboard shortcuts for the :class:`~kivy.uix.textinput.TextInput` widget.
Please see the :mod:`Emacs behaviors module <kivy.uix.behaviors.emacs>`
documentation for more information.
.. versionadded:: 1.9.1
'''
key_bindings = StringProperty('emacs')
'''String name which determines the type of key bindings to use with the
:class:`~kivy.uix.textinput.TextInput`. This allows Emacs key bindings to
be enabled/disabled programmatically for widgets that inherit from
:class:`EmacsBehavior`. If the value is not ``'emacs'``, Emacs bindings
will be disabled. Use ``'default'`` for switching to the default key
bindings of TextInput.
:attr:`key_bindings` is a :class:`~kivy.properties.StringProperty`
and defaults to ``'emacs'``.
.. versionadded:: 1.10.0
'''
def __init__(self, **kwargs):
super(EmacsBehavior, self).__init__(**kwargs)
self.bindings = {
'ctrl': {
'a': lambda: self.do_cursor_movement('cursor_home'),
'e': lambda: self.do_cursor_movement('cursor_end'),
'f': lambda: self.do_cursor_movement('cursor_right'),
'b': lambda: self.do_cursor_movement('cursor_left'),
'w': lambda: self._cut(self.selection_text),
'y': self.paste,
},
'alt': {
'w': self.copy,
'f': lambda: self.do_cursor_movement('cursor_right',
control=True),
'b': lambda: self.do_cursor_movement('cursor_left',
control=True),
'd': self.delete_word_right,
'\x08': self.delete_word_left, # alt + backspace
},
}
def keyboard_on_key_down(self, window, keycode, text, modifiers):
key, key_str = keycode
# join the modifiers e.g. ['alt', 'ctrl']
mod = '+'.join(modifiers) if modifiers else None
is_emacs_shortcut = False
if key in range(256) and self.key_bindings == 'emacs':
if mod == 'ctrl' and chr(key) in self.bindings['ctrl'].keys():
is_emacs_shortcut = True
elif mod == 'alt' and chr(key) in self.bindings['alt'].keys():
is_emacs_shortcut = True
else: # e.g. ctrl+alt or alt+ctrl (alt-gr key)
is_emacs_shortcut = False
if is_emacs_shortcut:
# Look up mod and key
emacs_shortcut = self.bindings[mod][chr(key)]
emacs_shortcut()
else:
super(EmacsBehavior, self).keyboard_on_key_down(window, keycode,
text, modifiers)
def delete_word_right(self):
'''Delete text right of the cursor to the end of the word'''
if self._selection:
return
start_index = self.cursor_index()
start_cursor = self.cursor
self.do_cursor_movement('cursor_right', control=True)
end_index = self.cursor_index()
if start_index != end_index:
s = self.text[start_index:end_index]
self._set_unredo_delsel(start_index, end_index, s, from_undo=False)
self.text = self.text[:start_index] + self.text[end_index:]
self._set_cursor(pos=start_cursor)
def delete_word_left(self):
'''Delete text left of the cursor to the beginning of word'''
if self._selection:
return
start_index = self.cursor_index()
self.do_cursor_movement('cursor_left', control=True)
end_cursor = self.cursor
end_index = self.cursor_index()
if start_index != end_index:
s = self.text[end_index:start_index]
self._set_unredo_delsel(end_index, start_index, s, from_undo=False)
self.text = self.text[:end_index] + self.text[start_index:]
self._set_cursor(pos=end_cursor)

View file

@ -0,0 +1,595 @@
'''
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')
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
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')
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')
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
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
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()

View file

@ -0,0 +1,590 @@
'''
Kivy Namespaces
===============
.. versionadded:: 1.9.1
.. warning::
This code is still experimental, and its API is subject to change in a
future version.
The :class:`KNSpaceBehavior` `mixin <https://en.wikipedia.org/wiki/Mixin>`_
class provides namespace functionality for Kivy objects. It allows kivy objects
to be named and then accessed using namespaces.
:class:`KNSpace` instances are the namespaces that store the named objects
in Kivy :class:`~kivy.properties.ObjectProperty` instances.
In addition, when inheriting from :class:`KNSpaceBehavior`, if the derived
object is named, the name will automatically be added to the associated
namespace and will point to a :attr:`~kivy.uix.widget.proxy_ref` of the
derived object.
Basic examples
--------------
By default, there's only a single namespace: the :attr:`knspace` namespace. The
simplest example is adding a widget to the namespace:
.. code-block:: python
from kivy.uix.behaviors.knspace import knspace
widget = Widget()
knspace.my_widget = widget
This adds a kivy :class:`~kivy.properties.ObjectProperty` with `rebind=True`
and `allownone=True` to the :attr:`knspace` namespace with a property name
`my_widget`. And the property now also points to this widget.
This can be done automatically with:
.. code-block:: python
class MyWidget(KNSpaceBehavior, Widget):
pass
widget = MyWidget(knsname='my_widget')
Or in kv:
.. code-block:: kv
<MyWidget@KNSpaceBehavior+Widget>
MyWidget:
knsname: 'my_widget'
Now, `knspace.my_widget` will point to that widget.
When one creates a second widget with the same name, the namespace will
also change to point to the new widget. E.g.:
.. code-block:: python
widget = MyWidget(knsname='my_widget')
# knspace.my_widget now points to widget
widget2 = MyWidget(knsname='my_widget')
# knspace.my_widget now points to widget2
Setting the namespace
---------------------
One can also create ones own namespace rather than using the default
:attr:`knspace` by directly setting :attr:`KNSpaceBehavior.knspace`:
.. code-block:: python
class MyWidget(KNSpaceBehavior, Widget):
pass
widget = MyWidget(knsname='my_widget')
my_new_namespace = KNSpace()
widget.knspace = my_new_namespace
Initially, `my_widget` is added to the default namespace, but when the widget's
namespace is changed to `my_new_namespace`, the reference to `my_widget` is
moved to that namespace. We could have also of course first set the namespace
to `my_new_namespace` and then have named the widget `my_widget`, thereby
avoiding the initial assignment to the default namespace.
Similarly, in kv:
.. code-block:: kv
<MyWidget@KNSpaceBehavior+Widget>
MyWidget:
knspace: KNSpace()
knsname: 'my_widget'
Inheriting the namespace
------------------------
In the previous example, we directly set the namespace we wished to use.
In the following example, we inherit it from the parent, so we only have to set
it once:
.. code-block:: kv
<MyWidget@KNSpaceBehavior+Widget>
<MyLabel@KNSpaceBehavior+Label>
<MyComplexWidget@MyWidget>:
knsname: 'my_complex'
MyLabel:
knsname: 'label1'
MyLabel:
knsname: 'label2'
Then, we do:
.. code-block:: python
widget = MyComplexWidget()
new_knspace = KNSpace()
widget.knspace = new_knspace
The rule is that if no knspace has been assigned to a widget, it looks for a
namespace in its parent and parent's parent and so on until it find one to
use. If none are found, it uses the default :attr:`knspace`.
When `MyComplexWidget` is created, it still used the default namespace.
However, when we assigned the root widget its new namespace, all its
children switched to using that new namespace as well. So `new_knspace` now
contains `label1` and `label2` as well as `my_complex`.
If we had first done:
.. code-block:: python
widget = MyComplexWidget()
new_knspace = KNSpace()
knspace.label1.knspace = knspace
widget.knspace = new_knspace
Then `label1` would remain stored in the default :attr:`knspace` since it was
directly set, but `label2` and `my_complex` would still be added to the new
namespace.
One can customize the attribute used to search the parent tree by changing
:attr:`KNSpaceBehavior.knspace_key`. If the desired knspace is not reachable
through a widgets parent tree, e.g. in a popup that is not a widget's child,
:attr:`KNSpaceBehavior.knspace_key` can be used to establish a different
search order.
Accessing the namespace
-----------------------
As seen in the previous example, if not directly assigned, the namespace is
found by searching the parent tree. Consequently, if a namespace was assigned
further up the parent tree, all its children and below could access that
namespace through their :attr:`KNSpaceBehavior.knspace` property.
This allows the creation of multiple widgets with identically given names
if each root widget instance is assigned a new namespace. For example:
.. code-block:: kv
<MyComplexWidget@KNSpaceBehavior+Widget>:
Label:
text: root.knspace.pretty.text if root.knspace.pretty else ''
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
knsname: 'pretty'
text: 'Hello'
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
MyComplexWidget
MyPrettyWidget
Now, when we do:
.. code-block:: python
knspace1, knspace2 = KNSpace(), KNSpace()
composite1 = MyCompositeWidget()
composite1.knspace = knspace1
composite2 = MyCompositeWidget()
composite2.knspace = knspace2
knspace1.pretty = "Here's the ladder, now fix the roof!"
knspace2.pretty = "Get that raccoon off me!"
Because each of the `MyCompositeWidget` instances have a different namespace
their children also use different namespaces. Consequently, the
pretty and complex widgets of each instance will have different text.
Further, because both the namespace :class:`~kivy.properties.ObjectProperty`
references, and :attr:`KNSpaceBehavior.knspace` have `rebind=True`, the
text of the `MyComplexWidget` label is rebound to match the text of
`MyPrettyWidget` when either the root's namespace changes or when the
`root.knspace.pretty` property changes, as expected.
Forking a namespace
-------------------
Forking a namespace provides the opportunity to create a new namespace
from a parent namespace so that the forked namespace will contain everything
in the origin namespace, but the origin namespace will not have access to
anything added to the forked namespace.
For example:
.. code-block:: python
child = knspace.fork()
grandchild = child.fork()
child.label = Label()
grandchild.button = Button()
Now label is accessible by both child and grandchild, but not by knspace. And
button is only accessible by the grandchild but not by the child or by knspace.
Finally, doing `grandchild.label = Label()` will leave `grandchild.label`
and `child.label` pointing to different labels.
A motivating example is the example from above:
.. code-block:: kv
<MyComplexWidget@KNSpaceBehavior+Widget>:
Label:
text: root.knspace.pretty.text if root.knspace.pretty else ''
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
knsname: 'pretty'
text: 'Hello'
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
knspace: 'fork'
MyComplexWidget
MyPrettyWidget
Notice the addition of `knspace: 'fork'`. This is identical to doing
`knspace: self.knspace.fork()`. However, doing that would lead to infinite
recursion as that kv rule would be executed recursively because `self.knspace`
will keep on changing. However, allowing `knspace: 'fork'` cirumvents that.
See :attr:`KNSpaceBehavior.knspace`.
Now, having forked, we just need to do:
.. code-block:: python
composite1 = MyCompositeWidget()
composite2 = MyCompositeWidget()
composite1.knspace.pretty = "Here's the ladder, now fix the roof!"
composite2.knspace.pretty = "Get that raccoon off me!"
Since by forking we automatically created a unique namespace for each
`MyCompositeWidget` instance.
'''
__all__ = ('KNSpace', 'KNSpaceBehavior', 'knspace')
from kivy.event import EventDispatcher
from kivy.properties import StringProperty, ObjectProperty, AliasProperty
from kivy.context import register_context
class KNSpace(EventDispatcher):
'''Each :class:`KNSpace` instance is a namespace that stores the named Kivy
objects associated with this namespace. Each named object is
stored as the value of a Kivy :class:`~kivy.properties.ObjectProperty` of
this instance whose property name is the object's given name. Both `rebind`
and `allownone` are set to `True` for the property.
See :attr:`KNSpaceBehavior.knspace` for details on how a namespace is
associated with a named object.
When storing an object in the namespace, the object's `proxy_ref` is
stored if the object has such an attribute.
:Parameters:
`parent`: (internal) A :class:`KNSpace` instance or None.
If specified, it's a parent namespace, in which case, the current
namespace will have in its namespace all its named objects
as well as the named objects of its parent and parent's parent
etc. See :meth:`fork` for more details.
'''
parent = None
'''(internal) The parent namespace instance, :class:`KNSpace`, or None. See
:meth:`fork`.
'''
__has_applied = None
keep_ref = False
'''Whether a direct reference should be kept to the stored objects.
If ``True``, we use the direct object, otherwise we use
:attr:`~kivy.uix.widget.proxy_ref` when present.
Defaults to False.
'''
def __init__(self, parent=None, keep_ref=False, **kwargs):
self.keep_ref = keep_ref
super(KNSpace, self).__init__(**kwargs)
self.parent = parent
self.__has_applied = set(self.properties().keys())
def __setattr__(self, name, value):
prop = super(KNSpace, self).property(name, quiet=True)
has_applied = self.__has_applied
if prop is None:
if hasattr(self, name):
super(KNSpace, self).__setattr__(name, value)
else:
self.apply_property(
**{name:
ObjectProperty(None, rebind=True, allownone=True)}
)
if not self.keep_ref:
value = getattr(value, 'proxy_ref', value)
has_applied.add(name)
super(KNSpace, self).__setattr__(name, value)
elif name not in has_applied:
self.apply_property(**{name: prop})
has_applied.add(name)
if not self.keep_ref:
value = getattr(value, 'proxy_ref', value)
super(KNSpace, self).__setattr__(name, value)
else:
if not self.keep_ref:
value = getattr(value, 'proxy_ref', value)
super(KNSpace, self).__setattr__(name, value)
def __getattribute__(self, name):
if name in super(KNSpace, self).__getattribute__('__dict__'):
return super(KNSpace, self).__getattribute__(name)
try:
value = super(KNSpace, self).__getattribute__(name)
except AttributeError:
parent = super(KNSpace, self).__getattribute__('parent')
if parent is None:
raise AttributeError(name)
return getattr(parent, name)
if value is not None:
return value
parent = super(KNSpace, self).__getattribute__('parent')
if parent is None:
return None
try:
return getattr(parent, name) # if parent doesn't have it
except AttributeError:
return None
def property(self, name, quiet=False):
# needs to overwrite EventDispatcher.property so kv lang will work
prop = super(KNSpace, self).property(name, quiet=True)
if prop is not None:
return prop
prop = ObjectProperty(None, rebind=True, allownone=True)
self.apply_property(**{name: prop})
self.__has_applied.add(name)
return prop
def fork(self):
'''Returns a new :class:`KNSpace` instance which will have access to
all the named objects in the current namespace but will also have a
namespace of its own that is unique to it.
For example:
.. code-block:: python
forked_knspace1 = knspace.fork()
forked_knspace2 = knspace.fork()
Now, any names added to `knspace` will be accessible by the
`forked_knspace1` and `forked_knspace2` namespaces by the normal means.
However, any names added to `forked_knspace1` will not be accessible
from `knspace` or `forked_knspace2`. Similar for `forked_knspace2`.
'''
return KNSpace(parent=self)
class KNSpaceBehavior(object):
'''Inheriting from this class allows naming of the inherited objects, which
are then added to the associated namespace :attr:`knspace` and accessible
through it.
Please see the :mod:`knspace behaviors module <kivy.uix.behaviors.knspace>`
documentation for more information.
'''
_knspace = ObjectProperty(None, allownone=True)
_knsname = StringProperty('')
__last_knspace = None
__callbacks = None
def __init__(self, knspace=None, **kwargs):
self.knspace = knspace
super(KNSpaceBehavior, self).__init__(**kwargs)
def __knspace_clear_callbacks(self, *largs):
for obj, name, uid in self.__callbacks:
obj.unbind_uid(name, uid)
last = self.__last_knspace
self.__last_knspace = self.__callbacks = None
assert self._knspace is None
assert last
new = self.__set_parent_knspace()
if new is last:
return
self.property('_knspace').dispatch(self)
name = self.knsname
if not name:
return
if getattr(last, name) == self:
setattr(last, name, None)
if new:
setattr(new, name, self)
else:
raise ValueError('Object has name "{}", but no namespace'.
format(name))
def __set_parent_knspace(self):
callbacks = self.__callbacks = []
fbind = self.fbind
append = callbacks.append
parent_key = self.knspace_key
clear = self.__knspace_clear_callbacks
append((self, 'knspace_key', fbind('knspace_key', clear)))
if not parent_key:
self.__last_knspace = knspace
return knspace
append((self, parent_key, fbind(parent_key, clear)))
parent = getattr(self, parent_key, None)
while parent is not None:
fbind = parent.fbind
parent_knspace = getattr(parent, 'knspace', 0)
if parent_knspace != 0:
append((parent, 'knspace', fbind('knspace', clear)))
self.__last_knspace = parent_knspace
return parent_knspace
append((parent, parent_key, fbind(parent_key, clear)))
new_parent = getattr(parent, parent_key, None)
if new_parent is parent:
break
parent = new_parent
self.__last_knspace = knspace
return knspace
def _get_knspace(self):
_knspace = self._knspace
if _knspace is not None:
return _knspace
if self.__callbacks is not None:
return self.__last_knspace
# we only get here if we never accessed our knspace
return self.__set_parent_knspace()
def _set_knspace(self, value):
if value is self._knspace:
return
knspace = self._knspace or self.__last_knspace
name = self.knsname
if name and knspace and getattr(knspace, name) == self:
setattr(knspace, name, None) # reset old namespace
if value == 'fork':
if not knspace:
knspace = self.knspace # get parents in case we haven't before
if knspace:
value = knspace.fork()
else:
raise ValueError('Cannot fork with no namespace')
for obj, prop_name, uid in self.__callbacks or []:
obj.unbind_uid(prop_name, uid)
self.__last_knspace = self.__callbacks = None
if name:
if value is None: # if None, first update the recursive knspace
knspace = self.__set_parent_knspace()
if knspace:
setattr(knspace, name, self)
self._knspace = None # cause a kv trigger
else:
setattr(value, name, self)
knspace = self._knspace = value
if not knspace:
raise ValueError('Object has name "{}", but no namespace'.
format(name))
else:
if value is None:
self.__set_parent_knspace() # update before trigger below
self._knspace = value
knspace = AliasProperty(
_get_knspace, _set_knspace, bind=('_knspace', ), cache=False,
rebind=True, allownone=True)
'''The namespace instance, :class:`KNSpace`, associated with this widget.
The :attr:`knspace` namespace stores this widget when naming this widget
with :attr:`knsname`.
If the namespace has been set with a :class:`KNSpace` instance, e.g. with
`self.knspace = KNSpace()`, then that instance is returned (setting with
`None` doesn't count). Otherwise, if :attr:`knspace_key` is not None, we
look for a namespace to use in the object that is stored in the property
named :attr:`knspace_key`, of this instance. I.e.
`object = getattr(self, self.knspace_key)`.
If that object has a knspace property, then we return its value. Otherwise,
we go further up, e.g. with `getattr(object, self.knspace_key)` and look
for its `knspace` property.
Finally, if we reach a value of `None`, or :attr:`knspace_key` was `None`,
the default :attr:`~kivy.uix.behaviors.knspace.knspace` namespace is
returned.
If :attr:`knspace` is set to the string `'fork'`, the current namespace
in :attr:`knspace` will be forked with :meth:`KNSpace.fork` and the
resulting namespace will be assigned to this instance's :attr:`knspace`.
See the module examples for a motivating example.
Both `rebind` and `allownone` are `True`.
'''
knspace_key = StringProperty('parent', allownone=True)
'''The name of the property of this instance, to use to search upwards for
a namespace to use by this instance. Defaults to `'parent'` so that we'll
search the parent tree. See :attr:`knspace`.
When `None`, we won't search the parent tree for the namespace.
`allownone` is `True`.
'''
def _get_knsname(self):
return self._knsname
def _set_knsname(self, value):
old_name = self._knsname
knspace = self.knspace
if old_name and knspace and getattr(knspace, old_name) == self:
setattr(knspace, old_name, None)
self._knsname = value
if value:
if knspace:
setattr(knspace, value, self)
else:
raise ValueError('Object has name "{}", but no namespace'.
format(value))
knsname = AliasProperty(
_get_knsname, _set_knsname, bind=('_knsname', ), cache=False)
'''The name given to this instance. If named, the name will be added to the
associated :attr:`knspace` namespace, which will then point to the
`proxy_ref` of this instance.
When named, one can access this object by e.g. self.knspace.name, where
`name` is the given name of this instance. See :attr:`knspace` and the
module description for more details.
'''
knspace = register_context('knspace', KNSpace)
'''The default :class:`KNSpace` namespace. See :attr:`KNSpaceBehavior.knspace`
for more details.
'''

View file

@ -0,0 +1,156 @@
'''
ToggleButton Behavior
=====================
The :class:`~kivy.uix.behaviors.togglebutton.ToggleButtonBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
:class:`~kivy.uix.togglebutton.ToggleButton` behavior. You can combine this
class with other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
alternative togglebuttons that preserve Kivy togglebutton behavior.
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.
Example
-------
The following example adds togglebutton behavior to an image to make a checkbox
that behaves like a togglebutton::
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.behaviors import ToggleButtonBehavior
class MyButton(ToggleButtonBehavior, Image):
def __init__(self, **kwargs):
super(MyButton, self).__init__(**kwargs)
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
def on_state(self, widget, value):
if value == 'down':
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
else:
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
class SampleApp(App):
def build(self):
return MyButton()
SampleApp().run()
'''
__all__ = ('ToggleButtonBehavior', )
from kivy.properties import ObjectProperty, BooleanProperty
from kivy.uix.behaviors.button import ButtonBehavior
from weakref import ref
class ToggleButtonBehavior(ButtonBehavior):
'''This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
:mod:`~kivy.uix.togglebutton` behavior. Please see the
:mod:`togglebutton behaviors module <kivy.uix.behaviors.togglebutton>`
documentation for more information.
.. versionadded:: 1.8.0
'''
__groups = {}
group = ObjectProperty(None, allownone=True)
'''Group of the button. If `None`, no group will be used (the button will be
independent). If specified, :attr:`group` must be a hashable object, like
a string. Only one button in a group can be in a 'down' state.
:attr:`group` is a :class:`~kivy.properties.ObjectProperty` and defaults to
`None`.
'''
allow_no_selection = BooleanProperty(True)
'''This specifies whether the widgets in a group allow no selection i.e.
everything to be deselected.
.. versionadded:: 1.9.0
:attr:`allow_no_selection` is a :class:`BooleanProperty` and defaults to
`True`
'''
def __init__(self, **kwargs):
self._previous_group = None
super(ToggleButtonBehavior, self).__init__(**kwargs)
def on_group(self, *largs):
groups = ToggleButtonBehavior.__groups
if self._previous_group:
group = groups[self._previous_group]
for item in group[:]:
if item() is self:
group.remove(item)
break
group = self._previous_group = self.group
if group not in groups:
groups[group] = []
r = ref(self, ToggleButtonBehavior._clear_groups)
groups[group].append(r)
def _release_group(self, current):
if self.group is None:
return
group = self.__groups[self.group]
for item in group[:]:
widget = item()
if widget is None:
group.remove(item)
if widget is current:
continue
widget.state = 'normal'
def _do_press(self):
if (not self.allow_no_selection and
self.group and self.state == 'down'):
return
self._release_group(self)
self.state = 'normal' if self.state == 'down' else 'down'
def _do_release(self, *args):
pass
@staticmethod
def _clear_groups(wk):
# auto flush the element when the weak reference have been deleted
groups = ToggleButtonBehavior.__groups
for group in list(groups.values()):
if wk in group:
group.remove(wk)
break
@staticmethod
def get_widgets(groupname):
'''Return a list of the widgets contained in a specific group. If the
group doesn't exist, an empty list will be returned.
.. note::
Always release the result of this method! Holding a reference to
any of these widgets can prevent them from being garbage collected.
If in doubt, do::
l = ToggleButtonBehavior.get_widgets('mygroup')
# do your job
del l
.. warning::
It's possible that some widgets that you have previously
deleted are still in the list. The garbage collector might need
to release other objects before flushing them.
'''
groups = ToggleButtonBehavior.__groups
if groupname not in groups:
return []
return [x() for x in groups[groupname] if x()][:]

View file

@ -0,0 +1,318 @@
'''
Touch Ripple
============
.. versionadded:: 1.10.1
.. warning::
This code is still experimental, and its API is subject to change in a
future version.
This module contains `mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes
to add a touch ripple visual effect known from `Google Material Design
<https://en.wikipedia.org/wiki/Material_Design>_` to widgets.
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleBehavior` provides
rendering the ripple animation.
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleButtonBehavior`
basically provides the same functionality as
:class:`~kivy.uix.behaviors.button.ButtonBehavior` but rendering the ripple
animation instead of default press/release visualization.
'''
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.graphics import CanvasBase, Color, Ellipse, ScissorPush, ScissorPop
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, \
ObjectProperty, StringProperty
from kivy.uix.relativelayout import RelativeLayout
__all__ = (
'TouchRippleBehavior',
'TouchRippleButtonBehavior'
)
class TouchRippleBehavior(object):
'''Touch ripple behavior.
Supposed to be used as mixin on widget classes.
Ripple behavior does not trigger automatically, concrete implementation
needs to call :func:`ripple_show` respective :func:`ripple_fade` manually.
Example
-------
Here we create a Label which renders the touch ripple animation on
interaction::
class RippleLabel(TouchRippleBehavior, Label):
def __init__(self, **kwargs):
super(RippleLabel, self).__init__(**kwargs)
def on_touch_down(self, touch):
collide_point = self.collide_point(touch.x, touch.y)
if collide_point:
touch.grab(self)
self.ripple_show(touch)
return True
return False
def on_touch_up(self, touch):
if touch.grab_current is self:
touch.ungrab(self)
self.ripple_fade()
return True
return False
'''
ripple_rad_default = NumericProperty(10)
'''Default radius the animation starts from.
:attr:`ripple_rad_default` is a :class:`~kivy.properties.NumericProperty`
and defaults to `10`.
'''
ripple_duration_in = NumericProperty(.5)
'''Animation duration taken to show the overlay.
:attr:`ripple_duration_in` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.5`.
'''
ripple_duration_out = NumericProperty(.2)
'''Animation duration taken to fade the overlay.
:attr:`ripple_duration_out` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
'''
ripple_fade_from_alpha = NumericProperty(.5)
'''Alpha channel for ripple color the animation starts with.
:attr:`ripple_fade_from_alpha` is a
:class:`~kivy.properties.NumericProperty` and defaults to `0.5`.
'''
ripple_fade_to_alpha = NumericProperty(.8)
'''Alpha channel for ripple color the animation targets to.
:attr:`ripple_fade_to_alpha` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.8`.
'''
ripple_scale = NumericProperty(2.)
'''Max scale of the animation overlay calculated from max(width/height) of
the decorated widget.
:attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty`
and defaults to `2.0`.
'''
ripple_func_in = StringProperty('in_cubic')
'''Animation callback for showing the overlay.
:attr:`ripple_func_in` is a :class:`~kivy.properties.StringProperty`
and defaults to `in_cubic`.
'''
ripple_func_out = StringProperty('out_quad')
'''Animation callback for hiding the overlay.
:attr:`ripple_func_out` is a :class:`~kivy.properties.StringProperty`
and defaults to `out_quad`.
'''
ripple_rad = NumericProperty(10)
ripple_pos = ListProperty([0, 0])
ripple_color = ListProperty((1., 1., 1., .5))
def __init__(self, **kwargs):
super(TouchRippleBehavior, self).__init__(**kwargs)
self.ripple_pane = CanvasBase()
self.canvas.add(self.ripple_pane)
self.bind(
ripple_color=self._ripple_set_color,
ripple_pos=self._ripple_set_ellipse,
ripple_rad=self._ripple_set_ellipse
)
self.ripple_ellipse = None
self.ripple_col_instruction = None
def ripple_show(self, touch):
'''Begin ripple animation on current widget.
Expects touch event as argument.
'''
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
self._ripple_reset_pane()
x, y = self.to_window(*self.pos)
width, height = self.size
if isinstance(self, RelativeLayout):
self.ripple_pos = ripple_pos = (touch.x - x, touch.y - y)
else:
self.ripple_pos = ripple_pos = (touch.x, touch.y)
rc = self.ripple_color
ripple_rad = self.ripple_rad
self.ripple_color = [rc[0], rc[1], rc[2], self.ripple_fade_from_alpha]
with self.ripple_pane:
ScissorPush(
x=int(round(x)),
y=int(round(y)),
width=int(round(width)),
height=int(round(height))
)
self.ripple_col_instruction = Color(rgba=self.ripple_color)
self.ripple_ellipse = Ellipse(
size=(ripple_rad, ripple_rad),
pos=(
ripple_pos[0] - ripple_rad / 2.,
ripple_pos[1] - ripple_rad / 2.
)
)
ScissorPop()
anim = Animation(
ripple_rad=max(width, height) * self.ripple_scale,
t=self.ripple_func_in,
ripple_color=[rc[0], rc[1], rc[2], self.ripple_fade_to_alpha],
duration=self.ripple_duration_in
)
anim.start(self)
def ripple_fade(self):
'''Finish ripple animation on current widget.
'''
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
width, height = self.size
rc = self.ripple_color
duration = self.ripple_duration_out
anim = Animation(
ripple_rad=max(width, height) * self.ripple_scale,
ripple_color=[rc[0], rc[1], rc[2], 0.],
t=self.ripple_func_out,
duration=duration
)
anim.bind(on_complete=self._ripple_anim_complete)
anim.start(self)
def _ripple_set_ellipse(self, instance, value):
ellipse = self.ripple_ellipse
if not ellipse:
return
ripple_pos = self.ripple_pos
ripple_rad = self.ripple_rad
ellipse.size = (ripple_rad, ripple_rad)
ellipse.pos = (
ripple_pos[0] - ripple_rad / 2.,
ripple_pos[1] - ripple_rad / 2.
)
def _ripple_set_color(self, instance, value):
if not self.ripple_col_instruction:
return
self.ripple_col_instruction.rgba = value
def _ripple_anim_complete(self, anim, instance):
self._ripple_reset_pane()
def _ripple_reset_pane(self):
self.ripple_rad = self.ripple_rad_default
self.ripple_pane.clear()
class TouchRippleButtonBehavior(TouchRippleBehavior):
'''
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
a similar behavior to :class:`~kivy.uix.behaviors.button.ButtonBehavior`
but provides touch ripple animation instead of button pressed/released as
visual effect.
:Events:
`on_press`
Fired when the button is pressed.
`on_release`
Fired when the button is released (i.e. the touch/click that
pressed the button goes away).
'''
last_touch = ObjectProperty(None)
'''Contains the last relevant touch received by the Button. This can
be used in `on_press` or `on_release` in order to know which touch
dispatched the event.
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
defaults to `None`.
'''
always_release = BooleanProperty(False)
'''This determines whether or not the widget fires an `on_release` event if
the touch_up is outside the widget.
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
defaults to `False`.
'''
def __init__(self, **kwargs):
self.register_event_type('on_press')
self.register_event_type('on_release')
super(TouchRippleButtonBehavior, self).__init__(**kwargs)
def on_touch_down(self, touch):
if super(TouchRippleButtonBehavior, self).on_touch_down(touch):
return True
if touch.is_mouse_scrolling:
return False
if not self.collide_point(touch.x, touch.y):
return False
if self in touch.ud:
return False
touch.grab(self)
touch.ud[self] = True
self.last_touch = touch
self.ripple_show(touch)
self.dispatch('on_press')
return True
def on_touch_move(self, touch):
if touch.grab_current is self:
return True
if super(TouchRippleButtonBehavior, self).on_touch_move(touch):
return True
return self in touch.ud
def on_touch_up(self, touch):
if touch.grab_current is not self:
return super(TouchRippleButtonBehavior, self).on_touch_up(touch)
assert self in touch.ud
touch.ungrab(self)
self.last_touch = touch
if self.disabled:
return
self.ripple_fade()
if not self.always_release and not self.collide_point(*touch.pos):
return
# defer on_release until ripple_fade has completed
def defer_release(dt):
self.dispatch('on_release')
Clock.schedule_once(defer_release, self.ripple_duration_out)
return True
def on_disabled(self, instance, value):
# ensure ripple animation completes if disabled gets set to True
if value:
self.ripple_fade()
return super(TouchRippleButtonBehavior, self).on_disabled(
instance, value)
def on_press(self):
pass
def on_release(self):
pass