first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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.
|
||||
'''
|
|
@ -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()][:]
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue