Ajout du GUI
This commit is contained in:
95
kivy/uix/behaviors/__init__.py
Normal file
95
kivy/uix/behaviors/__init__.py
Normal file
@@ -0,0 +1,95 @@
|
||||
'''
|
||||
Behaviors
|
||||
=========
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Behavior mixin classes
|
||||
----------------------
|
||||
|
||||
This module implements behaviors that can be
|
||||
`mixed in <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
with existing base widgets. The idea behind these classes is to encapsulate
|
||||
properties and events associated with certain types of widgets.
|
||||
|
||||
Isolating these properties and events in a mixin class allows you to define
|
||||
your own implementation for standard kivy widgets that can act as drop-in
|
||||
replacements. This means you can re-style and re-define widgets as desired
|
||||
without breaking compatibility: as long as they implement the behaviors
|
||||
correctly, they can simply replace the standard widgets.
|
||||
|
||||
Adding behaviors
|
||||
----------------
|
||||
|
||||
Say you want to add :class:`~kivy.uix.button.Button` capabilities to an
|
||||
:class:`~kivy.uix.image.Image`, you could do::
|
||||
|
||||
class IconButton(ButtonBehavior, Image):
|
||||
pass
|
||||
|
||||
This would give you an :class:`~kivy.uix.image.Image` with the events and
|
||||
properties inherited from :class:`ButtonBehavior`. For example, the *on_press*
|
||||
and *on_release* events would be fired when appropriate::
|
||||
|
||||
class IconButton(ButtonBehavior, Image):
|
||||
def on_press(self):
|
||||
print("on_press")
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
IconButton:
|
||||
on_press: print('on_press')
|
||||
|
||||
Naturally, you could also bind to any property changes the behavior class
|
||||
offers:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def state_changed(*args):
|
||||
print('state changed')
|
||||
|
||||
button = IconButton()
|
||||
button.bind(state=state_changed)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The behavior class must always be _before_ the widget class. If you don't
|
||||
specify the inheritance in this order, the behavior will not work because
|
||||
the behavior methods are overwritten by the class method listed first.
|
||||
|
||||
Similarly, if you combine a behavior class with a class which
|
||||
requires the use of the methods also defined by the behavior class, the
|
||||
resulting class may not function properly. For example, when combining the
|
||||
:class:`ButtonBehavior` with a :class:`~kivy.uix.slider.Slider`, both of
|
||||
which use the :meth:`~kivy.uix.widget.Widget.on_touch_up` method,
|
||||
the resulting class may not work properly.
|
||||
|
||||
.. versionchanged:: 1.9.1
|
||||
|
||||
The individual behavior classes, previously in one big `behaviors.py`
|
||||
file, has been split into a single file for each class under the
|
||||
:mod:`~kivy.uix.behaviors` module. All the behaviors are still imported
|
||||
in the :mod:`~kivy.uix.behaviors` module so they are accessible as before
|
||||
(e.g. both `from kivy.uix.behaviors import ButtonBehavior` and
|
||||
`from kivy.uix.behaviors.button import ButtonBehavior` work).
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', 'ToggleButtonBehavior', 'DragBehavior',
|
||||
'FocusBehavior', 'CompoundSelectionBehavior',
|
||||
'CodeNavigationBehavior', 'EmacsBehavior', 'CoverBehavior',
|
||||
'TouchRippleBehavior', 'TouchRippleButtonBehavior')
|
||||
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from kivy.uix.behaviors.togglebutton import ToggleButtonBehavior
|
||||
from kivy.uix.behaviors.drag import DragBehavior
|
||||
from kivy.uix.behaviors.focus import FocusBehavior
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.behaviors.codenavigation import CodeNavigationBehavior
|
||||
from kivy.uix.behaviors.emacs import EmacsBehavior
|
||||
from kivy.uix.behaviors.cover import CoverBehavior
|
||||
from kivy.uix.behaviors.touchripple import TouchRippleBehavior
|
||||
from kivy.uix.behaviors.touchripple import TouchRippleButtonBehavior
|
||||
BIN
kivy/uix/behaviors/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/button.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/button.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/codenavigation.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/codenavigation.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/compoundselection.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/compoundselection.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/cover.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/cover.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/drag.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/drag.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/emacs.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/emacs.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/focus.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/focus.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/knspace.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/knspace.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/togglebutton.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/togglebutton.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/touchripple.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/touchripple.cpython-310.pyc
Normal file
Binary file not shown.
212
kivy/uix/behaviors/button.py
Normal file
212
kivy/uix/behaviors/button.py
Normal file
@@ -0,0 +1,212 @@
|
||||
'''
|
||||
Button Behavior
|
||||
===============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. You can combine this class with
|
||||
other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative buttons that preserve Kivy button behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds button behavior to an image to make a checkbox that
|
||||
behaves like a button::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_press(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
|
||||
def on_release(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
See :class:`~kivy.uix.behaviors.ButtonBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
from kivy.properties import OptionProperty, ObjectProperty, \
|
||||
BooleanProperty, NumericProperty
|
||||
from time import time
|
||||
|
||||
|
||||
class ButtonBehavior(object):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. Please see the
|
||||
:mod:`button behaviors module <kivy.uix.behaviors.button>` documentation
|
||||
for more information.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
|
||||
'''
|
||||
|
||||
state = OptionProperty('normal', options=('normal', 'down'))
|
||||
'''The state of the button, must be one of 'normal' or 'down'.
|
||||
The state is 'down' only when the button is currently touched/clicked,
|
||||
otherwise its 'normal'.
|
||||
|
||||
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'normal'.
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
min_state_time = NumericProperty(0)
|
||||
'''The minimum period of time which the widget must remain in the
|
||||
`'down'` state.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
:attr:`min_state_time` is a float and defaults to 0.035. This value is
|
||||
taken from :class:`~kivy.config.Config`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The default value is now False.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
if 'min_state_time' not in kwargs:
|
||||
self.min_state_time = float(Config.get('graphics',
|
||||
'min_state_time'))
|
||||
super(ButtonBehavior, self).__init__(**kwargs)
|
||||
self.__state_event = None
|
||||
self.__touch_time = None
|
||||
self.fbind('state', self.cancel_event)
|
||||
|
||||
def _do_press(self):
|
||||
self.state = 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
self.state = 'normal'
|
||||
|
||||
def cancel_event(self, *args):
|
||||
if self.__state_event:
|
||||
self.__state_event.cancel()
|
||||
self.__state_event = None
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(ButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.__touch_time = time()
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(ButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(ButtonBehavior, self).on_touch_up(touch)
|
||||
assert(self in touch.ud)
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
|
||||
if (not self.always_release and
|
||||
not self.collide_point(*touch.pos)):
|
||||
self._do_release()
|
||||
return
|
||||
|
||||
touchtime = time() - self.__touch_time
|
||||
if touchtime < self.min_state_time:
|
||||
self.__state_event = Clock.schedule_once(
|
||||
self._do_release, self.min_state_time - touchtime)
|
||||
else:
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
return True
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
def trigger_action(self, duration=0.1):
|
||||
'''Trigger whatever action(s) have been bound to the button by calling
|
||||
both the on_press and on_release callbacks.
|
||||
|
||||
This is similar to a quick button press without using any touch events,
|
||||
but note that like most kivy code, this is not guaranteed to be safe to
|
||||
call from external threads. If needed use
|
||||
:class:`Clock <kivy.clock.Clock>` to safely schedule this function and
|
||||
the resulting callbacks to be called from the main thread.
|
||||
|
||||
Duration is the length of the press in seconds. Pass 0 if you want
|
||||
the action to happen instantly.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
|
||||
def trigger_release(dt):
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
if not duration:
|
||||
trigger_release(0)
|
||||
else:
|
||||
Clock.schedule_once(trigger_release, duration)
|
||||
167
kivy/uix/behaviors/codenavigation.py
Normal file
167
kivy/uix/behaviors/codenavigation.py
Normal file
@@ -0,0 +1,167 @@
|
||||
'''
|
||||
Code Navigation Behavior
|
||||
========================
|
||||
|
||||
The :class:`~kivy.uix.bahviors.CodeNavigationBehavior` modifies navigation
|
||||
behavior in the :class:`~kivy.uix.textinput.TextInput`, making it work like an
|
||||
IDE instead of a word processor.
|
||||
|
||||
Using this mixin gives the TextInput the ability to recognize whitespace,
|
||||
punctuation and case variations (e.g. CamelCase) when moving over text. It
|
||||
is currently used by the :class:`~kivy.uix.codeinput.CodeInput` widget.
|
||||
'''
|
||||
|
||||
__all__ = ('CodeNavigationBehavior', )
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
import string
|
||||
|
||||
|
||||
class CodeNavigationBehavior(EventDispatcher):
|
||||
'''Code navigation behavior. Modifies the navigation behavior in TextInput
|
||||
to work like an IDE instead of a word processor. Please see the
|
||||
:mod:`code navigation behaviors module <kivy.uix.behaviors.codenavigation>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
def _move_cursor_word_left(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
pos -= 1
|
||||
|
||||
if pos == 0:
|
||||
return 0, 0
|
||||
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if col == -1:
|
||||
if row == 0:
|
||||
return 0, 0
|
||||
row -= 1
|
||||
rline = lines[row]
|
||||
col = len(rline)
|
||||
lc = c
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
if lc not in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in punct:
|
||||
col += 1
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
col += 1
|
||||
break
|
||||
if mode == 'us' and c != '_' and (c in punct or c in ws):
|
||||
col += 1
|
||||
break
|
||||
|
||||
if mode == 'us' and c != '_':
|
||||
mode = ('normal' if c in ucase
|
||||
else 'ws' if c in ws
|
||||
else 'camel')
|
||||
elif mode == 'ws' and c not in ws:
|
||||
mode = ('normal' if c in ucase
|
||||
else 'us' if c == '_'
|
||||
else 'punct' if c in punct
|
||||
else 'camel')
|
||||
|
||||
col -= 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == len(lines) - 1:
|
||||
return row, len(lines[row])
|
||||
row += 1
|
||||
col = 0
|
||||
|
||||
return col, row
|
||||
|
||||
def _move_cursor_word_right(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
mrow = len(lines) - 1
|
||||
|
||||
if row == mrow and col == len(lines[row]):
|
||||
return col, row
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c in lcase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if mode in ('normal', 'camel', 'punct') and c in ws:
|
||||
mode = 'ws'
|
||||
elif mode in ('normal', 'camel') and c == '_':
|
||||
mode = 'us'
|
||||
elif mode == 'normal' and c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
if mode == 'us':
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c != '_':
|
||||
break
|
||||
if mode == 'ws' and c not in ws:
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
break
|
||||
if mode != 'punct' and c != '_' and c in punct:
|
||||
break
|
||||
|
||||
col += 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == mrow:
|
||||
return len(rline), mrow
|
||||
row += 1
|
||||
rline = lines[row]
|
||||
col = 0
|
||||
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
break
|
||||
|
||||
return col, row
|
||||
689
kivy/uix/behaviors/compoundselection.py
Normal file
689
kivy/uix/behaviors/compoundselection.py
Normal file
@@ -0,0 +1,689 @@
|
||||
'''
|
||||
Compound Selection Behavior
|
||||
===========================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class implements the logic
|
||||
behind keyboard and touch selection of selectable widgets managed by the
|
||||
derived widget. For example, it can be combined with a
|
||||
:class:`~kivy.uix.gridlayout.GridLayout` to add selection to the layout.
|
||||
|
||||
Compound selection concepts
|
||||
---------------------------
|
||||
|
||||
At its core, it keeps a dynamic list of widgets that can be selected.
|
||||
Then, as the touches and keyboard input are passed in, it selects one or
|
||||
more of the widgets based on these inputs. For example, it uses the mouse
|
||||
scroll and keyboard up/down buttons to scroll through the list of widgets.
|
||||
Multiselection can also be achieved using the keyboard shift and ctrl keys.
|
||||
|
||||
Finally, in addition to the up/down type keyboard inputs, compound selection
|
||||
can also accept letters from the keyboard to be used to select nodes with
|
||||
associated strings that start with those letters, similar to how files
|
||||
are selected by a file browser.
|
||||
|
||||
Selection mechanics
|
||||
-------------------
|
||||
|
||||
When the controller needs to select a node, it calls :meth:`select_node` and
|
||||
:meth:`deselect_node`. Therefore, they must be overwritten in order alter
|
||||
node selection. By default, the class doesn't listen for keyboard or
|
||||
touch events, so the derived widget must call
|
||||
:meth:`select_with_touch`, :meth:`select_with_key_down`, and
|
||||
:meth:`select_with_key_up` on events that it wants to pass on for selection
|
||||
purposes.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
To add selection to a grid layout which will contain
|
||||
:class:`~kivy.uix.Button` widgets. For each button added to the layout, you
|
||||
need to bind the :attr:`~kivy.uix.widget.Widget.on_touch_down` of the button
|
||||
to :meth:`select_with_touch` to pass on the touch events::
|
||||
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.core.window import Window
|
||||
from kivy.app import App
|
||||
|
||||
|
||||
class SelectableGrid(FocusBehavior, CompoundSelectionBehavior, GridLayout):
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key presses will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_down(
|
||||
window, keycode, text, modifiers):
|
||||
return True
|
||||
if self.select_with_key_down(window, keycode, text, modifiers):
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key release will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_up(window, keycode):
|
||||
return True
|
||||
if self.select_with_key_up(window, keycode):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
""" Override the adding of widgets so we can bind and catch their
|
||||
*on_touch_down* events. """
|
||||
widget.bind(on_touch_down=self.button_touch_down,
|
||||
on_touch_up=self.button_touch_up)
|
||||
return super(SelectableGrid, self)\
|
||||
.add_widget(widget, *args, **kwargs)
|
||||
|
||||
def button_touch_down(self, button, touch):
|
||||
""" Use collision detection to select buttons when the touch occurs
|
||||
within their area. """
|
||||
if button.collide_point(*touch.pos):
|
||||
self.select_with_touch(button, touch)
|
||||
|
||||
def button_touch_up(self, button, touch):
|
||||
""" Use collision detection to de-select buttons when the touch
|
||||
occurs outside their area and *touch_multiselect* is not True. """
|
||||
if not (button.collide_point(*touch.pos) or
|
||||
self.touch_multiselect):
|
||||
self.deselect_node(button)
|
||||
|
||||
def select_node(self, node):
|
||||
node.background_color = (1, 0, 0, 1)
|
||||
return super(SelectableGrid, self).select_node(node)
|
||||
|
||||
def deselect_node(self, node):
|
||||
node.background_color = (1, 1, 1, 1)
|
||||
super(SelectableGrid, self).deselect_node(node)
|
||||
|
||||
def on_selected_nodes(self, grid, nodes):
|
||||
print("Selected nodes = {0}".format(nodes))
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
grid = SelectableGrid(cols=3, rows=2, touch_multiselect=True,
|
||||
multiselect=True)
|
||||
for i in range(0, 6):
|
||||
grid.add_widget(Button(text="Button {0}".format(i)))
|
||||
return grid
|
||||
|
||||
|
||||
TestApp().run()
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('CompoundSelectionBehavior', )
|
||||
|
||||
from time import time
|
||||
from os import environ
|
||||
|
||||
from kivy.properties import NumericProperty, BooleanProperty, ListProperty
|
||||
|
||||
|
||||
if 'KIVY_DOC' not in environ:
|
||||
from kivy.config import Config
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
else:
|
||||
_is_desktop = False
|
||||
|
||||
|
||||
class CompoundSelectionBehavior(object):
|
||||
'''The Selection behavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
implements the logic behind keyboard and touch
|
||||
selection of selectable widgets managed by the derived widget. Please see
|
||||
the :mod:`compound selection behaviors module
|
||||
<kivy.uix.behaviors.compoundselection>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
selected_nodes = ListProperty([])
|
||||
'''The list of selected nodes.
|
||||
|
||||
.. note::
|
||||
|
||||
Multiple nodes can be selected right after one another e.g. using the
|
||||
keyboard. When listening to :attr:`selected_nodes`, one should be
|
||||
aware of this.
|
||||
|
||||
:attr:`selected_nodes` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to the empty list, []. It is read-only and should not be modified.
|
||||
'''
|
||||
|
||||
touch_multiselect = BooleanProperty(False)
|
||||
'''A special touch mode which determines whether touch events, as
|
||||
processed by :meth:`select_with_touch`, will add the currently touched
|
||||
node to the selection, or if it will clear the selection before adding the
|
||||
node. This allows the selection of multiple nodes by simply touching them.
|
||||
|
||||
This is different from :attr:`multiselect` because when it is True,
|
||||
simply touching an unselected node will select it, even if ctrl is not
|
||||
pressed. If it is False, however, ctrl must be pressed in order to
|
||||
add to the selection when :attr:`multiselect` is True.
|
||||
|
||||
.. note::
|
||||
|
||||
:attr:`multiselect`, when False, will disable
|
||||
:attr:`touch_multiselect`.
|
||||
|
||||
:attr:`touch_multiselect` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
multiselect = BooleanProperty(False)
|
||||
'''Determines whether multiple nodes can be selected. If enabled, keyboard
|
||||
shift and ctrl selection, optionally combined with touch, for example, will
|
||||
be able to select multiple widgets in the normally expected manner.
|
||||
This dominates :attr:`touch_multiselect` when False.
|
||||
|
||||
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
touch_deselect_last = BooleanProperty(not _is_desktop)
|
||||
'''Determines whether the last selected node can be deselected when
|
||||
:attr:`multiselect` or :attr:`touch_multiselect` is False.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`touch_deselect_last` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True on mobile, False on desktop platforms.
|
||||
'''
|
||||
|
||||
keyboard_select = BooleanProperty(True)
|
||||
'''Determines whether the keyboard can be used for selection. If False,
|
||||
keyboard inputs will be ignored.
|
||||
|
||||
:attr:`keyboard_select` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
page_count = NumericProperty(10)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when pageup (or pagedown) is
|
||||
pressed.
|
||||
|
||||
:attr:`page_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 10.
|
||||
'''
|
||||
|
||||
up_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the up (or down) arrow on
|
||||
the keyboard is pressed.
|
||||
|
||||
:attr:`up_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
right_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the right (or left) arrow
|
||||
on the keyboard is pressed.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
scroll_count = NumericProperty(0)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the mouse scroll wheel is
|
||||
scrolled.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
nodes_order_reversed = BooleanProperty(True)
|
||||
''' (Internal) Indicates whether the order of the nodes as displayed top-
|
||||
down is reversed compared to their order in :meth:`get_selectable_nodes`
|
||||
(e.g. how the children property is reversed compared to how
|
||||
it's displayed).
|
||||
'''
|
||||
|
||||
text_entry_timeout = NumericProperty(1.)
|
||||
'''When typing characters in rapid succession (i.e. the time difference since
|
||||
the last character is less than :attr:`text_entry_timeout`), the keys get
|
||||
concatenated and the combined text is passed as the key argument of
|
||||
:meth:`goto_node`.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
_anchor = None # the last anchor node selected (e.g. shift relative node)
|
||||
# the idx may be out of sync
|
||||
_anchor_idx = 0 # cache indexs 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
|
||||
160
kivy/uix/behaviors/cover.py
Normal file
160
kivy/uix/behaviors/cover.py
Normal file
@@ -0,0 +1,160 @@
|
||||
'''
|
||||
Cover Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.cover.CoverBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ is intended for rendering
|
||||
textures to full widget size keeping the aspect ratio of the original texture.
|
||||
|
||||
Use cases are i.e. rendering full size background images or video content in
|
||||
a dynamic layout.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following examples add cover behavior to an image:
|
||||
|
||||
In python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import CoverBehavior
|
||||
from kivy.uix.image import Image
|
||||
|
||||
|
||||
class CoverImage(CoverBehavior, Image):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverImage, self).__init__(**kwargs)
|
||||
texture = self._coreimage.texture
|
||||
self.reference_size = texture.size
|
||||
self.texture = texture
|
||||
|
||||
|
||||
class MainApp(App):
|
||||
|
||||
def build(self):
|
||||
return CoverImage(source='image.jpg')
|
||||
|
||||
MainApp().run()
|
||||
|
||||
In Kivy Language:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
CoverImage:
|
||||
source: 'image.png'
|
||||
|
||||
<CoverImage@CoverBehavior+Image>:
|
||||
reference_size: self.texture_size
|
||||
|
||||
See :class:`~kivy.uix.behaviors.cover.CoverBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('CoverBehavior', )
|
||||
|
||||
from decimal import Decimal
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ListProperty
|
||||
|
||||
|
||||
Builder.load_string("""
|
||||
<-CoverBehavior>:
|
||||
canvas.before:
|
||||
StencilPush
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilUse
|
||||
canvas:
|
||||
Rectangle:
|
||||
texture: self.texture
|
||||
size: self.cover_size
|
||||
pos: self.cover_pos
|
||||
canvas.after:
|
||||
StencilUnUse
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilPop
|
||||
""")
|
||||
|
||||
|
||||
class CoverBehavior(object):
|
||||
'''The CoverBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
provides rendering a texture covering full widget size keeping aspect ratio
|
||||
of the original texture.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
reference_size = ListProperty([])
|
||||
'''Reference size used for aspect ratio approximation calculation.
|
||||
|
||||
:attr:`reference_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[]`.
|
||||
'''
|
||||
|
||||
cover_size = ListProperty([0, 0])
|
||||
'''Size of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
cover_pos = ListProperty([0, 0])
|
||||
'''Position of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_pos` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverBehavior, self).__init__(**kwargs)
|
||||
# bind covering
|
||||
self.bind(
|
||||
size=self.calculate_cover,
|
||||
pos=self.calculate_cover
|
||||
)
|
||||
|
||||
def _aspect_ratio_approximate(self, size):
|
||||
# return a decimal approximation of an aspect ratio.
|
||||
return Decimal('%.2f' % (float(size[0]) / size[1]))
|
||||
|
||||
def _scale_size(self, size, sizer):
|
||||
# return scaled size based on sizer, where sizer (n, None) scales x
|
||||
# to n and (None, n) scales y to n
|
||||
size_new = list(sizer)
|
||||
i = size_new.index(None)
|
||||
j = i * -1 + 1
|
||||
size_new[i] = (size_new[j] * size[i]) / size[j]
|
||||
return tuple(size_new)
|
||||
|
||||
def calculate_cover(self, *args):
|
||||
# return if no reference size yet
|
||||
if not self.reference_size:
|
||||
return
|
||||
size = self.size
|
||||
origin_appr = self._aspect_ratio_approximate(self.reference_size)
|
||||
crop_appr = self._aspect_ratio_approximate(size)
|
||||
# same aspect ratio
|
||||
if origin_appr == crop_appr:
|
||||
crop_size = self.size
|
||||
offset = (0, 0)
|
||||
# scale x
|
||||
elif origin_appr < crop_appr:
|
||||
crop_size = self._scale_size(self.reference_size, (size[0], None))
|
||||
offset = (0, ((crop_size[1] - size[1]) / 2) * -1)
|
||||
# scale y
|
||||
else:
|
||||
crop_size = self._scale_size(self.reference_size, (None, size[1]))
|
||||
offset = (((crop_size[0] - size[0]) / 2) * -1, 0)
|
||||
# set background size and position
|
||||
self.cover_size = crop_size
|
||||
self.cover_pos = offset
|
||||
234
kivy/uix/behaviors/drag.py
Normal file
234
kivy/uix/behaviors/drag.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Drag Behavior
|
||||
=============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.drag.DragBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides Drag behavior.
|
||||
When combined with a widget, dragging in the rectangle defined by the
|
||||
:attr:`~kivy.uix.behaviors.drag.DragBehavior.drag_rectangle` will drag the
|
||||
widget.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example creates a draggable label::
|
||||
|
||||
from kivy.uix.label import Label
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import DragBehavior
|
||||
from kivy.lang import Builder
|
||||
|
||||
# You could also put the following in your kv file...
|
||||
kv = '''
|
||||
<DragLabel>:
|
||||
# Define the properties for the DragLabel
|
||||
drag_rectangle: self.x, self.y, self.width, self.height
|
||||
drag_timeout: 10000000
|
||||
drag_distance: 0
|
||||
|
||||
FloatLayout:
|
||||
# Define the root widget
|
||||
DragLabel:
|
||||
size_hint: 0.25, 0.2
|
||||
text: 'Drag me'
|
||||
'''
|
||||
|
||||
|
||||
class DragLabel(DragBehavior, Label):
|
||||
pass
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return Builder.load_string(kv)
|
||||
|
||||
TestApp().run()
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ('DragBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import NumericProperty, ReferenceListProperty
|
||||
from kivy.config import Config
|
||||
from kivy.metrics import sp
|
||||
from functools import partial
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_scroll_timeout = _scroll_distance = 0
|
||||
if Config:
|
||||
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
|
||||
_scroll_distance = Config.getint('widgets', 'scroll_distance')
|
||||
|
||||
|
||||
class DragBehavior(object):
|
||||
'''
|
||||
The DragBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_ provides
|
||||
Drag behavior. When combined with a widget, dragging in the rectangle
|
||||
defined by :attr:`drag_rectangle` will drag the widget. Please see
|
||||
the :mod:`drag behaviors module <kivy.uix.behaviors.drag>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
drag_distance = NumericProperty(_scroll_distance)
|
||||
'''Distance to move before dragging the :class:`DragBehavior`, in pixels.
|
||||
As soon as the distance has been traveled, the :class:`DragBehavior` will
|
||||
start to drag, and no touch event will be dispatched to the children.
|
||||
It is advisable that you base this value on the dpi of your target device's
|
||||
screen.
|
||||
|
||||
:attr:`drag_distance` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_distance` as defined in the user
|
||||
:class:`~kivy.config.Config` (20 pixels by default).
|
||||
'''
|
||||
|
||||
drag_timeout = NumericProperty(_scroll_timeout)
|
||||
'''Timeout allowed to trigger the :attr:`drag_distance`, in milliseconds.
|
||||
If the user has not moved :attr:`drag_distance` within the timeout,
|
||||
dragging will be disabled, and the touch event will be dispatched to the
|
||||
children.
|
||||
|
||||
:attr:`drag_timeout` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_timeout` as defined in the user
|
||||
:class:`~kivy.config.Config` (55 milliseconds by default).
|
||||
'''
|
||||
|
||||
drag_rect_x = NumericProperty(0)
|
||||
'''X position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_x` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_y = NumericProperty(0)
|
||||
'''Y position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_Y` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_width = NumericProperty(100)
|
||||
'''Width of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rect_height = NumericProperty(100)
|
||||
'''Height of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rectangle = ReferenceListProperty(drag_rect_x, drag_rect_y,
|
||||
drag_rect_width, drag_rect_height)
|
||||
'''Position and size of the axis aligned bounding rectangle where dragging
|
||||
is allowed.
|
||||
|
||||
:attr:`drag_rectangle` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`drag_rect_x`, :attr:`drag_rect_y`, :attr:`drag_rect_width`,
|
||||
:attr:`drag_rect_height`) properties.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._drag_touch = None
|
||||
super(DragBehavior, self).__init__(**kwargs)
|
||||
|
||||
def _get_uid(self, prefix='sv'):
|
||||
return '{0}.{1}'.format(prefix, self.uid)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
xx, yy, w, h = self.drag_rectangle
|
||||
x, y = touch.pos
|
||||
if not self.collide_point(x, y):
|
||||
touch.ud[self._get_uid('svavoid')] = True
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
if self._drag_touch or ('button' in touch.profile and
|
||||
touch.button.startswith('scroll')) or\
|
||||
not ((xx < x <= xx + w) and (yy < y <= yy + h)):
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
|
||||
# no mouse scrolling, so the user is going to drag with this touch.
|
||||
self._drag_touch = touch
|
||||
uid = self._get_uid()
|
||||
touch.grab(self)
|
||||
touch.ud[uid] = {
|
||||
'mode': 'unknown',
|
||||
'dx': 0,
|
||||
'dy': 0}
|
||||
Clock.schedule_once(self._change_touch_mode,
|
||||
self.drag_timeout / 1000.)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud or\
|
||||
self._drag_touch is not touch:
|
||||
return super(DragBehavior, self).on_touch_move(touch) or\
|
||||
self._get_uid() in touch.ud
|
||||
if touch.grab_current is not self:
|
||||
return True
|
||||
|
||||
uid = self._get_uid()
|
||||
ud = touch.ud[uid]
|
||||
mode = ud['mode']
|
||||
if mode == 'unknown':
|
||||
ud['dx'] += abs(touch.dx)
|
||||
ud['dy'] += abs(touch.dy)
|
||||
if ud['dx'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
if ud['dy'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
ud['mode'] = mode
|
||||
if mode == 'drag':
|
||||
self.x += touch.dx
|
||||
self.y += touch.dy
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud:
|
||||
return super(DragBehavior, self).on_touch_up(touch)
|
||||
|
||||
if self._drag_touch and self in [x() for x in touch.grab_list]:
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
ud = touch.ud[self._get_uid()]
|
||||
if ud['mode'] == 'unknown':
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
|
||||
else:
|
||||
if self._drag_touch is not touch:
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
|
||||
def _do_touch_up(self, touch, *largs):
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
# don't forget about grab event!
|
||||
for x in touch.grab_list[:]:
|
||||
touch.grab_list.remove(x)
|
||||
x = x()
|
||||
if not x:
|
||||
continue
|
||||
touch.grab_current = x
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
touch.grab_current = None
|
||||
|
||||
def _change_touch_mode(self, *largs):
|
||||
if not self._drag_touch:
|
||||
return
|
||||
uid = self._get_uid()
|
||||
touch = self._drag_touch
|
||||
ud = touch.ud[uid]
|
||||
if ud['mode'] != 'unknown':
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.parent.to_widget)
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
touch.pop()
|
||||
return
|
||||
140
kivy/uix/behaviors/emacs.py
Normal file
140
kivy/uix/behaviors/emacs.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
Emacs Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.emacs.EmacsBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ allows you to add
|
||||
`Emacs <https://www.gnu.org/software/emacs/>`_ keyboard shortcuts for basic
|
||||
movement and editing to the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
The shortcuts currently available are listed below:
|
||||
|
||||
Emacs shortcuts
|
||||
---------------
|
||||
=============== ========================================================
|
||||
Shortcut Description
|
||||
--------------- --------------------------------------------------------
|
||||
Control + a Move cursor to the beginning of the line
|
||||
Control + e Move cursor to the end of the line
|
||||
Control + f Move cursor one character to the right
|
||||
Control + b Move cursor one character to the left
|
||||
Alt + f Move cursor to the end of the word to the right
|
||||
Alt + b Move cursor to the start of the word to the left
|
||||
Alt + Backspace Delete text left of the cursor to the beginning of word
|
||||
Alt + d Delete text right of the cursor to the end of the word
|
||||
Alt + w Copy selection
|
||||
Control + w Cut selection
|
||||
Control + y Paste selection
|
||||
=============== ========================================================
|
||||
|
||||
.. warning::
|
||||
If you have the :mod:`~kivy.modules.inspector` module enabled, the
|
||||
shortcut for opening the inspector (Control + e) conflicts with the
|
||||
Emacs shortcut to move to the end of the line (it will still move the
|
||||
cursor to the end of the line, but the inspector will open as well).
|
||||
'''
|
||||
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
|
||||
__all__ = ('EmacsBehavior', )
|
||||
|
||||
|
||||
class EmacsBehavior(object):
|
||||
'''
|
||||
A `mixin <https://en.wikipedia.org/wiki/Mixin>`_ that enables Emacs-style
|
||||
keyboard shortcuts for the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
Please see the :mod:`Emacs behaviors module <kivy.uix.behaviors.emacs>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
key_bindings = StringProperty('emacs')
|
||||
'''String name which determines the type of key bindings to use with the
|
||||
:class:`~kivy.uix.textinput.TextInput`. This allows Emacs key bindings to
|
||||
be enabled/disabled programmatically for widgets that inherit from
|
||||
:class:`EmacsBehavior`. If the value is not ``'emacs'``, Emacs bindings
|
||||
will be disabled. Use ``'default'`` for switching to the default key
|
||||
bindings of TextInput.
|
||||
|
||||
:attr:`key_bindings` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to ``'emacs'``.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EmacsBehavior, self).__init__(**kwargs)
|
||||
|
||||
self.bindings = {
|
||||
'ctrl': {
|
||||
'a': lambda: self.do_cursor_movement('cursor_home'),
|
||||
'e': lambda: self.do_cursor_movement('cursor_end'),
|
||||
'f': lambda: self.do_cursor_movement('cursor_right'),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left'),
|
||||
'w': lambda: self._cut(self.selection_text),
|
||||
'y': self.paste,
|
||||
},
|
||||
'alt': {
|
||||
'w': self.copy,
|
||||
'f': lambda: self.do_cursor_movement('cursor_right',
|
||||
control=True),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left',
|
||||
control=True),
|
||||
'd': self.delete_word_right,
|
||||
'\x08': self.delete_word_left, # alt + backspace
|
||||
},
|
||||
}
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
|
||||
key, key_str = keycode
|
||||
|
||||
# join the modifiers e.g. ['alt', 'ctrl']
|
||||
mod = '+'.join(modifiers) if modifiers else None
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if key in range(256) and self.key_bindings == 'emacs':
|
||||
if mod == 'ctrl' and chr(key) in self.bindings['ctrl'].keys():
|
||||
is_emacs_shortcut = True
|
||||
elif mod == 'alt' and chr(key) in self.bindings['alt'].keys():
|
||||
is_emacs_shortcut = True
|
||||
else: # e.g. ctrl+alt or alt+ctrl (alt-gr key)
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if is_emacs_shortcut:
|
||||
# Look up mod and key
|
||||
emacs_shortcut = self.bindings[mod][chr(key)]
|
||||
emacs_shortcut()
|
||||
else:
|
||||
super(EmacsBehavior, self).keyboard_on_key_down(window, keycode,
|
||||
text, modifiers)
|
||||
|
||||
def delete_word_right(self):
|
||||
'''Delete text right of the cursor to the end of the word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
start_cursor = self.cursor
|
||||
self.do_cursor_movement('cursor_right', control=True)
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[start_index:end_index]
|
||||
self._set_unredo_delsel(start_index, end_index, s, from_undo=False)
|
||||
self.text = self.text[:start_index] + self.text[end_index:]
|
||||
self._set_cursor(pos=start_cursor)
|
||||
|
||||
def delete_word_left(self):
|
||||
'''Delete text left of the cursor to the beginning of word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
self.do_cursor_movement('cursor_left', control=True)
|
||||
end_cursor = self.cursor
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[end_index:start_index]
|
||||
self._set_unredo_delsel(end_index, start_index, s, from_undo=False)
|
||||
self.text = self.text[:end_index] + self.text[start_index:]
|
||||
self._set_cursor(pos=end_cursor)
|
||||
587
kivy/uix/behaviors/focus.py
Normal file
587
kivy/uix/behaviors/focus.py
Normal file
@@ -0,0 +1,587 @@
|
||||
'''
|
||||
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`.
|
||||
|
||||
.. 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 code, ensure following code is not infinite loop:
|
||||
widget.focus_next = widget
|
||||
widget.focus_previous = widget
|
||||
widget.focus_previous = widget2
|
||||
'''
|
||||
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 behaviour 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 of only one keyboard, are
|
||||
all focus aware widgets), then as the other widgets gains focus, this
|
||||
widget loses focus. In addition to that, if this property is `True`,
|
||||
clicking on any widget other than this widget, will remove focus form this
|
||||
widget.
|
||||
|
||||
:attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` 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 ``tab`` + ``shift`` key 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()
|
||||
590
kivy/uix/behaviors/knspace.py
Normal file
590
kivy/uix/behaviors/knspace.py
Normal file
@@ -0,0 +1,590 @@
|
||||
'''
|
||||
Kivy Namespaces
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
The :class:`KNSpaceBehavior` `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
class provides namespace functionality for Kivy objects. It allows kivy objects
|
||||
to be named and then accessed using namespaces.
|
||||
|
||||
:class:`KNSpace` instances are the namespaces that store the named objects
|
||||
in Kivy :class:`~kivy.properties.ObjectProperty` instances.
|
||||
In addition, when inheriting from :class:`KNSpaceBehavior`, if the derived
|
||||
object is named, the name will automatically be added to the associated
|
||||
namespace and will point to a :attr:`~kivy.uix.widget.proxy_ref` of the
|
||||
derived object.
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
|
||||
By default, there's only a single namespace: the :attr:`knspace` namespace. The
|
||||
simplest example is adding a widget to the namespace:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.uix.behaviors.knspace import knspace
|
||||
widget = Widget()
|
||||
knspace.my_widget = widget
|
||||
|
||||
This adds a kivy :class:`~kivy.properties.ObjectProperty` with `rebind=True`
|
||||
and `allownone=True` to the :attr:`knspace` namespace with a property name
|
||||
`my_widget`. And the property now also points to this widget.
|
||||
|
||||
This can be done automatically with:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knsname: 'my_widget'
|
||||
|
||||
Now, `knspace.my_widget` will point to that widget.
|
||||
|
||||
When one creates a second widget with the same name, the namespace will
|
||||
also change to point to the new widget. E.g.:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget
|
||||
widget2 = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget2
|
||||
|
||||
Setting the namespace
|
||||
---------------------
|
||||
|
||||
One can also create ones own namespace rather than using the default
|
||||
:attr:`knspace` by directly setting :attr:`KNSpaceBehavior.knspace`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
my_new_namespace = KNSpace()
|
||||
widget.knspace = my_new_namespace
|
||||
|
||||
Initially, `my_widget` is added to the default namespace, but when the widget's
|
||||
namespace is changed to `my_new_namespace`, the reference to `my_widget` is
|
||||
moved to that namespace. We could have also of course first set the namespace
|
||||
to `my_new_namespace` and then have named the widget `my_widget`, thereby
|
||||
avoiding the initial assignment to the default namespace.
|
||||
|
||||
Similarly, in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knspace: KNSpace()
|
||||
knsname: 'my_widget'
|
||||
|
||||
Inheriting the namespace
|
||||
------------------------
|
||||
|
||||
In the previous example, we directly set the namespace we wished to use.
|
||||
In the following example, we inherit it from the parent, so we only have to set
|
||||
it once:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
<MyLabel@KNSpaceBehavior+Label>
|
||||
|
||||
<MyComplexWidget@MyWidget>:
|
||||
knsname: 'my_complex'
|
||||
MyLabel:
|
||||
knsname: 'label1'
|
||||
MyLabel:
|
||||
knsname: 'label2'
|
||||
|
||||
Then, we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
widget.knspace = new_knspace
|
||||
|
||||
The rule is that if no knspace has been assigned to a widget, it looks for a
|
||||
namespace in its parent and parent's parent and so on until it find one to
|
||||
use. If none are found, it uses the default :attr:`knspace`.
|
||||
|
||||
When `MyComplexWidget` is created, it still used the default namespace.
|
||||
However, when we assigned the root widget its new namespace, all its
|
||||
children switched to using that new namespace as well. So `new_knspace` now
|
||||
contains `label1` and `label2` as well as `my_complex`.
|
||||
|
||||
If we had first done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
knspace.label1.knspace = knspace
|
||||
widget.knspace = new_knspace
|
||||
|
||||
Then `label1` would remain stored in the default :attr:`knspace` since it was
|
||||
directly set, but `label2` and `my_complex` would still be added to the new
|
||||
namespace.
|
||||
|
||||
One can customize the attribute used to search the parent tree by changing
|
||||
:attr:`KNSpaceBehavior.knspace_key`. If the desired knspace is not reachable
|
||||
through a widgets parent tree, e.g. in a popup that is not a widget's child,
|
||||
:attr:`KNSpaceBehavior.knspace_key` can be used to establish a different
|
||||
search order.
|
||||
|
||||
Accessing the namespace
|
||||
-----------------------
|
||||
|
||||
As seen in the previous example, if not directly assigned, the namespace is
|
||||
found by searching the parent tree. Consequently, if a namespace was assigned
|
||||
further up the parent tree, all its children and below could access that
|
||||
namespace through their :attr:`KNSpaceBehavior.knspace` property.
|
||||
|
||||
This allows the creation of multiple widgets with identically given names
|
||||
if each root widget instance is assigned a new namespace. For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Now, when we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
knspace1, knspace2 = KNSpace(), KNSpace()
|
||||
composite1 = MyCompositeWidget()
|
||||
composite1.knspace = knspace1
|
||||
|
||||
composite2 = MyCompositeWidget()
|
||||
composite2.knspace = knspace2
|
||||
|
||||
knspace1.pretty = "Here's the ladder, now fix the roof!"
|
||||
knspace2.pretty = "Get that raccoon off me!"
|
||||
|
||||
Because each of the `MyCompositeWidget` instances have a different namespace
|
||||
their children also use different namespaces. Consequently, the
|
||||
pretty and complex widgets of each instance will have different text.
|
||||
|
||||
Further, because both the namespace :class:`~kivy.properties.ObjectProperty`
|
||||
references, and :attr:`KNSpaceBehavior.knspace` have `rebind=True`, the
|
||||
text of the `MyComplexWidget` label is rebound to match the text of
|
||||
`MyPrettyWidget` when either the root's namespace changes or when the
|
||||
`root.knspace.pretty` property changes, as expected.
|
||||
|
||||
Forking a namespace
|
||||
-------------------
|
||||
|
||||
Forking a namespace provides the opportunity to create a new namespace
|
||||
from a parent namespace so that the forked namespace will contain everything
|
||||
in the origin namespace, but the origin namespace will not have access to
|
||||
anything added to the forked namespace.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
child = knspace.fork()
|
||||
grandchild = child.fork()
|
||||
|
||||
child.label = Label()
|
||||
grandchild.button = Button()
|
||||
|
||||
Now label is accessible by both child and grandchild, but not by knspace. And
|
||||
button is only accessible by the grandchild but not by the child or by knspace.
|
||||
Finally, doing `grandchild.label = Label()` will leave `grandchild.label`
|
||||
and `child.label` pointing to different labels.
|
||||
|
||||
A motivating example is the example from above:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
knspace: 'fork'
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Notice the addition of `knspace: 'fork'`. This is identical to doing
|
||||
`knspace: self.knspace.fork()`. However, doing that would lead to infinite
|
||||
recursion as that kv rule would be executed recursively because `self.knspace`
|
||||
will keep on changing. However, allowing `knspace: 'fork'` cirumvents that.
|
||||
See :attr:`KNSpaceBehavior.knspace`.
|
||||
|
||||
Now, having forked, we just need to do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
composite1 = MyCompositeWidget()
|
||||
composite2 = MyCompositeWidget()
|
||||
|
||||
composite1.knspace.pretty = "Here's the ladder, now fix the roof!"
|
||||
composite2.knspace.pretty = "Get that raccoon off me!"
|
||||
|
||||
Since by forking we automatically created a unique namespace for each
|
||||
`MyCompositeWidget` instance.
|
||||
'''
|
||||
|
||||
__all__ = ('KNSpace', 'KNSpaceBehavior', 'knspace')
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.properties import StringProperty, ObjectProperty, AliasProperty
|
||||
from kivy.context import register_context
|
||||
|
||||
|
||||
class KNSpace(EventDispatcher):
|
||||
'''Each :class:`KNSpace` instance is a namespace that stores the named Kivy
|
||||
objects associated with this namespace. Each named object is
|
||||
stored as the value of a Kivy :class:`~kivy.properties.ObjectProperty` of
|
||||
this instance whose property name is the object's given name. Both `rebind`
|
||||
and `allownone` are set to `True` for the property.
|
||||
|
||||
See :attr:`KNSpaceBehavior.knspace` for details on how a namespace is
|
||||
associated with a named object.
|
||||
|
||||
When storing an object in the namespace, the object's `proxy_ref` is
|
||||
stored if the object has such an attribute.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`parent`: (internal) A :class:`KNSpace` instance or None.
|
||||
If specified, it's a parent namespace, in which case, the current
|
||||
namespace will have in its namespace all its named objects
|
||||
as well as the named objects of its parent and parent's parent
|
||||
etc. See :meth:`fork` for more details.
|
||||
'''
|
||||
|
||||
parent = None
|
||||
'''(internal) The parent namespace instance, :class:`KNSpace`, or None. See
|
||||
:meth:`fork`.
|
||||
'''
|
||||
__has_applied = None
|
||||
|
||||
keep_ref = False
|
||||
'''Whether a direct reference should be kept to the stored objects.
|
||||
If ``True``, we use the direct object, otherwise we use
|
||||
:attr:`~kivy.uix.widget.proxy_ref` when present.
|
||||
|
||||
Defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, parent=None, keep_ref=False, **kwargs):
|
||||
self.keep_ref = keep_ref
|
||||
super(KNSpace, self).__init__(**kwargs)
|
||||
self.parent = parent
|
||||
self.__has_applied = set(self.properties().keys())
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
has_applied = self.__has_applied
|
||||
if prop is None:
|
||||
if hasattr(self, name):
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
self.apply_property(
|
||||
**{name:
|
||||
ObjectProperty(None, rebind=True, allownone=True)}
|
||||
)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
has_applied.add(name)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
elif name not in has_applied:
|
||||
self.apply_property(**{name: prop})
|
||||
has_applied.add(name)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in super(KNSpace, self).__getattribute__('__dict__'):
|
||||
return super(KNSpace, self).__getattribute__(name)
|
||||
|
||||
try:
|
||||
value = super(KNSpace, self).__getattribute__(name)
|
||||
except AttributeError:
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
raise AttributeError(name)
|
||||
return getattr(parent, name)
|
||||
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return getattr(parent, name) # if parent doesn't have it
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def property(self, name, quiet=False):
|
||||
# needs to overwrite EventDispatcher.property so kv lang will work
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
if prop is not None:
|
||||
return prop
|
||||
|
||||
prop = ObjectProperty(None, rebind=True, allownone=True)
|
||||
self.apply_property(**{name: prop})
|
||||
self.__has_applied.add(name)
|
||||
return prop
|
||||
|
||||
def fork(self):
|
||||
'''Returns a new :class:`KNSpace` instance which will have access to
|
||||
all the named objects in the current namespace but will also have a
|
||||
namespace of its own that is unique to it.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
forked_knspace1 = knspace.fork()
|
||||
forked_knspace2 = knspace.fork()
|
||||
|
||||
Now, any names added to `knspace` will be accessible by the
|
||||
`forked_knspace1` and `forked_knspace2` namespaces by the normal means.
|
||||
However, any names added to `forked_knspace1` will not be accessible
|
||||
from `knspace` or `forked_knspace2`. Similar for `forked_knspace2`.
|
||||
'''
|
||||
return KNSpace(parent=self)
|
||||
|
||||
|
||||
class KNSpaceBehavior(object):
|
||||
'''Inheriting from this class allows naming of the inherited objects, which
|
||||
are then added to the associated namespace :attr:`knspace` and accessible
|
||||
through it.
|
||||
|
||||
Please see the :mod:`knspace behaviors module <kivy.uix.behaviors.knspace>`
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
_knspace = ObjectProperty(None, allownone=True)
|
||||
_knsname = StringProperty('')
|
||||
__last_knspace = None
|
||||
__callbacks = None
|
||||
|
||||
def __init__(self, knspace=None, **kwargs):
|
||||
self.knspace = knspace
|
||||
super(KNSpaceBehavior, self).__init__(**kwargs)
|
||||
|
||||
def __knspace_clear_callbacks(self, *largs):
|
||||
for obj, name, uid in self.__callbacks:
|
||||
obj.unbind_uid(name, uid)
|
||||
last = self.__last_knspace
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
assert self._knspace is None
|
||||
assert last
|
||||
|
||||
new = self.__set_parent_knspace()
|
||||
if new is last:
|
||||
return
|
||||
self.property('_knspace').dispatch(self)
|
||||
|
||||
name = self.knsname
|
||||
if not name:
|
||||
return
|
||||
|
||||
if getattr(last, name) == self:
|
||||
setattr(last, name, None)
|
||||
|
||||
if new:
|
||||
setattr(new, name, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
|
||||
def __set_parent_knspace(self):
|
||||
callbacks = self.__callbacks = []
|
||||
fbind = self.fbind
|
||||
append = callbacks.append
|
||||
parent_key = self.knspace_key
|
||||
clear = self.__knspace_clear_callbacks
|
||||
|
||||
append((self, 'knspace_key', fbind('knspace_key', clear)))
|
||||
if not parent_key:
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
append((self, parent_key, fbind(parent_key, clear)))
|
||||
parent = getattr(self, parent_key, None)
|
||||
while parent is not None:
|
||||
fbind = parent.fbind
|
||||
|
||||
parent_knspace = getattr(parent, 'knspace', 0)
|
||||
if parent_knspace != 0:
|
||||
append((parent, 'knspace', fbind('knspace', clear)))
|
||||
self.__last_knspace = parent_knspace
|
||||
return parent_knspace
|
||||
|
||||
append((parent, parent_key, fbind(parent_key, clear)))
|
||||
new_parent = getattr(parent, parent_key, None)
|
||||
if new_parent is parent:
|
||||
break
|
||||
parent = new_parent
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
def _get_knspace(self):
|
||||
_knspace = self._knspace
|
||||
if _knspace is not None:
|
||||
return _knspace
|
||||
|
||||
if self.__callbacks is not None:
|
||||
return self.__last_knspace
|
||||
|
||||
# we only get here if we never accessed our knspace
|
||||
return self.__set_parent_knspace()
|
||||
|
||||
def _set_knspace(self, value):
|
||||
if value is self._knspace:
|
||||
return
|
||||
|
||||
knspace = self._knspace or self.__last_knspace
|
||||
name = self.knsname
|
||||
if name and knspace and getattr(knspace, name) == self:
|
||||
setattr(knspace, name, None) # reset old namespace
|
||||
|
||||
if value == 'fork':
|
||||
if not knspace:
|
||||
knspace = self.knspace # get parents in case we haven't before
|
||||
if knspace:
|
||||
value = knspace.fork()
|
||||
else:
|
||||
raise ValueError('Cannot fork with no namespace')
|
||||
|
||||
for obj, prop_name, uid in self.__callbacks or []:
|
||||
obj.unbind_uid(prop_name, uid)
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
if name:
|
||||
if value is None: # if None, first update the recursive knspace
|
||||
knspace = self.__set_parent_knspace()
|
||||
if knspace:
|
||||
setattr(knspace, name, self)
|
||||
self._knspace = None # cause a kv trigger
|
||||
else:
|
||||
setattr(value, name, self)
|
||||
knspace = self._knspace = value
|
||||
|
||||
if not knspace:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
else:
|
||||
if value is None:
|
||||
self.__set_parent_knspace() # update before trigger below
|
||||
self._knspace = value
|
||||
|
||||
knspace = AliasProperty(
|
||||
_get_knspace, _set_knspace, bind=('_knspace', ), cache=False,
|
||||
rebind=True, allownone=True)
|
||||
'''The namespace instance, :class:`KNSpace`, associated with this widget.
|
||||
The :attr:`knspace` namespace stores this widget when naming this widget
|
||||
with :attr:`knsname`.
|
||||
|
||||
If the namespace has been set with a :class:`KNSpace` instance, e.g. with
|
||||
`self.knspace = KNSpace()`, then that instance is returned (setting with
|
||||
`None` doesn't count). Otherwise, if :attr:`knspace_key` is not None, we
|
||||
look for a namespace to use in the object that is stored in the property
|
||||
named :attr:`knspace_key`, of this instance. I.e.
|
||||
`object = getattr(self, self.knspace_key)`.
|
||||
|
||||
If that object has a knspace property, then we return its value. Otherwise,
|
||||
we go further up, e.g. with `getattr(object, self.knspace_key)` and look
|
||||
for its `knspace` property.
|
||||
|
||||
Finally, if we reach a value of `None`, or :attr:`knspace_key` was `None`,
|
||||
the default :attr:`~kivy.uix.behaviors.knspace.knspace` namespace is
|
||||
returned.
|
||||
|
||||
If :attr:`knspace` is set to the string `'fork'`, the current namespace
|
||||
in :attr:`knspace` will be forked with :meth:`KNSpace.fork` and the
|
||||
resulting namespace will be assigned to this instance's :attr:`knspace`.
|
||||
See the module examples for a motivating example.
|
||||
|
||||
Both `rebind` and `allownone` are `True`.
|
||||
'''
|
||||
|
||||
knspace_key = StringProperty('parent', allownone=True)
|
||||
'''The name of the property of this instance, to use to search upwards for
|
||||
a namespace to use by this instance. Defaults to `'parent'` so that we'll
|
||||
search the parent tree. See :attr:`knspace`.
|
||||
|
||||
When `None`, we won't search the parent tree for the namespace.
|
||||
`allownone` is `True`.
|
||||
'''
|
||||
|
||||
def _get_knsname(self):
|
||||
return self._knsname
|
||||
|
||||
def _set_knsname(self, value):
|
||||
old_name = self._knsname
|
||||
knspace = self.knspace
|
||||
if old_name and knspace and getattr(knspace, old_name) == self:
|
||||
setattr(knspace, old_name, None)
|
||||
|
||||
self._knsname = value
|
||||
if value:
|
||||
if knspace:
|
||||
setattr(knspace, value, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(value))
|
||||
|
||||
knsname = AliasProperty(
|
||||
_get_knsname, _set_knsname, bind=('_knsname', ), cache=False)
|
||||
'''The name given to this instance. If named, the name will be added to the
|
||||
associated :attr:`knspace` namespace, which will then point to the
|
||||
`proxy_ref` of this instance.
|
||||
|
||||
When named, one can access this object by e.g. self.knspace.name, where
|
||||
`name` is the given name of this instance. See :attr:`knspace` and the
|
||||
module description for more details.
|
||||
'''
|
||||
|
||||
|
||||
knspace = register_context('knspace', KNSpace)
|
||||
'''The default :class:`KNSpace` namespace. See :attr:`KNSpaceBehavior.knspace`
|
||||
for more details.
|
||||
'''
|
||||
156
kivy/uix/behaviors/togglebutton.py
Normal file
156
kivy/uix/behaviors/togglebutton.py
Normal file
@@ -0,0 +1,156 @@
|
||||
'''
|
||||
ToggleButton Behavior
|
||||
=====================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.togglebutton.ToggleButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.togglebutton.ToggleButton` behavior. You can combine this
|
||||
class with other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative togglebuttons that preserve Kivy togglebutton behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds togglebutton behavior to an image to make a checkbox
|
||||
that behaves like a togglebutton::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ToggleButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ToggleButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_state(self, widget, value):
|
||||
if value == 'down':
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
else:
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
'''
|
||||
|
||||
__all__ = ('ToggleButtonBehavior', )
|
||||
|
||||
from kivy.properties import ObjectProperty, BooleanProperty
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from weakref import ref
|
||||
|
||||
|
||||
class ToggleButtonBehavior(ButtonBehavior):
|
||||
'''This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:mod:`~kivy.uix.togglebutton` behavior. Please see the
|
||||
:mod:`togglebutton behaviors module <kivy.uix.behaviors.togglebutton>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
__groups = {}
|
||||
|
||||
group = ObjectProperty(None, allownone=True)
|
||||
'''Group of the button. If `None`, no group will be used (the button will be
|
||||
independent). If specified, :attr:`group` must be a hashable object, like
|
||||
a string. Only one button in a group can be in a 'down' state.
|
||||
|
||||
:attr:`group` is a :class:`~kivy.properties.ObjectProperty` and defaults to
|
||||
`None`.
|
||||
'''
|
||||
|
||||
allow_no_selection = BooleanProperty(True)
|
||||
'''This specifies whether the widgets in a group allow no selection i.e.
|
||||
everything to be deselected.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`allow_no_selection` is a :class:`BooleanProperty` and defaults to
|
||||
`True`
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._previous_group = None
|
||||
super(ToggleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_group(self, *largs):
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if self._previous_group:
|
||||
group = groups[self._previous_group]
|
||||
for item in group[:]:
|
||||
if item() is self:
|
||||
group.remove(item)
|
||||
break
|
||||
group = self._previous_group = self.group
|
||||
if group not in groups:
|
||||
groups[group] = []
|
||||
r = ref(self, ToggleButtonBehavior._clear_groups)
|
||||
groups[group].append(r)
|
||||
|
||||
def _release_group(self, current):
|
||||
if self.group is None:
|
||||
return
|
||||
group = self.__groups[self.group]
|
||||
for item in group[:]:
|
||||
widget = item()
|
||||
if widget is None:
|
||||
group.remove(item)
|
||||
if widget is current:
|
||||
continue
|
||||
widget.state = 'normal'
|
||||
|
||||
def _do_press(self):
|
||||
if (not self.allow_no_selection and
|
||||
self.group and self.state == 'down'):
|
||||
return
|
||||
|
||||
self._release_group(self)
|
||||
self.state = 'normal' if self.state == 'down' else 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _clear_groups(wk):
|
||||
# auto flush the element when the weak reference have been deleted
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
for group in list(groups.values()):
|
||||
if wk in group:
|
||||
group.remove(wk)
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def get_widgets(groupname):
|
||||
'''Return a list of the widgets contained in a specific group. If the
|
||||
group doesn't exist, an empty list will be returned.
|
||||
|
||||
.. note::
|
||||
|
||||
Always release the result of this method! Holding a reference to
|
||||
any of these widgets can prevent them from being garbage collected.
|
||||
If in doubt, do::
|
||||
|
||||
l = ToggleButtonBehavior.get_widgets('mygroup')
|
||||
# do your job
|
||||
del l
|
||||
|
||||
.. warning::
|
||||
|
||||
It's possible that some widgets that you have previously
|
||||
deleted are still in the list. The garbage collector might need
|
||||
to release other objects before flushing them.
|
||||
'''
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if groupname not in groups:
|
||||
return []
|
||||
return [x() for x in groups[groupname] if x()][:]
|
||||
318
kivy/uix/behaviors/touchripple.py
Normal file
318
kivy/uix/behaviors/touchripple.py
Normal file
@@ -0,0 +1,318 @@
|
||||
'''
|
||||
Touch Ripple
|
||||
============
|
||||
|
||||
.. versionadded:: 1.10.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
This module contains `mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes
|
||||
to add a touch ripple visual effect known from `Google Material Design
|
||||
<https://en.wikipedia.org/wiki/Material_Design>_` to widgets.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleBehavior` provides
|
||||
rendering the ripple animation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleButtonBehavior`
|
||||
basically provides the same functionality as
|
||||
:class:`~kivy.uix.behaviors.button.ButtonBehavior` but rendering the ripple
|
||||
animation instead of default press/release visualization.
|
||||
'''
|
||||
from kivy.animation import Animation
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import CanvasBase, Color, Ellipse, ScissorPush, ScissorPop
|
||||
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, \
|
||||
ObjectProperty, StringProperty
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TouchRippleBehavior',
|
||||
'TouchRippleButtonBehavior'
|
||||
)
|
||||
|
||||
|
||||
class TouchRippleBehavior(object):
|
||||
'''Touch ripple behavior.
|
||||
|
||||
Supposed to be used as mixin on widget classes.
|
||||
|
||||
Ripple behavior does not trigger automatically, concrete implementation
|
||||
needs to call :func:`ripple_show` respective :func:`ripple_fade` manually.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here we create a Label which renders the touch ripple animation on
|
||||
interaction::
|
||||
|
||||
class RippleLabel(TouchRippleBehavior, Label):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RippleLabel, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
collide_point = self.collide_point(touch.x, touch.y)
|
||||
if collide_point:
|
||||
touch.grab(self)
|
||||
self.ripple_show(touch)
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is self:
|
||||
touch.ungrab(self)
|
||||
self.ripple_fade()
|
||||
return True
|
||||
return False
|
||||
'''
|
||||
|
||||
ripple_rad_default = NumericProperty(10)
|
||||
'''Default radius the animation starts from.
|
||||
|
||||
:attr:`ripple_rad_default` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `10`.
|
||||
'''
|
||||
|
||||
ripple_duration_in = NumericProperty(.5)
|
||||
'''Animation duration taken to show the overlay.
|
||||
|
||||
:attr:`ripple_duration_in` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_duration_out = NumericProperty(.2)
|
||||
'''Animation duration taken to fade the overlay.
|
||||
|
||||
:attr:`ripple_duration_out` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.2`.
|
||||
'''
|
||||
|
||||
ripple_fade_from_alpha = NumericProperty(.5)
|
||||
'''Alpha channel for ripple color the animation starts with.
|
||||
|
||||
:attr:`ripple_fade_from_alpha` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_fade_to_alpha = NumericProperty(.8)
|
||||
'''Alpha channel for ripple color the animation targets to.
|
||||
|
||||
:attr:`ripple_fade_to_alpha` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.8`.
|
||||
'''
|
||||
|
||||
ripple_scale = NumericProperty(2.)
|
||||
'''Max scale of the animation overlay calculated from max(width/height) of
|
||||
the decorated widget.
|
||||
|
||||
:attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `2.0`.
|
||||
'''
|
||||
|
||||
ripple_func_in = StringProperty('in_cubic')
|
||||
'''Animation callback for showing the overlay.
|
||||
|
||||
:attr:`ripple_func_in` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `in_cubic`.
|
||||
'''
|
||||
|
||||
ripple_func_out = StringProperty('out_quad')
|
||||
'''Animation callback for hiding the overlay.
|
||||
|
||||
:attr:`ripple_func_out` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `out_quad`.
|
||||
'''
|
||||
|
||||
ripple_rad = NumericProperty(10)
|
||||
ripple_pos = ListProperty([0, 0])
|
||||
ripple_color = ListProperty((1., 1., 1., .5))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(TouchRippleBehavior, self).__init__(**kwargs)
|
||||
self.ripple_pane = CanvasBase()
|
||||
self.canvas.add(self.ripple_pane)
|
||||
self.bind(
|
||||
ripple_color=self._ripple_set_color,
|
||||
ripple_pos=self._ripple_set_ellipse,
|
||||
ripple_rad=self._ripple_set_ellipse
|
||||
)
|
||||
self.ripple_ellipse = None
|
||||
self.ripple_col_instruction = None
|
||||
|
||||
def ripple_show(self, touch):
|
||||
'''Begin ripple animation on current widget.
|
||||
|
||||
Expects touch event as argument.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
self._ripple_reset_pane()
|
||||
x, y = self.to_window(*self.pos)
|
||||
width, height = self.size
|
||||
if isinstance(self, RelativeLayout):
|
||||
self.ripple_pos = ripple_pos = (touch.x - x, touch.y - y)
|
||||
else:
|
||||
self.ripple_pos = ripple_pos = (touch.x, touch.y)
|
||||
rc = self.ripple_color
|
||||
ripple_rad = self.ripple_rad
|
||||
self.ripple_color = [rc[0], rc[1], rc[2], self.ripple_fade_from_alpha]
|
||||
with self.ripple_pane:
|
||||
ScissorPush(
|
||||
x=int(round(x)),
|
||||
y=int(round(y)),
|
||||
width=int(round(width)),
|
||||
height=int(round(height))
|
||||
)
|
||||
self.ripple_col_instruction = Color(rgba=self.ripple_color)
|
||||
self.ripple_ellipse = Ellipse(
|
||||
size=(ripple_rad, ripple_rad),
|
||||
pos=(
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
)
|
||||
ScissorPop()
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
t=self.ripple_func_in,
|
||||
ripple_color=[rc[0], rc[1], rc[2], self.ripple_fade_to_alpha],
|
||||
duration=self.ripple_duration_in
|
||||
)
|
||||
anim.start(self)
|
||||
|
||||
def ripple_fade(self):
|
||||
'''Finish ripple animation on current widget.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
width, height = self.size
|
||||
rc = self.ripple_color
|
||||
duration = self.ripple_duration_out
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
ripple_color=[rc[0], rc[1], rc[2], 0.],
|
||||
t=self.ripple_func_out,
|
||||
duration=duration
|
||||
)
|
||||
anim.bind(on_complete=self._ripple_anim_complete)
|
||||
anim.start(self)
|
||||
|
||||
def _ripple_set_ellipse(self, instance, value):
|
||||
ellipse = self.ripple_ellipse
|
||||
if not ellipse:
|
||||
return
|
||||
ripple_pos = self.ripple_pos
|
||||
ripple_rad = self.ripple_rad
|
||||
ellipse.size = (ripple_rad, ripple_rad)
|
||||
ellipse.pos = (
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
|
||||
def _ripple_set_color(self, instance, value):
|
||||
if not self.ripple_col_instruction:
|
||||
return
|
||||
self.ripple_col_instruction.rgba = value
|
||||
|
||||
def _ripple_anim_complete(self, anim, instance):
|
||||
self._ripple_reset_pane()
|
||||
|
||||
def _ripple_reset_pane(self):
|
||||
self.ripple_rad = self.ripple_rad_default
|
||||
self.ripple_pane.clear()
|
||||
|
||||
|
||||
class TouchRippleButtonBehavior(TouchRippleBehavior):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
a similar behavior to :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
but provides touch ripple animation instead of button pressed/released as
|
||||
visual effect.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
super(TouchRippleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.ripple_show(touch)
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(TouchRippleButtonBehavior, self).on_touch_up(touch)
|
||||
assert(self in touch.ud)
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
if self.disabled:
|
||||
return
|
||||
self.ripple_fade()
|
||||
if not self.always_release and not self.collide_point(*touch.pos):
|
||||
return
|
||||
|
||||
# defer on_release until ripple_fade has completed
|
||||
def defer_release(dt):
|
||||
self.dispatch('on_release')
|
||||
Clock.schedule_once(defer_release, self.ripple_duration_out)
|
||||
return True
|
||||
|
||||
def on_disabled(self, instance, value):
|
||||
# ensure ripple animation completes if disabled gets set to True
|
||||
if value:
|
||||
self.ripple_fade()
|
||||
return super(TouchRippleButtonBehavior, self).on_disabled(
|
||||
instance, value)
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
Reference in New Issue
Block a user