Stav 23.06.2026
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
'''
|
||||
Widgets
|
||||
=======
|
||||
|
||||
Widgets are elements of a graphical user interface that form part of the
|
||||
`User Experience <http://en.wikipedia.org/wiki/User_experience>`_.
|
||||
The `kivy.uix` module contains classes for creating and managing Widgets.
|
||||
Please refer to the :doc:`api-kivy.uix.widget` documentation for further
|
||||
information.
|
||||
|
||||
Kivy widgets can be categorized as follows:
|
||||
|
||||
- **UX widgets**: Classical user interface widgets, ready to be assembled to
|
||||
create more complex widgets.
|
||||
|
||||
:doc:`api-kivy.uix.label`, :doc:`api-kivy.uix.button`,
|
||||
:doc:`api-kivy.uix.checkbox`,
|
||||
:doc:`api-kivy.uix.image`, :doc:`api-kivy.uix.slider`,
|
||||
:doc:`api-kivy.uix.progressbar`, :doc:`api-kivy.uix.textinput`,
|
||||
:doc:`api-kivy.uix.togglebutton`, :doc:`api-kivy.uix.switch`,
|
||||
:doc:`api-kivy.uix.video`
|
||||
|
||||
- **Layouts**: A layout widget does no rendering but just acts as a trigger
|
||||
that arranges its children in a specific way. Read more on
|
||||
:doc:`Layouts here <api-kivy.uix.layout>`.
|
||||
|
||||
:doc:`api-kivy.uix.anchorlayout`, :doc:`api-kivy.uix.boxlayout`,
|
||||
:doc:`api-kivy.uix.floatlayout`,
|
||||
:doc:`api-kivy.uix.gridlayout`, :doc:`api-kivy.uix.pagelayout`,
|
||||
:doc:`api-kivy.uix.relativelayout`, :doc:`api-kivy.uix.scatterlayout`,
|
||||
:doc:`api-kivy.uix.stacklayout`
|
||||
|
||||
- **Complex UX widgets**: Non-atomic widgets that are the result of
|
||||
combining multiple classic widgets.
|
||||
We call them complex because their assembly and usage are not as
|
||||
generic as the classical widgets.
|
||||
|
||||
:doc:`api-kivy.uix.bubble`, :doc:`api-kivy.uix.dropdown`,
|
||||
:doc:`api-kivy.uix.filechooser`, :doc:`api-kivy.uix.popup`,
|
||||
:doc:`api-kivy.uix.spinner`,
|
||||
:doc:`api-kivy.uix.recycleview`,
|
||||
:doc:`api-kivy.uix.tabbedpanel`, :doc:`api-kivy.uix.videoplayer`,
|
||||
:doc:`api-kivy.uix.vkeyboard`,
|
||||
|
||||
- **Behaviors widgets**: These widgets do no rendering but act on the
|
||||
graphics instructions or interaction (touch) behavior of their children.
|
||||
|
||||
:doc:`api-kivy.uix.scatter`, :doc:`api-kivy.uix.stencilview`
|
||||
|
||||
- **Screen manager**: Manages screens and transitions when switching
|
||||
from one to another.
|
||||
|
||||
:doc:`api-kivy.uix.screenmanager`
|
||||
|
||||
----
|
||||
'''
|
||||
@@ -0,0 +1,484 @@
|
||||
'''
|
||||
Accordion
|
||||
=========
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
|
||||
.. image:: images/accordion.jpg
|
||||
:align: right
|
||||
|
||||
The Accordion widget is a form of menu where the options are stacked either
|
||||
vertically or horizontally and the item in focus (when touched) opens up to
|
||||
display its content.
|
||||
|
||||
The :class:`Accordion` should contain one or many :class:`AccordionItem`
|
||||
instances, each of which should contain one root content widget. You'll end up
|
||||
with a Tree something like this:
|
||||
|
||||
- Accordion
|
||||
|
||||
- AccordionItem
|
||||
|
||||
- YourContent
|
||||
|
||||
- AccordionItem
|
||||
|
||||
- BoxLayout
|
||||
|
||||
- Another user content 1
|
||||
|
||||
- Another user content 2
|
||||
|
||||
- AccordionItem
|
||||
|
||||
- Another user content
|
||||
|
||||
|
||||
The current implementation divides the :class:`AccordionItem` into two parts:
|
||||
|
||||
#. One container for the title bar
|
||||
#. One container for the content
|
||||
|
||||
The title bar is made from a Kv template. We'll see how to create a new
|
||||
template to customize the design of the title bar.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you see message like::
|
||||
|
||||
[WARNING] [Accordion] not have enough space for displaying all children
|
||||
[WARNING] [Accordion] need 440px, got 100px
|
||||
[WARNING] [Accordion] layout aborted.
|
||||
|
||||
That means you have too many children and there is no more space to
|
||||
display the content. This is "normal" and nothing will be done. Try to
|
||||
increase the space for the accordion or reduce the number of children. You
|
||||
can also reduce the :attr:`Accordion.min_space`.
|
||||
|
||||
Simple example
|
||||
--------------
|
||||
|
||||
.. include:: ../../examples/widgets/accordion_1.py
|
||||
:literal:
|
||||
|
||||
Customize the accordion
|
||||
-----------------------
|
||||
|
||||
You can increase the default size of the title bar::
|
||||
|
||||
root = Accordion(min_space=60)
|
||||
|
||||
Or change the orientation to vertical::
|
||||
|
||||
root = Accordion(orientation='vertical')
|
||||
|
||||
The AccordionItem is more configurable and you can set your own title
|
||||
background when the item is collapsed or opened::
|
||||
|
||||
item = AccordionItem(background_normal='image_when_collapsed.png',
|
||||
background_selected='image_when_selected.png')
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Accordion', 'AccordionItem', 'AccordionException')
|
||||
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.clock import Clock
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import (ObjectProperty, StringProperty,
|
||||
BooleanProperty, NumericProperty,
|
||||
ListProperty, OptionProperty, DictProperty)
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
class AccordionException(Exception):
|
||||
'''AccordionException class.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class AccordionItem(FloatLayout):
|
||||
'''AccordionItem class that must be used in conjunction with the
|
||||
:class:`Accordion` class. See the module documentation for more
|
||||
information.
|
||||
'''
|
||||
|
||||
title = StringProperty('')
|
||||
'''Title string of the item. The title might be used in conjunction with the
|
||||
`AccordionItemTitle` template. If you are using a custom template, you can
|
||||
use that property as a text entry, or not. By default, it's used for the
|
||||
title text. See title_template and the example below.
|
||||
|
||||
:attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults
|
||||
to ''.
|
||||
'''
|
||||
|
||||
title_template = StringProperty('AccordionItemTitle')
|
||||
'''Template to use for creating the title part of the accordion item. The
|
||||
default template is a simple Label, not customizable (except the text) that
|
||||
supports vertical and horizontal orientation and different backgrounds for
|
||||
collapse and selected mode.
|
||||
|
||||
It's better to create and use your own template if the default template
|
||||
does not suffice.
|
||||
|
||||
:attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to
|
||||
'AccordionItemTitle'. The current default template lives in the
|
||||
`kivy/data/style.kv` file.
|
||||
|
||||
Here is the code if you want to build your own template::
|
||||
|
||||
[AccordionItemTitle@Label]:
|
||||
text: ctx.title
|
||||
canvas.before:
|
||||
Color:
|
||||
rgb: 1, 1, 1
|
||||
BorderImage:
|
||||
source:
|
||||
ctx.item.background_normal \
|
||||
if ctx.item.collapse \
|
||||
else ctx.item.background_selected
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
PushMatrix
|
||||
Translate:
|
||||
xy: self.center_x, self.center_y
|
||||
Rotate:
|
||||
angle: 90 if ctx.item.orientation == 'horizontal' else 0
|
||||
axis: 0, 0, 1
|
||||
Translate:
|
||||
xy: -self.center_x, -self.center_y
|
||||
canvas.after:
|
||||
PopMatrix
|
||||
|
||||
|
||||
'''
|
||||
|
||||
title_args = DictProperty({})
|
||||
'''Default arguments that will be passed to the
|
||||
:meth:`kivy.lang.Builder.template` method.
|
||||
|
||||
:attr:`title_args` is a :class:`~kivy.properties.DictProperty` and defaults
|
||||
to {}.
|
||||
'''
|
||||
|
||||
collapse = BooleanProperty(True)
|
||||
'''Boolean to indicate if the current item is collapsed or not.
|
||||
|
||||
:attr:`collapse` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
collapse_alpha = NumericProperty(1.)
|
||||
'''Value between 0 and 1 to indicate how much the item is collapsed (1) or
|
||||
whether it is selected (0). It's mostly used for animation.
|
||||
|
||||
:attr:`collapse_alpha` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
accordion = ObjectProperty(None)
|
||||
'''Instance of the :class:`Accordion` that the item belongs to.
|
||||
|
||||
:attr:`accordion` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button')
|
||||
'''Background image of the accordion item used for the default graphical
|
||||
representation when the item is collapsed.
|
||||
|
||||
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/button'.
|
||||
'''
|
||||
|
||||
background_disabled_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_disabled')
|
||||
'''Background image of the accordion item used for the default graphical
|
||||
representation when the item is collapsed and disabled.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background__disabled_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/button_disabled'.
|
||||
'''
|
||||
|
||||
background_selected = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_pressed')
|
||||
'''Background image of the accordion item used for the default graphical
|
||||
representation when the item is selected (not collapsed).
|
||||
|
||||
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/button_pressed'.
|
||||
'''
|
||||
|
||||
background_disabled_selected = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_disabled_pressed')
|
||||
'''Background image of the accordion item used for the default graphical
|
||||
representation when the item is selected (not collapsed) and disabled.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled_selected` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/button_disabled_pressed'.
|
||||
'''
|
||||
|
||||
orientation = OptionProperty('vertical', options=(
|
||||
'horizontal', 'vertical'))
|
||||
'''Link to the :attr:`Accordion.orientation` property.
|
||||
'''
|
||||
|
||||
min_space = NumericProperty('44dp')
|
||||
'''Link to the :attr:`Accordion.min_space` property.
|
||||
'''
|
||||
|
||||
content_size = ListProperty([100, 100])
|
||||
'''(internal) Set by the :class:`Accordion` to the size allocated for the
|
||||
content.
|
||||
'''
|
||||
|
||||
container = ObjectProperty(None)
|
||||
'''(internal) Property that will be set to the container of children inside
|
||||
the AccordionItem representation.
|
||||
'''
|
||||
|
||||
container_title = ObjectProperty(None)
|
||||
'''(internal) Property that will be set to the container of title inside
|
||||
the AccordionItem representation.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._trigger_title = Clock.create_trigger(self._update_title, -1)
|
||||
self._anim_collapse = None
|
||||
super(AccordionItem, self).__init__(**kwargs)
|
||||
trigger_title = self._trigger_title
|
||||
fbind = self.fbind
|
||||
fbind('title', trigger_title)
|
||||
fbind('title_template', trigger_title)
|
||||
fbind('title_args', trigger_title)
|
||||
trigger_title()
|
||||
|
||||
def add_widget(self, *args, **kwargs):
|
||||
if self.container is None:
|
||||
super(AccordionItem, self).add_widget(*args, **kwargs)
|
||||
return
|
||||
self.container.add_widget(*args, **kwargs)
|
||||
|
||||
def remove_widget(self, *args, **kwargs):
|
||||
if self.container:
|
||||
self.container.remove_widget(*args, **kwargs)
|
||||
return
|
||||
super(AccordionItem, self).remove_widget(*args, **kwargs)
|
||||
|
||||
def on_collapse(self, instance, value):
|
||||
accordion = self.accordion
|
||||
if accordion is None:
|
||||
return
|
||||
if not value:
|
||||
self.accordion.select(self)
|
||||
collapse_alpha = float(value)
|
||||
if self._anim_collapse:
|
||||
self._anim_collapse.stop(self)
|
||||
self._anim_collapse = None
|
||||
if self.collapse_alpha != collapse_alpha:
|
||||
self._anim_collapse = Animation(
|
||||
collapse_alpha=collapse_alpha,
|
||||
t=accordion.anim_func,
|
||||
d=accordion.anim_duration)
|
||||
self._anim_collapse.start(self)
|
||||
|
||||
def on_collapse_alpha(self, instance, value):
|
||||
self.accordion._trigger_layout()
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
if self.disabled:
|
||||
return True
|
||||
if self.collapse:
|
||||
self.collapse = False
|
||||
return True
|
||||
else:
|
||||
return super(AccordionItem, self).on_touch_down(touch)
|
||||
|
||||
def _update_title(self, dt):
|
||||
if not self.container_title:
|
||||
self._trigger_title()
|
||||
return
|
||||
c = self.container_title
|
||||
c.clear_widgets()
|
||||
instance = Builder.template(self.title_template,
|
||||
title=self.title,
|
||||
item=self,
|
||||
**self.title_args)
|
||||
c.add_widget(instance)
|
||||
|
||||
|
||||
class Accordion(Widget):
|
||||
'''Accordion class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
orientation = OptionProperty('horizontal', options=(
|
||||
'horizontal', 'vertical'))
|
||||
'''Orientation of the layout.
|
||||
|
||||
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty`
|
||||
and defaults to 'horizontal'. Can take a value of 'vertical' or
|
||||
'horizontal'.
|
||||
|
||||
'''
|
||||
|
||||
anim_duration = NumericProperty(.25)
|
||||
'''Duration of the animation in seconds when a new accordion item is
|
||||
selected.
|
||||
|
||||
:attr:`anim_duration` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to .25 (250ms).
|
||||
'''
|
||||
|
||||
anim_func = ObjectProperty('out_expo')
|
||||
'''Easing function to use for the animation. Check
|
||||
:class:`kivy.animation.AnimationTransition` for more information about
|
||||
available animation functions.
|
||||
|
||||
:attr:`anim_func` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to 'out_expo'. You can set a string or a function to use as an
|
||||
easing function.
|
||||
'''
|
||||
|
||||
min_space = NumericProperty('44dp')
|
||||
'''Minimum space to use for the title of each item. This value is
|
||||
automatically set for each child every time the layout event occurs.
|
||||
|
||||
:attr:`min_space` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 44 (px).
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Accordion, self).__init__(**kwargs)
|
||||
update = self._trigger_layout = \
|
||||
Clock.create_trigger(self._do_layout, -1)
|
||||
fbind = self.fbind
|
||||
fbind('orientation', update)
|
||||
fbind('children', update)
|
||||
fbind('size', update)
|
||||
fbind('pos', update)
|
||||
fbind('min_space', update)
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
if not isinstance(widget, AccordionItem):
|
||||
raise AccordionException('Accordion accept only AccordionItem')
|
||||
widget.accordion = self
|
||||
super(Accordion, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
def select(self, instance):
|
||||
if instance not in self.children:
|
||||
raise AccordionException(
|
||||
'Accordion: instance not found in children')
|
||||
for widget in self.children:
|
||||
widget.collapse = widget is not instance
|
||||
self._trigger_layout()
|
||||
|
||||
def _do_layout(self, dt):
|
||||
children = self.children
|
||||
if children:
|
||||
all_collapsed = all(x.collapse for x in children)
|
||||
else:
|
||||
all_collapsed = False
|
||||
|
||||
if all_collapsed:
|
||||
children[0].collapse = False
|
||||
|
||||
orientation = self.orientation
|
||||
min_space = self.min_space
|
||||
min_space_total = len(children) * self.min_space
|
||||
w, h = self.size
|
||||
x, y = self.pos
|
||||
if orientation == 'horizontal':
|
||||
display_space = self.width - min_space_total
|
||||
else:
|
||||
display_space = self.height - min_space_total
|
||||
|
||||
if display_space <= 0:
|
||||
Logger.warning('Accordion: not enough space '
|
||||
'for displaying all children')
|
||||
Logger.warning('Accordion: need %dpx, got %dpx' % (
|
||||
min_space_total, min_space_total + display_space))
|
||||
Logger.warning('Accordion: layout aborted.')
|
||||
return
|
||||
|
||||
if orientation == 'horizontal':
|
||||
children = reversed(children)
|
||||
|
||||
for child in children:
|
||||
child_space = min_space
|
||||
child_space += display_space * (1 - child.collapse_alpha)
|
||||
child._min_space = min_space
|
||||
child.x = x
|
||||
child.y = y
|
||||
child.orientation = self.orientation
|
||||
if orientation == 'horizontal':
|
||||
child.content_size = display_space, h
|
||||
child.width = child_space
|
||||
child.height = h
|
||||
x += child_space
|
||||
else:
|
||||
child.content_size = w, display_space
|
||||
child.width = w
|
||||
child.height = child_space
|
||||
y += child_space
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.label import Label
|
||||
|
||||
acc = Accordion()
|
||||
for x in range(10):
|
||||
item = AccordionItem(title='Title %d' % x)
|
||||
if x == 0:
|
||||
item.add_widget(Button(text='Content %d' % x))
|
||||
elif x == 1:
|
||||
z = BoxLayout(orientation='vertical')
|
||||
z.add_widget(Button(text=str(x), size_hint_y=None, height=35))
|
||||
z.add_widget(Label(text='Content %d' % x))
|
||||
item.add_widget(z)
|
||||
else:
|
||||
item.add_widget(Label(text='This is a big content\n' * 20))
|
||||
acc.add_widget(item)
|
||||
|
||||
def toggle_layout(*l):
|
||||
o = acc.orientation
|
||||
acc.orientation = 'vertical' if o == 'horizontal' else 'horizontal'
|
||||
btn = Button(text='Toggle layout')
|
||||
btn.bind(on_release=toggle_layout)
|
||||
|
||||
def select_2nd_item(*l):
|
||||
acc.select(acc.children[-2])
|
||||
btn2 = Button(text='Select 2nd item')
|
||||
btn2.bind(on_release=select_2nd_item)
|
||||
|
||||
from kivy.uix.slider import Slider
|
||||
slider = Slider()
|
||||
|
||||
def update_min_space(instance, value):
|
||||
acc.min_space = value
|
||||
|
||||
slider.bind(value=update_min_space)
|
||||
|
||||
root = BoxLayout(spacing=20, padding=20)
|
||||
controls = BoxLayout(orientation='vertical', size_hint_x=.3)
|
||||
controls.add_widget(btn)
|
||||
controls.add_widget(btn2)
|
||||
controls.add_widget(slider)
|
||||
root.add_widget(controls)
|
||||
root.add_widget(acc)
|
||||
runTouchApp(root)
|
||||
@@ -0,0 +1,934 @@
|
||||
'''
|
||||
Action Bar
|
||||
==========
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. image:: images/actionbar.png
|
||||
:align: right
|
||||
|
||||
The ActionBar widget is like Android's `ActionBar
|
||||
<http://developer.android.com/guide/topics/ui/actionbar.html>`_, where items
|
||||
are stacked horizontally. When the area becomes to small, widgets are moved
|
||||
into the :class:`ActionOverflow` area.
|
||||
|
||||
An :class:`ActionBar` must contain an :class:`ActionView` with various
|
||||
:class:`ContextualActionViews <kivy.uix.actionbar.ContextualActionView>`.
|
||||
An :class:`ActionView` must contain a child :class:`ActionPrevious` which may
|
||||
have title, app_icon and previous_icon properties. :class:`ActionView` children
|
||||
must be
|
||||
subclasses of :class:`ActionItems <ActionItem>`. Some predefined ones include
|
||||
an :class:`ActionButton`, an :class:`ActionToggleButton`, an
|
||||
:class:`ActionCheck`, an :class:`ActionSeparator` and an :class:`ActionGroup`.
|
||||
|
||||
An :class:`ActionGroup` is used to display :class:`ActionItems <ActionItem>`
|
||||
in a group. An :class:`ActionView` will always display an :class:`ActionGroup`
|
||||
after other :class:`ActionItems <ActionItem>`. An :class:`ActionView` contains
|
||||
an :class:`ActionOverflow`, but this is only made visible when required i.e.
|
||||
the available area is too small to fit all the widgets. A
|
||||
:class:`ContextualActionView` is a subclass of an:class:`ActionView`.
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
:class:`ActionGroup` core rewritten from :class:`Spinner` to pure
|
||||
:class:`DropDown`
|
||||
'''
|
||||
|
||||
__all__ = ('ActionBarException', 'ActionItem', 'ActionButton',
|
||||
'ActionToggleButton', 'ActionCheck', 'ActionSeparator',
|
||||
'ActionDropDown', 'ActionGroup', 'ActionOverflow',
|
||||
'ActionView', 'ContextualActionView', 'ActionPrevious',
|
||||
'ActionBar')
|
||||
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.uix.checkbox import CheckBox
|
||||
from kivy.uix.spinner import Spinner
|
||||
from kivy.uix.label import Label
|
||||
from kivy.config import Config
|
||||
from kivy.properties import ObjectProperty, NumericProperty, BooleanProperty, \
|
||||
StringProperty, ListProperty, OptionProperty, AliasProperty, ColorProperty
|
||||
from kivy.metrics import sp
|
||||
from kivy.lang import Builder
|
||||
from functools import partial
|
||||
|
||||
|
||||
window_icon = ''
|
||||
if Config:
|
||||
window_icon = Config.get('kivy', 'window_icon')
|
||||
|
||||
|
||||
class ActionBarException(Exception):
|
||||
'''
|
||||
ActionBarException class
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionItem(object):
|
||||
'''
|
||||
ActionItem class, an abstract class for all ActionBar widgets. To create a
|
||||
custom widget for an ActionBar, inherit from this class. See module
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
minimum_width = NumericProperty('90sp')
|
||||
'''
|
||||
Minimum Width required by an ActionItem.
|
||||
|
||||
:attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to '90sp'.
|
||||
'''
|
||||
|
||||
def get_pack_width(self):
|
||||
return max(self.minimum_width, self.width)
|
||||
|
||||
pack_width = AliasProperty(get_pack_width,
|
||||
bind=('minimum_width', 'width'),
|
||||
cache=True)
|
||||
'''
|
||||
(read-only) The actual width to use when packing the items. Equal to the
|
||||
greater of minimum_width and width.
|
||||
|
||||
:attr:`pack_width` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
important = BooleanProperty(False)
|
||||
'''
|
||||
Determines if an ActionItem is important or not. If an item is important
|
||||
and space is limited, this item will be displayed in preference to others.
|
||||
|
||||
:attr:`important` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
inside_group = BooleanProperty(False)
|
||||
'''
|
||||
(internal) Determines if an ActionItem is displayed inside an
|
||||
ActionGroup or not.
|
||||
|
||||
:attr:`inside_group` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_item')
|
||||
'''
|
||||
Background image of the ActionItem used for the default graphical
|
||||
representation when the ActionItem is not pressed.
|
||||
|
||||
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_item'.
|
||||
'''
|
||||
|
||||
background_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_item_down')
|
||||
'''
|
||||
Background image of the ActionItem used for the default graphical
|
||||
representation when an ActionItem is pressed.
|
||||
|
||||
:attr:`background_down` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_item_down'.
|
||||
'''
|
||||
|
||||
mipmap = BooleanProperty(True)
|
||||
'''
|
||||
Defines whether the image/icon dispayed on top of the button uses a
|
||||
mipmap or not.
|
||||
|
||||
:attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `True`.
|
||||
'''
|
||||
|
||||
|
||||
class ActionButton(Button, ActionItem):
|
||||
'''
|
||||
ActionButton class, see module documentation for more information.
|
||||
|
||||
The text color, width and size_hint_x are set manually via the Kv language
|
||||
file. It covers a lot of cases: with/without an icon, with/without a group
|
||||
and takes care of the padding between elements.
|
||||
|
||||
You don't have much control over these properties, so if you want to
|
||||
customize its appearance, we suggest you create you own button
|
||||
representation. You can do this by creating a class that subclasses an
|
||||
existing widget and an :class:`ActionItem`::
|
||||
|
||||
class MyOwnActionButton(Button, ActionItem):
|
||||
pass
|
||||
|
||||
You can then create your own style using the Kv language.
|
||||
'''
|
||||
|
||||
icon = StringProperty(None, allownone=True)
|
||||
'''
|
||||
Source image to use when the Button is part of the ActionBar. If the
|
||||
Button is in a group, the text will be preferred.
|
||||
|
||||
:attr:`icon` is a :class:`~kivy.properties.StringProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
|
||||
class ActionPrevious(BoxLayout, ActionItem):
|
||||
'''
|
||||
ActionPrevious class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
with_previous = BooleanProperty(True)
|
||||
'''
|
||||
Specifies whether the previous_icon will be shown or not. Note that it is
|
||||
up to the user to implement the desired behavior using the *on_press* or
|
||||
similar events.
|
||||
|
||||
:attr:`with_previous` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
app_icon = StringProperty(window_icon)
|
||||
'''
|
||||
Application icon for the ActionView.
|
||||
|
||||
:attr:`app_icon` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to the window icon if set, otherwise
|
||||
'data/logo/kivy-icon-32.png'.
|
||||
'''
|
||||
|
||||
app_icon_width = NumericProperty(0)
|
||||
'''
|
||||
Width of app_icon image.
|
||||
|
||||
:attr:`app_icon_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
app_icon_height = NumericProperty(0)
|
||||
'''
|
||||
Height of app_icon image.
|
||||
|
||||
:attr:`app_icon_height` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
color = ColorProperty([1, 1, 1, 1])
|
||||
'''
|
||||
Text color, in the format (r, g, b, a)
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults
|
||||
to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
previous_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/previous_normal')
|
||||
'''
|
||||
Image for the 'previous' ActionButtons default graphical representation.
|
||||
|
||||
:attr:`previous_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/previous_normal'.
|
||||
'''
|
||||
|
||||
previous_image_width = NumericProperty(0)
|
||||
'''
|
||||
Width of previous_image image.
|
||||
|
||||
:attr:`width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
previous_image_height = NumericProperty(0)
|
||||
'''
|
||||
Height of previous_image image.
|
||||
|
||||
:attr:`app_icon_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
title = StringProperty('')
|
||||
'''
|
||||
Title for ActionView.
|
||||
|
||||
:attr:`title` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to ''.
|
||||
'''
|
||||
|
||||
markup = BooleanProperty(False)
|
||||
'''
|
||||
If True, the text will be rendered using the
|
||||
:class:`~kivy.core.text.markup.MarkupLabel`: you can change the style of
|
||||
the text using tags. Check the :doc:`api-kivy.core.text.markup`
|
||||
documentation for more information.
|
||||
|
||||
:attr:`markup` 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(ActionPrevious, self).__init__(**kwargs)
|
||||
if not self.app_icon:
|
||||
self.app_icon = 'data/logo/kivy-icon-32.png'
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
|
||||
class ActionToggleButton(ActionItem, ToggleButton):
|
||||
'''
|
||||
ActionToggleButton class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
icon = StringProperty(None, allownone=True)
|
||||
'''
|
||||
Source image to use when the Button is part of the ActionBar. If the
|
||||
Button is in a group, the text will be preferred.
|
||||
'''
|
||||
|
||||
|
||||
class ActionLabel(ActionItem, Label):
|
||||
'''
|
||||
ActionLabel class, see module documentation for more information.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionCheck(ActionItem, CheckBox):
|
||||
'''
|
||||
ActionCheck class, see module documentation for more information.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionSeparator(ActionItem, Widget):
|
||||
'''
|
||||
ActionSeparator class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/separator')
|
||||
'''
|
||||
Background image for the separators default graphical representation.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/separator'.
|
||||
'''
|
||||
|
||||
|
||||
class ActionDropDown(DropDown):
|
||||
'''
|
||||
ActionDropDown class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
|
||||
class ActionGroup(ActionItem, Button):
|
||||
'''
|
||||
ActionGroup class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
use_separator = BooleanProperty(False)
|
||||
'''
|
||||
Specifies whether to use a separator after/before this group or not.
|
||||
|
||||
:attr:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
separator_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/separator')
|
||||
'''
|
||||
Background Image for an ActionSeparator in an ActionView.
|
||||
|
||||
:attr:`separator_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/separator'.
|
||||
'''
|
||||
|
||||
separator_width = NumericProperty(0)
|
||||
'''
|
||||
Width of the ActionSeparator in an ActionView.
|
||||
|
||||
:attr:`separator_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
mode = OptionProperty('normal', options=('normal', 'spinner'))
|
||||
'''
|
||||
Sets the current mode of an ActionGroup. If mode is 'normal', the
|
||||
ActionGroups children will be displayed normally if there is enough
|
||||
space, otherwise they will be displayed in a spinner. If mode is
|
||||
'spinner', then the children will always be displayed in a spinner.
|
||||
|
||||
:attr:`mode` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'normal'.
|
||||
'''
|
||||
|
||||
dropdown_width = NumericProperty(0)
|
||||
'''
|
||||
If non zero, provides the width for the associated DropDown. This is
|
||||
useful when some items in the ActionGroup's DropDown are wider than usual
|
||||
and you don't want to make the ActionGroup widget itself wider.
|
||||
|
||||
:attr:`dropdown_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
is_open = BooleanProperty(False)
|
||||
'''By default, the DropDown is not open. Set to True to open it.
|
||||
|
||||
:attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.list_action_item = []
|
||||
self._list_overflow_items = []
|
||||
super(ActionGroup, self).__init__(**kwargs)
|
||||
|
||||
# real is_open independent on public event
|
||||
self._is_open = False
|
||||
|
||||
# create DropDown for the group and save its state to _is_open
|
||||
self._dropdown = ActionDropDown()
|
||||
self._dropdown.bind(attach_to=lambda ins, value: setattr(
|
||||
self, '_is_open', True if value else False
|
||||
))
|
||||
|
||||
# put open/close responsibility to the event
|
||||
# - trigger dropdown opening when clicked
|
||||
self.bind(on_release=lambda *args: setattr(
|
||||
self, 'is_open', True
|
||||
))
|
||||
|
||||
# - trigger dropdown closing when an item
|
||||
# in the dropdown is clicked
|
||||
self._dropdown.bind(on_dismiss=lambda *args: setattr(
|
||||
self, 'is_open', False
|
||||
))
|
||||
|
||||
def on_is_open(self, instance, value):
|
||||
# opening only if the DropDown is closed
|
||||
if value and not self._is_open:
|
||||
self._toggle_dropdown()
|
||||
self._dropdown.open(self)
|
||||
return
|
||||
|
||||
# closing is_open manually, dismiss manually
|
||||
if not value and self._is_open:
|
||||
self._dropdown.dismiss()
|
||||
|
||||
def _toggle_dropdown(self, *largs):
|
||||
ddn = self._dropdown
|
||||
ddn.size_hint_x = None
|
||||
|
||||
# if container was set incorrectly and/or is missing
|
||||
if not ddn.container:
|
||||
return
|
||||
children = ddn.container.children
|
||||
|
||||
# set DropDown width manually or if not set, then widen
|
||||
# the ActionGroup + DropDown until the widest child fits
|
||||
if children:
|
||||
ddn.width = self.dropdown_width or max(
|
||||
self.width, max(c.pack_width for c in children)
|
||||
)
|
||||
else:
|
||||
ddn.width = self.width
|
||||
|
||||
# set the DropDown children's height
|
||||
for item in children:
|
||||
item.size_hint_y = None
|
||||
item.height = max([self.height, sp(48)])
|
||||
|
||||
# dismiss DropDown manually
|
||||
# auto_dismiss applies to touching outside of the DropDown
|
||||
item.bind(on_release=ddn.dismiss)
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `item` to `widget`.
|
||||
'''
|
||||
# if adding ActionSeparator ('normal' mode,
|
||||
# everything visible), add it to the parent
|
||||
if isinstance(widget, ActionSeparator):
|
||||
super(ActionGroup, self).add_widget(widget, *args, **kwargs)
|
||||
return
|
||||
|
||||
if not isinstance(widget, ActionItem):
|
||||
raise ActionBarException('ActionGroup only accepts ActionItem')
|
||||
|
||||
self.list_action_item.append(widget)
|
||||
|
||||
def show_group(self):
|
||||
# 'normal' mode, items can fit to the view
|
||||
self.clear_widgets()
|
||||
for item in self._list_overflow_items + self.list_action_item:
|
||||
item.inside_group = True
|
||||
self._dropdown.add_widget(item)
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
self._dropdown.clear_widgets(*args, **kwargs)
|
||||
|
||||
|
||||
class ActionOverflow(ActionGroup):
|
||||
'''
|
||||
ActionOverflow class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
overflow_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/overflow')
|
||||
'''
|
||||
Image to be used as an Overflow Image.
|
||||
|
||||
:attr:`overflow_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/overflow'.
|
||||
'''
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `action_item` to `widget`.
|
||||
'''
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
if isinstance(widget, ActionSeparator):
|
||||
return
|
||||
|
||||
if not isinstance(widget, ActionItem):
|
||||
raise ActionBarException('ActionView only accepts ActionItem'
|
||||
' (got {!r}'.format(widget))
|
||||
|
||||
else:
|
||||
if index == 0:
|
||||
index = len(self._list_overflow_items)
|
||||
self._list_overflow_items.insert(index, widget)
|
||||
|
||||
def show_default_items(self, parent):
|
||||
# display overflow and its items if widget's directly added to it
|
||||
if self._list_overflow_items == []:
|
||||
return
|
||||
self.show_group()
|
||||
super(ActionView, parent).add_widget(self)
|
||||
|
||||
|
||||
class ActionView(BoxLayout):
|
||||
'''
|
||||
ActionView class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
action_previous = ObjectProperty(None)
|
||||
'''
|
||||
Previous button for an ActionView.
|
||||
|
||||
:attr:`action_previous` is an :class:`~kivy.properties.ObjectProperty`
|
||||
and defaults to None.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''
|
||||
Background color in the format (r, g, b, a).
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_view')
|
||||
'''
|
||||
Background image of an ActionViews default graphical representation.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_view'.
|
||||
'''
|
||||
|
||||
use_separator = BooleanProperty(False)
|
||||
'''
|
||||
Specify whether to use a separator before every ActionGroup or not.
|
||||
|
||||
:attr:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
overflow_group = ObjectProperty(None)
|
||||
'''
|
||||
Widget to be used for the overflow.
|
||||
|
||||
:attr:`overflow_group` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to an instance of :class:`ActionOverflow`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._list_action_items = []
|
||||
self._list_action_group = []
|
||||
super(ActionView, self).__init__(**kwargs)
|
||||
self._state = ''
|
||||
if not self.overflow_group:
|
||||
self.overflow_group = ActionOverflow(
|
||||
use_separator=self.use_separator)
|
||||
|
||||
def on_action_previous(self, instance, value):
|
||||
self._list_action_items.insert(0, value)
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `action_item` to `widget`.
|
||||
'''
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
if not isinstance(widget, ActionItem):
|
||||
raise ActionBarException('ActionView only accepts ActionItem'
|
||||
' (got {!r}'.format(widget))
|
||||
|
||||
elif isinstance(widget, ActionOverflow):
|
||||
self.overflow_group = widget
|
||||
widget.use_separator = self.use_separator
|
||||
|
||||
elif isinstance(widget, ActionGroup):
|
||||
self._list_action_group.append(widget)
|
||||
widget.use_separator = self.use_separator
|
||||
|
||||
elif isinstance(widget, ActionPrevious):
|
||||
self.action_previous = widget
|
||||
|
||||
else:
|
||||
super(ActionView, self).add_widget(widget, index, *args, **kwargs)
|
||||
if index == 0:
|
||||
index = len(self._list_action_items)
|
||||
self._list_action_items.insert(index, widget)
|
||||
|
||||
def on_use_separator(self, instance, value):
|
||||
for group in self._list_action_group:
|
||||
group.use_separator = value
|
||||
if self.overflow_group:
|
||||
self.overflow_group.use_separator = value
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
super(ActionView, self).remove_widget(widget, *args, **kwargs)
|
||||
if isinstance(widget, ActionOverflow):
|
||||
for item in widget.list_action_item:
|
||||
if item in self._list_action_items:
|
||||
self._list_action_items.remove(item)
|
||||
|
||||
if widget in self._list_action_items:
|
||||
self._list_action_items.remove(widget)
|
||||
|
||||
def _clear_all(self):
|
||||
lst = self._list_action_items[:]
|
||||
self.clear_widgets()
|
||||
for group in self._list_action_group:
|
||||
group.clear_widgets()
|
||||
|
||||
self.overflow_group.clear_widgets()
|
||||
self.overflow_group.list_action_item = []
|
||||
self._list_action_items = lst
|
||||
|
||||
def _layout_all(self):
|
||||
# all the items can fit to the view, so expand everything
|
||||
super_add = super(ActionView, self).add_widget
|
||||
self._state = 'all'
|
||||
self._clear_all()
|
||||
if not self.action_previous.parent:
|
||||
super_add(self.action_previous)
|
||||
if len(self._list_action_items) > 1:
|
||||
for child in self._list_action_items[1:]:
|
||||
child.inside_group = False
|
||||
super_add(child)
|
||||
|
||||
for group in self._list_action_group:
|
||||
if group.mode == 'spinner':
|
||||
super_add(group)
|
||||
group.show_group()
|
||||
else:
|
||||
if group.list_action_item != []:
|
||||
super_add(ActionSeparator())
|
||||
for child in group.list_action_item:
|
||||
child.inside_group = False
|
||||
super_add(child)
|
||||
|
||||
self.overflow_group.show_default_items(self)
|
||||
|
||||
def _layout_group(self):
|
||||
# layout all the items in order to pack them per group
|
||||
super_add = super(ActionView, self).add_widget
|
||||
self._state = 'group'
|
||||
self._clear_all()
|
||||
if not self.action_previous.parent:
|
||||
super_add(self.action_previous)
|
||||
if len(self._list_action_items) > 1:
|
||||
for child in self._list_action_items[1:]:
|
||||
super_add(child)
|
||||
child.inside_group = False
|
||||
|
||||
for group in self._list_action_group:
|
||||
super_add(group)
|
||||
group.show_group()
|
||||
|
||||
self.overflow_group.show_default_items(self)
|
||||
|
||||
def _layout_random(self):
|
||||
# layout the items in order to pack all of them grouped, and display
|
||||
# only the action items having 'important'
|
||||
super_add = super(ActionView, self).add_widget
|
||||
self._state = 'random'
|
||||
self._clear_all()
|
||||
hidden_items = []
|
||||
hidden_groups = []
|
||||
total_width = 0
|
||||
if not self.action_previous.parent:
|
||||
super_add(self.action_previous)
|
||||
|
||||
width = (self.width - self.overflow_group.pack_width -
|
||||
self.action_previous.minimum_width)
|
||||
|
||||
if len(self._list_action_items):
|
||||
for child in self._list_action_items[1:]:
|
||||
if child.important:
|
||||
if child.pack_width + total_width < width:
|
||||
super_add(child)
|
||||
child.inside_group = False
|
||||
total_width += child.pack_width
|
||||
else:
|
||||
hidden_items.append(child)
|
||||
else:
|
||||
hidden_items.append(child)
|
||||
|
||||
# if space is left then display ActionItem inside their
|
||||
# ActionGroup
|
||||
if total_width < self.width:
|
||||
for group in self._list_action_group:
|
||||
if group.pack_width + total_width +\
|
||||
group.separator_width < width:
|
||||
super_add(group)
|
||||
group.show_group()
|
||||
total_width += (group.pack_width +
|
||||
group.separator_width)
|
||||
|
||||
else:
|
||||
hidden_groups.append(group)
|
||||
group_index = len(self.children) - 1
|
||||
# if space is left then display other ActionItems
|
||||
if total_width < self.width:
|
||||
for child in hidden_items[:]:
|
||||
if child.pack_width + total_width < width:
|
||||
super_add(child, group_index)
|
||||
total_width += child.pack_width
|
||||
child.inside_group = False
|
||||
hidden_items.remove(child)
|
||||
|
||||
# for all the remaining ActionItems and ActionItems with in
|
||||
# ActionGroups, Display them inside overflow_group
|
||||
extend_hidden = hidden_items.extend
|
||||
for group in hidden_groups:
|
||||
extend_hidden(group.list_action_item)
|
||||
|
||||
overflow_group = self.overflow_group
|
||||
|
||||
if hidden_items != []:
|
||||
over_add = super(overflow_group.__class__,
|
||||
overflow_group).add_widget
|
||||
for child in hidden_items:
|
||||
over_add(child)
|
||||
|
||||
overflow_group.show_group()
|
||||
if not self.overflow_group.parent:
|
||||
super_add(overflow_group)
|
||||
|
||||
def on_width(self, width, *args):
|
||||
# determine the layout to use
|
||||
|
||||
# can we display all of them?
|
||||
total_width = 0
|
||||
for child in self._list_action_items:
|
||||
total_width += child.pack_width
|
||||
for group in self._list_action_group:
|
||||
for child in group.list_action_item:
|
||||
total_width += child.pack_width
|
||||
if total_width <= self.width:
|
||||
if self._state != 'all':
|
||||
self._layout_all()
|
||||
return
|
||||
|
||||
# can we display them per group?
|
||||
total_width = 0
|
||||
for child in self._list_action_items:
|
||||
total_width += child.pack_width
|
||||
for group in self._list_action_group:
|
||||
total_width += group.pack_width
|
||||
if total_width < self.width:
|
||||
# ok, we can display all the items grouped
|
||||
if self._state != 'group':
|
||||
self._layout_group()
|
||||
return
|
||||
|
||||
# none of the solutions worked, display them in pack mode
|
||||
self._layout_random()
|
||||
|
||||
|
||||
class ContextualActionView(ActionView):
|
||||
'''
|
||||
ContextualActionView class, see the module documentation for more
|
||||
information.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionBar(BoxLayout):
|
||||
'''
|
||||
ActionBar class, which acts as the main container for an
|
||||
:class:`ActionView` instance. The ActionBar determines the overall
|
||||
styling aspects of the bar. :class:`ActionItem`\\s are not added to
|
||||
this class directly, but to the contained :class:`ActionView` instance.
|
||||
|
||||
:Events:
|
||||
`on_previous`
|
||||
Fired when action_previous of action_view is pressed.
|
||||
|
||||
Please see the module documentation for more information.
|
||||
'''
|
||||
|
||||
action_view = ObjectProperty(None)
|
||||
'''
|
||||
action_view of the ActionBar.
|
||||
|
||||
:attr:`action_view` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None or the last ActionView instance added to the ActionBar.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''
|
||||
Background color, in the format (r, g, b, a).
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_bar')
|
||||
|
||||
'''
|
||||
Background image of the ActionBars default graphical representation.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_bar'.
|
||||
'''
|
||||
|
||||
border = ListProperty([2, 2, 2, 2])
|
||||
'''
|
||||
The border to be applied to the :attr:`background_image`.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
[2, 2, 2, 2]
|
||||
'''
|
||||
|
||||
__events__ = ('on_previous',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ActionBar, self).__init__(**kwargs)
|
||||
self._stack_cont_action_view = []
|
||||
self._emit_previous = partial(self.dispatch, 'on_previous')
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `view` to `widget`.
|
||||
'''
|
||||
if isinstance(widget, ContextualActionView):
|
||||
self._stack_cont_action_view.append(widget)
|
||||
if widget.action_previous is not None:
|
||||
widget.action_previous.unbind(on_release=self._emit_previous)
|
||||
widget.action_previous.bind(on_release=self._emit_previous)
|
||||
self.clear_widgets()
|
||||
super(ActionBar, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
elif isinstance(widget, ActionView):
|
||||
self.action_view = widget
|
||||
super(ActionBar, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
else:
|
||||
raise ActionBarException(
|
||||
'ActionBar can only add ContextualActionView or ActionView')
|
||||
|
||||
def on_previous(self, *args):
|
||||
self._pop_contextual_action_view()
|
||||
|
||||
def _pop_contextual_action_view(self):
|
||||
'''Remove the current ContextualActionView and display either the
|
||||
previous one or the ActionView.
|
||||
'''
|
||||
self._stack_cont_action_view.pop()
|
||||
self.clear_widgets()
|
||||
if self._stack_cont_action_view == []:
|
||||
super(ActionBar, self).add_widget(self.action_view)
|
||||
else:
|
||||
super(ActionBar, self).add_widget(self._stack_cont_action_view[-1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.factory import Factory
|
||||
|
||||
# XXX clean the first registration done from '__main__' here.
|
||||
# otherwise kivy.uix.actionbar.ActionPrevious != __main__.ActionPrevious
|
||||
Factory.unregister('ActionPrevious')
|
||||
|
||||
Builder.load_string('''
|
||||
<MainWindow>:
|
||||
ActionBar:
|
||||
pos_hint: {'top':1}
|
||||
ActionView:
|
||||
use_separator: True
|
||||
ActionPrevious:
|
||||
title: 'Action Bar'
|
||||
with_previous: False
|
||||
ActionOverflow:
|
||||
ActionButton:
|
||||
text: 'Btn0'
|
||||
icon: 'atlas://data/images/defaulttheme/audio-volume-high'
|
||||
ActionButton:
|
||||
text: 'Btn1'
|
||||
ActionButton:
|
||||
text: 'Btn2'
|
||||
ActionGroup:
|
||||
text: 'Group 1'
|
||||
ActionButton:
|
||||
text: 'Btn3'
|
||||
ActionButton:
|
||||
text: 'Btn4'
|
||||
ActionGroup:
|
||||
dropdown_width: 200
|
||||
text: 'Group 2'
|
||||
ActionButton:
|
||||
text: 'Btn5'
|
||||
ActionButton:
|
||||
text: 'Btn6'
|
||||
ActionButton:
|
||||
text: 'Btn7'
|
||||
''')
|
||||
|
||||
class MainWindow(FloatLayout):
|
||||
pass
|
||||
|
||||
float_layout = MainWindow()
|
||||
runTouchApp(float_layout)
|
||||
@@ -0,0 +1,122 @@
|
||||
'''
|
||||
Anchor Layout
|
||||
=============
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: images/anchorlayout.gif
|
||||
:align: right
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: images/anchorlayout.png
|
||||
:align: right
|
||||
|
||||
The :class:`AnchorLayout` aligns its children to a border (top, bottom,
|
||||
left, right) or center.
|
||||
|
||||
|
||||
To draw a button in the lower-right corner::
|
||||
|
||||
layout = AnchorLayout(
|
||||
anchor_x='right', anchor_y='bottom')
|
||||
btn = Button(text='Hello World')
|
||||
layout.add_widget(btn)
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('AnchorLayout', )
|
||||
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import OptionProperty, VariableListProperty
|
||||
|
||||
|
||||
class AnchorLayout(Layout):
|
||||
'''Anchor layout class. See the module documentation for more information.
|
||||
'''
|
||||
|
||||
padding = VariableListProperty([0, 0, 0, 0])
|
||||
'''Padding between the widget box and its children, in pixels:
|
||||
[padding_left, padding_top, padding_right, padding_bottom].
|
||||
|
||||
padding also accepts a two argument form [padding_horizontal,
|
||||
padding_vertical] and a one argument form [padding].
|
||||
|
||||
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
|
||||
defaults to [0, 0, 0, 0].
|
||||
'''
|
||||
|
||||
anchor_x = OptionProperty('center', options=(
|
||||
'left', 'center', 'right'))
|
||||
'''Horizontal anchor.
|
||||
|
||||
:attr:`anchor_x` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'center'. It accepts values of 'left', 'center' or
|
||||
'right'.
|
||||
'''
|
||||
|
||||
anchor_y = OptionProperty('center', options=(
|
||||
'top', 'center', 'bottom'))
|
||||
'''Vertical anchor.
|
||||
|
||||
:attr:`anchor_y` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'center'. It accepts values of 'top', 'center' or
|
||||
'bottom'.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AnchorLayout, self).__init__(**kwargs)
|
||||
fbind = self.fbind
|
||||
update = self._trigger_layout
|
||||
fbind('children', update)
|
||||
fbind('parent', update)
|
||||
fbind('padding', update)
|
||||
fbind('anchor_x', update)
|
||||
fbind('anchor_y', update)
|
||||
fbind('size', update)
|
||||
fbind('pos', update)
|
||||
|
||||
def do_layout(self, *largs):
|
||||
_x, _y = self.pos
|
||||
width = self.width
|
||||
height = self.height
|
||||
anchor_x = self.anchor_x
|
||||
anchor_y = self.anchor_y
|
||||
pad_left, pad_top, pad_right, pad_bottom = self.padding
|
||||
|
||||
for c in self.children:
|
||||
x, y = _x, _y
|
||||
cw, ch = c.size
|
||||
shw, shh = c.size_hint
|
||||
shw_min, shh_min = c.size_hint_min
|
||||
shw_max, shh_max = c.size_hint_max
|
||||
|
||||
if shw is not None:
|
||||
cw = shw * (width - pad_left - pad_right)
|
||||
if shw_min is not None and cw < shw_min:
|
||||
cw = shw_min
|
||||
elif shw_max is not None and cw > shw_max:
|
||||
cw = shw_max
|
||||
|
||||
if shh is not None:
|
||||
ch = shh * (height - pad_top - pad_bottom)
|
||||
if shh_min is not None and ch < shh_min:
|
||||
ch = shh_min
|
||||
elif shh_max is not None and ch > shh_max:
|
||||
ch = shh_max
|
||||
|
||||
if anchor_x == 'left':
|
||||
x = x + pad_left
|
||||
elif anchor_x == 'right':
|
||||
x = x + width - (cw + pad_right)
|
||||
else:
|
||||
x = x + (width - pad_right + pad_left - cw) / 2
|
||||
if anchor_y == 'bottom':
|
||||
y = y + pad_bottom
|
||||
elif anchor_y == 'top':
|
||||
y = y + height - (ch + pad_top)
|
||||
else:
|
||||
y = y + (height - pad_top + pad_bottom - ch) / 2
|
||||
|
||||
c.pos = x, y
|
||||
c.size = cw, ch
|
||||
@@ -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
|
||||
@@ -0,0 +1,212 @@
|
||||
'''
|
||||
Button Behavior
|
||||
===============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. You can combine this class with
|
||||
other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative buttons that preserve Kivy button behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds button behavior to an image to make a checkbox that
|
||||
behaves like a button::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_press(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
|
||||
def on_release(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
See :class:`~kivy.uix.behaviors.ButtonBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
from kivy.properties import OptionProperty, ObjectProperty, \
|
||||
BooleanProperty, NumericProperty
|
||||
from time import time
|
||||
|
||||
|
||||
class ButtonBehavior(object):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. Please see the
|
||||
:mod:`button behaviors module <kivy.uix.behaviors.button>` documentation
|
||||
for more information.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
|
||||
'''
|
||||
|
||||
state = OptionProperty('normal', options=('normal', 'down'))
|
||||
'''The state of the button, must be one of 'normal' or 'down'.
|
||||
The state is 'down' only when the button is currently touched/clicked,
|
||||
otherwise its 'normal'.
|
||||
|
||||
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'normal'.
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
min_state_time = NumericProperty(0)
|
||||
'''The minimum period of time which the widget must remain in the
|
||||
`'down'` state.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
:attr:`min_state_time` is a float and defaults to 0.035. This value is
|
||||
taken from :class:`~kivy.config.Config`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The default value is now False.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
if 'min_state_time' not in kwargs:
|
||||
self.min_state_time = float(Config.get('graphics',
|
||||
'min_state_time'))
|
||||
super(ButtonBehavior, self).__init__(**kwargs)
|
||||
self.__state_event = None
|
||||
self.__touch_time = None
|
||||
self.fbind('state', self.cancel_event)
|
||||
|
||||
def _do_press(self):
|
||||
self.state = 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
self.state = 'normal'
|
||||
|
||||
def cancel_event(self, *args):
|
||||
if self.__state_event:
|
||||
self.__state_event.cancel()
|
||||
self.__state_event = None
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(ButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.__touch_time = time()
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(ButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(ButtonBehavior, self).on_touch_up(touch)
|
||||
assert self in touch.ud
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
|
||||
if (not self.always_release and
|
||||
not self.collide_point(*touch.pos)):
|
||||
self._do_release()
|
||||
return
|
||||
|
||||
touchtime = time() - self.__touch_time
|
||||
if touchtime < self.min_state_time:
|
||||
self.__state_event = Clock.schedule_once(
|
||||
self._do_release, self.min_state_time - touchtime)
|
||||
else:
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
return True
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
def trigger_action(self, duration=0.1):
|
||||
'''Trigger whatever action(s) have been bound to the button by calling
|
||||
both the on_press and on_release callbacks.
|
||||
|
||||
This is similar to a quick button press without using any touch events,
|
||||
but note that like most kivy code, this is not guaranteed to be safe to
|
||||
call from external threads. If needed use
|
||||
:class:`Clock <kivy.clock.Clock>` to safely schedule this function and
|
||||
the resulting callbacks to be called from the main thread.
|
||||
|
||||
Duration is the length of the press in seconds. Pass 0 if you want
|
||||
the action to happen instantly.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
|
||||
def trigger_release(dt):
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
if not duration:
|
||||
trigger_release(0)
|
||||
else:
|
||||
Clock.schedule_once(trigger_release, duration)
|
||||
@@ -0,0 +1,167 @@
|
||||
'''
|
||||
Code Navigation Behavior
|
||||
========================
|
||||
|
||||
The :class:`~kivy.uix.bahviors.CodeNavigationBehavior` modifies navigation
|
||||
behavior in the :class:`~kivy.uix.textinput.TextInput`, making it work like an
|
||||
IDE instead of a word processor.
|
||||
|
||||
Using this mixin gives the TextInput the ability to recognize whitespace,
|
||||
punctuation and case variations (e.g. CamelCase) when moving over text. It
|
||||
is currently used by the :class:`~kivy.uix.codeinput.CodeInput` widget.
|
||||
'''
|
||||
|
||||
__all__ = ('CodeNavigationBehavior', )
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
import string
|
||||
|
||||
|
||||
class CodeNavigationBehavior(EventDispatcher):
|
||||
'''Code navigation behavior. Modifies the navigation behavior in TextInput
|
||||
to work like an IDE instead of a word processor. Please see the
|
||||
:mod:`code navigation behaviors module <kivy.uix.behaviors.codenavigation>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
def _move_cursor_word_left(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
pos -= 1
|
||||
|
||||
if pos == 0:
|
||||
return 0, 0
|
||||
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if col == -1:
|
||||
if row == 0:
|
||||
return 0, 0
|
||||
row -= 1
|
||||
rline = lines[row]
|
||||
col = len(rline)
|
||||
lc = c
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
if lc not in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in punct:
|
||||
col += 1
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
col += 1
|
||||
break
|
||||
if mode == 'us' and c != '_' and (c in punct or c in ws):
|
||||
col += 1
|
||||
break
|
||||
|
||||
if mode == 'us' and c != '_':
|
||||
mode = ('normal' if c in ucase
|
||||
else 'ws' if c in ws
|
||||
else 'camel')
|
||||
elif mode == 'ws' and c not in ws:
|
||||
mode = ('normal' if c in ucase
|
||||
else 'us' if c == '_'
|
||||
else 'punct' if c in punct
|
||||
else 'camel')
|
||||
|
||||
col -= 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == len(lines) - 1:
|
||||
return row, len(lines[row])
|
||||
row += 1
|
||||
col = 0
|
||||
|
||||
return col, row
|
||||
|
||||
def _move_cursor_word_right(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
mrow = len(lines) - 1
|
||||
|
||||
if row == mrow and col == len(lines[row]):
|
||||
return col, row
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c in lcase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if mode in ('normal', 'camel', 'punct') and c in ws:
|
||||
mode = 'ws'
|
||||
elif mode in ('normal', 'camel') and c == '_':
|
||||
mode = 'us'
|
||||
elif mode == 'normal' and c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
if mode == 'us':
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c != '_':
|
||||
break
|
||||
if mode == 'ws' and c not in ws:
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
break
|
||||
if mode != 'punct' and c != '_' and c in punct:
|
||||
break
|
||||
|
||||
col += 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == mrow:
|
||||
return len(rline), mrow
|
||||
row += 1
|
||||
rline = lines[row]
|
||||
col = 0
|
||||
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
break
|
||||
|
||||
return col, row
|
||||
@@ -0,0 +1,688 @@
|
||||
'''
|
||||
Compound Selection Behavior
|
||||
===========================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class implements the logic
|
||||
behind keyboard and touch selection of selectable widgets managed by the
|
||||
derived widget. For example, it can be combined with a
|
||||
:class:`~kivy.uix.gridlayout.GridLayout` to add selection to the layout.
|
||||
|
||||
Compound selection concepts
|
||||
---------------------------
|
||||
|
||||
At its core, it keeps a dynamic list of widgets that can be selected.
|
||||
Then, as the touches and keyboard input are passed in, it selects one or
|
||||
more of the widgets based on these inputs. For example, it uses the mouse
|
||||
scroll and keyboard up/down buttons to scroll through the list of widgets.
|
||||
Multiselection can also be achieved using the keyboard shift and ctrl keys.
|
||||
|
||||
Finally, in addition to the up/down type keyboard inputs, compound selection
|
||||
can also accept letters from the keyboard to be used to select nodes with
|
||||
associated strings that start with those letters, similar to how files
|
||||
are selected by a file browser.
|
||||
|
||||
Selection mechanics
|
||||
-------------------
|
||||
|
||||
When the controller needs to select a node, it calls :meth:`select_node` and
|
||||
:meth:`deselect_node`. Therefore, they must be overwritten in order alter
|
||||
node selection. By default, the class doesn't listen for keyboard or
|
||||
touch events, so the derived widget must call
|
||||
:meth:`select_with_touch`, :meth:`select_with_key_down`, and
|
||||
:meth:`select_with_key_up` on events that it wants to pass on for selection
|
||||
purposes.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
To add selection to a grid layout which will contain
|
||||
:class:`~kivy.uix.Button` widgets. For each button added to the layout, you
|
||||
need to bind the :attr:`~kivy.uix.widget.Widget.on_touch_down` of the button
|
||||
to :meth:`select_with_touch` to pass on the touch events::
|
||||
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.core.window import Window
|
||||
from kivy.app import App
|
||||
|
||||
|
||||
class SelectableGrid(FocusBehavior, CompoundSelectionBehavior, GridLayout):
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key presses will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_down(
|
||||
window, keycode, text, modifiers):
|
||||
return True
|
||||
if self.select_with_key_down(window, keycode, text, modifiers):
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key release will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_up(window, keycode):
|
||||
return True
|
||||
if self.select_with_key_up(window, keycode):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
""" Override the adding of widgets so we can bind and catch their
|
||||
*on_touch_down* events. """
|
||||
widget.bind(on_touch_down=self.button_touch_down,
|
||||
on_touch_up=self.button_touch_up)
|
||||
return super(SelectableGrid, self)\
|
||||
.add_widget(widget, *args, **kwargs)
|
||||
|
||||
def button_touch_down(self, button, touch):
|
||||
""" Use collision detection to select buttons when the touch occurs
|
||||
within their area. """
|
||||
if button.collide_point(*touch.pos):
|
||||
self.select_with_touch(button, touch)
|
||||
|
||||
def button_touch_up(self, button, touch):
|
||||
""" Use collision detection to de-select buttons when the touch
|
||||
occurs outside their area and *touch_multiselect* is not True. """
|
||||
if not (button.collide_point(*touch.pos) or
|
||||
self.touch_multiselect):
|
||||
self.deselect_node(button)
|
||||
|
||||
def select_node(self, node):
|
||||
node.background_color = (1, 0, 0, 1)
|
||||
return super(SelectableGrid, self).select_node(node)
|
||||
|
||||
def deselect_node(self, node):
|
||||
node.background_color = (1, 1, 1, 1)
|
||||
super(SelectableGrid, self).deselect_node(node)
|
||||
|
||||
def on_selected_nodes(self, grid, nodes):
|
||||
print("Selected nodes = {0}".format(nodes))
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
grid = SelectableGrid(cols=3, rows=2, touch_multiselect=True,
|
||||
multiselect=True)
|
||||
for i in range(0, 6):
|
||||
grid.add_widget(Button(text="Button {0}".format(i)))
|
||||
return grid
|
||||
|
||||
|
||||
TestApp().run()
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('CompoundSelectionBehavior', )
|
||||
|
||||
from time import time
|
||||
from os import environ
|
||||
|
||||
from kivy.properties import NumericProperty, BooleanProperty, ListProperty
|
||||
|
||||
|
||||
if 'KIVY_DOC' not in environ:
|
||||
from kivy.config import Config
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
else:
|
||||
_is_desktop = False
|
||||
|
||||
|
||||
class CompoundSelectionBehavior(object):
|
||||
'''The Selection behavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
implements the logic behind keyboard and touch
|
||||
selection of selectable widgets managed by the derived widget. Please see
|
||||
the :mod:`compound selection behaviors module
|
||||
<kivy.uix.behaviors.compoundselection>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
selected_nodes = ListProperty([])
|
||||
'''The list of selected nodes.
|
||||
|
||||
.. note::
|
||||
|
||||
Multiple nodes can be selected right after one another e.g. using the
|
||||
keyboard. When listening to :attr:`selected_nodes`, one should be
|
||||
aware of this.
|
||||
|
||||
:attr:`selected_nodes` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to the empty list, []. It is read-only and should not be modified.
|
||||
'''
|
||||
|
||||
touch_multiselect = BooleanProperty(False)
|
||||
'''A special touch mode which determines whether touch events, as
|
||||
processed by :meth:`select_with_touch`, will add the currently touched
|
||||
node to the selection, or if it will clear the selection before adding the
|
||||
node. This allows the selection of multiple nodes by simply touching them.
|
||||
|
||||
This is different from :attr:`multiselect` because when it is True,
|
||||
simply touching an unselected node will select it, even if ctrl is not
|
||||
pressed. If it is False, however, ctrl must be pressed in order to
|
||||
add to the selection when :attr:`multiselect` is True.
|
||||
|
||||
.. note::
|
||||
|
||||
:attr:`multiselect`, when False, will disable
|
||||
:attr:`touch_multiselect`.
|
||||
|
||||
:attr:`touch_multiselect` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
multiselect = BooleanProperty(False)
|
||||
'''Determines whether multiple nodes can be selected. If enabled, keyboard
|
||||
shift and ctrl selection, optionally combined with touch, for example, will
|
||||
be able to select multiple widgets in the normally expected manner.
|
||||
This dominates :attr:`touch_multiselect` when False.
|
||||
|
||||
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
touch_deselect_last = BooleanProperty(not _is_desktop)
|
||||
'''Determines whether the last selected node can be deselected when
|
||||
:attr:`multiselect` or :attr:`touch_multiselect` is False.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`touch_deselect_last` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True on mobile, False on desktop platforms.
|
||||
'''
|
||||
|
||||
keyboard_select = BooleanProperty(True)
|
||||
'''Determines whether the keyboard can be used for selection. If False,
|
||||
keyboard inputs will be ignored.
|
||||
|
||||
:attr:`keyboard_select` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
page_count = NumericProperty(10)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when pageup (or pagedown) is
|
||||
pressed.
|
||||
|
||||
:attr:`page_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 10.
|
||||
'''
|
||||
|
||||
up_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the up (or down) arrow on
|
||||
the keyboard is pressed.
|
||||
|
||||
:attr:`up_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
right_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the right (or left) arrow
|
||||
on the keyboard is pressed.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
scroll_count = NumericProperty(0)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the mouse scroll wheel is
|
||||
scrolled.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
nodes_order_reversed = BooleanProperty(True)
|
||||
''' (Internal) Indicates whether the order of the nodes as displayed top-
|
||||
down is reversed compared to their order in :meth:`get_selectable_nodes`
|
||||
(e.g. how the children property is reversed compared to how
|
||||
it's displayed).
|
||||
'''
|
||||
|
||||
text_entry_timeout = NumericProperty(1.)
|
||||
'''When typing characters in rapid succession (i.e. the time difference
|
||||
since the last character is less than :attr:`text_entry_timeout`), the
|
||||
keys get concatenated and the combined text is passed as the key argument
|
||||
of :meth:`goto_node`.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
_anchor = None # the last anchor node selected (e.g. shift relative node)
|
||||
# the idx may be out of sync
|
||||
_anchor_idx = 0 # cache indexes in case list hasn't changed
|
||||
_last_selected_node = None # the absolute last node selected
|
||||
_last_node_idx = 0
|
||||
_ctrl_down = False # if it's pressed - for e.g. shift selection
|
||||
_shift_down = False
|
||||
# holds str used to find node, e.g. if word is typed. passed to goto_node
|
||||
_word_filter = ''
|
||||
_last_key_time = 0 # time since last press, for finding whole strs in node
|
||||
_key_list = [] # keys that are already pressed, to not press continuously
|
||||
_offset_counts = {} # cache of counts for faster access
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CompoundSelectionBehavior, self).__init__(**kwargs)
|
||||
self._key_list = []
|
||||
|
||||
def ensure_single_select(*l):
|
||||
if (not self.multiselect) and len(self.selected_nodes) > 1:
|
||||
self.clear_selection()
|
||||
update_counts = self._update_counts
|
||||
update_counts()
|
||||
fbind = self.fbind
|
||||
fbind('multiselect', ensure_single_select)
|
||||
fbind('page_count', update_counts)
|
||||
fbind('up_count', update_counts)
|
||||
fbind('right_count', update_counts)
|
||||
fbind('scroll_count', update_counts)
|
||||
|
||||
def select_with_touch(self, node, touch=None):
|
||||
'''(internal) Processes a touch on the node. This should be called by
|
||||
the derived widget when a node is touched and is to be used for
|
||||
selection. Depending on the keyboard keys pressed and the
|
||||
configuration, it could select or deslect this and other nodes in the
|
||||
selectable nodes list, :meth:`get_selectable_nodes`.
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node that received the touch. Can be None for a scroll
|
||||
type touch.
|
||||
`touch`
|
||||
Optionally, the touch. Defaults to None.
|
||||
|
||||
:Returns:
|
||||
bool, True if the touch was used, False otherwise.
|
||||
'''
|
||||
multi = self.multiselect
|
||||
multiselect = multi and (self._ctrl_down or self.touch_multiselect)
|
||||
range_select = multi and self._shift_down
|
||||
|
||||
if touch and 'button' in touch.profile and touch.button in\
|
||||
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
|
||||
node_src, idx_src = self._resolve_last_node()
|
||||
node, idx = self.goto_node(touch.button, node_src, idx_src)
|
||||
if node == node_src:
|
||||
return False
|
||||
if range_select:
|
||||
self._select_range(multiselect, True, node, idx)
|
||||
else:
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
if node is None:
|
||||
return False
|
||||
|
||||
if (node in self.selected_nodes and (not range_select)): # selected
|
||||
if multiselect:
|
||||
self.deselect_node(node)
|
||||
else:
|
||||
selected_node_count = len(self.selected_nodes)
|
||||
self.clear_selection()
|
||||
if not self.touch_deselect_last or selected_node_count > 1:
|
||||
self.select_node(node)
|
||||
elif range_select:
|
||||
# keep anchor only if not multiselect (ctrl-type selection)
|
||||
self._select_range(multiselect, not multiselect, node, 0)
|
||||
else: # it's not selected at this point
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
|
||||
def select_with_key_down(self, keyboard, scancode, codepoint, modifiers,
|
||||
**kwargs):
|
||||
'''Processes a key press. This is called when a key press is to be used
|
||||
for selection. Depending on the keyboard keys pressed and the
|
||||
configuration, it could select or deselect nodes or node ranges
|
||||
from the selectable nodes list, :meth:`get_selectable_nodes`.
|
||||
|
||||
The parameters are such that it could be bound directly to the
|
||||
on_key_down event of a keyboard. Therefore, it is safe to be called
|
||||
repeatedly when the key is held down as is done by the keyboard.
|
||||
|
||||
:Returns:
|
||||
bool, True if the keypress was used, False otherwise.
|
||||
'''
|
||||
if not self.keyboard_select:
|
||||
return False
|
||||
keys = self._key_list
|
||||
multi = self.multiselect
|
||||
node_src, idx_src = self._resolve_last_node()
|
||||
text = scancode[1]
|
||||
|
||||
if text == 'shift':
|
||||
self._shift_down = True
|
||||
elif text in ('ctrl', 'lctrl', 'rctrl'):
|
||||
self._ctrl_down = True
|
||||
elif (multi and 'ctrl' in modifiers and text in ('a', 'A') and
|
||||
text not in keys):
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
select = self.select_node
|
||||
for node in sister_nodes:
|
||||
select(node)
|
||||
keys.append(text)
|
||||
else:
|
||||
s = text
|
||||
if len(text) > 1:
|
||||
d = {'divide': '/', 'mul': '*', 'substract': '-', 'add': '+',
|
||||
'decimal': '.'}
|
||||
if text.startswith('numpad'):
|
||||
s = text[6:]
|
||||
if len(s) > 1:
|
||||
if s in d:
|
||||
s = d[s]
|
||||
else:
|
||||
s = None
|
||||
else:
|
||||
s = None
|
||||
|
||||
if s is not None:
|
||||
if s not in keys: # don't keep adding while holding down
|
||||
if time() - self._last_key_time <= self.text_entry_timeout:
|
||||
self._word_filter += s
|
||||
else:
|
||||
self._word_filter = s
|
||||
keys.append(s)
|
||||
|
||||
self._last_key_time = time()
|
||||
node, idx = self.goto_node(self._word_filter, node_src,
|
||||
idx_src)
|
||||
else:
|
||||
self._word_filter = ''
|
||||
node, idx = self.goto_node(text, node_src, idx_src)
|
||||
|
||||
if node == node_src:
|
||||
return False
|
||||
|
||||
multiselect = multi and 'ctrl' in modifiers
|
||||
if multi and 'shift' in modifiers:
|
||||
self._select_range(multiselect, True, node, idx)
|
||||
else:
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
self._word_filter = ''
|
||||
return False
|
||||
|
||||
def select_with_key_up(self, keyboard, scancode, **kwargs):
|
||||
'''(internal) Processes a key release. This must be called by the
|
||||
derived widget when a key that :meth:`select_with_key_down` returned
|
||||
True is released.
|
||||
|
||||
The parameters are such that it could be bound directly to the
|
||||
on_key_up event of a keyboard.
|
||||
|
||||
:Returns:
|
||||
bool, True if the key release was used, False otherwise.
|
||||
'''
|
||||
if scancode[1] == 'shift':
|
||||
self._shift_down = False
|
||||
elif scancode[1] in ('ctrl', 'lctrl', 'rctrl'):
|
||||
self._ctrl_down = False
|
||||
else:
|
||||
try:
|
||||
self._key_list.remove(scancode[1])
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _update_counts(self, *largs):
|
||||
# doesn't invert indices here
|
||||
pc = self.page_count
|
||||
uc = self.up_count
|
||||
rc = self.right_count
|
||||
sc = self.scroll_count
|
||||
self._offset_counts = {'pageup': -pc, 'pagedown': pc, 'up': -uc,
|
||||
'down': uc, 'right': rc, 'left': -rc, 'scrollup': sc,
|
||||
'scrolldown': -sc, 'scrollright': -sc, 'scrollleft': sc}
|
||||
|
||||
def _resolve_last_node(self):
|
||||
# for offset selection, we have a anchor, and we select everything
|
||||
# between anchor and added offset relative to last node
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
if not len(sister_nodes):
|
||||
return None, 0
|
||||
last_node = self._last_selected_node
|
||||
last_idx = self._last_node_idx
|
||||
end = len(sister_nodes) - 1
|
||||
|
||||
if last_node is None:
|
||||
last_node = self._anchor
|
||||
last_idx = self._anchor_idx
|
||||
if last_node is None:
|
||||
return sister_nodes[end], end
|
||||
if last_idx > end or sister_nodes[last_idx] != last_node:
|
||||
try:
|
||||
return last_node, self.get_index_of_node(last_node,
|
||||
sister_nodes)
|
||||
except ValueError:
|
||||
return sister_nodes[end], end
|
||||
return last_node, last_idx
|
||||
|
||||
def _select_range(self, multiselect, keep_anchor, node, idx):
|
||||
'''Selects a range between self._anchor and node or idx.
|
||||
If multiselect is True, it will be added to the selection, otherwise
|
||||
it will unselect everything before selecting the range. This is only
|
||||
called if self.multiselect is True.
|
||||
If keep anchor is False, the anchor is moved to node. This should
|
||||
always be True for keyboard selection.
|
||||
'''
|
||||
select = self.select_node
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
end = len(sister_nodes) - 1
|
||||
last_node = self._anchor
|
||||
last_idx = self._anchor_idx
|
||||
|
||||
if last_node is None:
|
||||
last_idx = end
|
||||
last_node = sister_nodes[end]
|
||||
else:
|
||||
if last_idx > end or sister_nodes[last_idx] != last_node:
|
||||
try:
|
||||
last_idx = self.get_index_of_node(last_node, sister_nodes)
|
||||
except ValueError:
|
||||
# list changed - cannot do select across them
|
||||
return
|
||||
if idx > end or sister_nodes[idx] != node:
|
||||
try: # just in case
|
||||
idx = self.get_index_of_node(node, sister_nodes)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if last_idx > idx:
|
||||
last_idx, idx = idx, last_idx
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
for item in sister_nodes[last_idx:idx + 1]:
|
||||
select(item)
|
||||
|
||||
if keep_anchor:
|
||||
self._anchor = last_node
|
||||
self._anchor_idx = last_idx
|
||||
else:
|
||||
self._anchor = node # in case idx was reversed, reset
|
||||
self._anchor_idx = idx
|
||||
self._last_selected_node = node
|
||||
self._last_node_idx = idx
|
||||
|
||||
def clear_selection(self):
|
||||
''' Deselects all the currently selected nodes.
|
||||
'''
|
||||
# keep the anchor and last selected node
|
||||
deselect = self.deselect_node
|
||||
nodes = self.selected_nodes
|
||||
# empty beforehand so lookup in deselect will be fast
|
||||
for node in nodes[:]:
|
||||
deselect(node)
|
||||
|
||||
def get_selectable_nodes(self):
|
||||
'''(internal) Returns a list of the nodes that can be selected. It can
|
||||
be overwritten by the derived widget to return the correct list.
|
||||
|
||||
This list is used to determine which nodes to select with group
|
||||
selection. E.g. the last element in the list will be selected when
|
||||
home is pressed, pagedown will move (or add to, if shift is held) the
|
||||
selection from the current position by negative :attr:`page_count`
|
||||
nodes starting from the position of the currently selected node in
|
||||
this list and so on. Still, nodes can be selected even if they are not
|
||||
in this list.
|
||||
|
||||
.. note::
|
||||
|
||||
It is safe to dynamically change this list including removing,
|
||||
adding, or re-arranging its elements. Nodes can be selected even
|
||||
if they are not on this list. And selected nodes removed from the
|
||||
list will remain selected until :meth:`deselect_node` is called.
|
||||
|
||||
.. warning::
|
||||
|
||||
Layouts display their children in the reverse order. That is, the
|
||||
contents of :attr:`~kivy.uix.widget.Widget.children` is displayed
|
||||
form right to left, bottom to top. Therefore, internally, the
|
||||
indices of the elements returned by this function are reversed to
|
||||
make it work by default for most layouts so that the final result
|
||||
is consistent e.g. home, although it will select the last element
|
||||
in this list visually, will select the first element when
|
||||
counting from top to bottom and left to right. If this behavior is
|
||||
not desired, a reversed list should be returned instead.
|
||||
|
||||
Defaults to returning :attr:`~kivy.uix.widget.Widget.children`.
|
||||
'''
|
||||
return self.children
|
||||
|
||||
def get_index_of_node(self, node, selectable_nodes):
|
||||
'''(internal) Returns the index of the `node` within the
|
||||
`selectable_nodes` returned by :meth:`get_selectable_nodes`.
|
||||
'''
|
||||
return selectable_nodes.index(node)
|
||||
|
||||
def goto_node(self, key, last_node, last_node_idx):
|
||||
'''(internal) Used by the controller to get the node at the position
|
||||
indicated by key. The key can be keyboard inputs, e.g. pageup,
|
||||
or scroll inputs from the mouse scroll wheel, e.g. scrollup.
|
||||
'last_node' is the last node selected and is used to find the resulting
|
||||
node. For example, if the key is up, the returned node is one node
|
||||
up from the last node.
|
||||
|
||||
It can be overwritten by the derived widget.
|
||||
|
||||
:Parameters:
|
||||
`key`
|
||||
str, the string used to find the desired node. It can be any
|
||||
of the keyboard keys, as well as the mouse scrollup,
|
||||
scrolldown, scrollright, and scrollleft strings. If letters
|
||||
are typed in quick succession, the letters will be combined
|
||||
before it's passed in as key and can be used to find nodes that
|
||||
have an associated string that starts with those letters.
|
||||
`last_node`
|
||||
The last node that was selected.
|
||||
`last_node_idx`
|
||||
The cached index of the last node selected in the
|
||||
:meth:`get_selectable_nodes` list. If the list hasn't changed
|
||||
it saves having to look up the index of `last_node` in that
|
||||
list.
|
||||
|
||||
:Returns:
|
||||
tuple, the node targeted by key and its index in the
|
||||
:meth:`get_selectable_nodes` list. Returning
|
||||
`(last_node, last_node_idx)` indicates a node wasn't found.
|
||||
'''
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
end = len(sister_nodes) - 1
|
||||
counts = self._offset_counts
|
||||
if end == -1:
|
||||
return last_node, last_node_idx
|
||||
if last_node_idx > end or sister_nodes[last_node_idx] != last_node:
|
||||
try: # just in case
|
||||
last_node_idx = self.get_index_of_node(last_node, sister_nodes)
|
||||
except ValueError:
|
||||
return last_node, last_node_idx
|
||||
|
||||
is_reversed = self.nodes_order_reversed
|
||||
if key in counts:
|
||||
count = -counts[key] if is_reversed else counts[key]
|
||||
idx = max(min(count + last_node_idx, end), 0)
|
||||
return sister_nodes[idx], idx
|
||||
elif key == 'home':
|
||||
if is_reversed:
|
||||
return sister_nodes[end], end
|
||||
return sister_nodes[0], 0
|
||||
elif key == 'end':
|
||||
if is_reversed:
|
||||
return sister_nodes[0], 0
|
||||
return sister_nodes[end], end
|
||||
else:
|
||||
return last_node, last_node_idx
|
||||
|
||||
def select_node(self, node):
|
||||
''' Selects a node.
|
||||
|
||||
It is called by the controller when it selects a node and can be
|
||||
called from the outside to select a node directly. The derived widget
|
||||
should overwrite this method and change the node state to selected
|
||||
when called.
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node to be selected.
|
||||
|
||||
:Returns:
|
||||
bool, True if the node was selected, False otherwise.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method must be called by the derived widget using super if it
|
||||
is overwritten.
|
||||
'''
|
||||
nodes = self.selected_nodes
|
||||
if node in nodes:
|
||||
return False
|
||||
|
||||
if (not self.multiselect) and len(nodes):
|
||||
self.clear_selection()
|
||||
if node not in nodes:
|
||||
nodes.append(node)
|
||||
self._anchor = node
|
||||
self._last_selected_node = node
|
||||
return True
|
||||
|
||||
def deselect_node(self, node):
|
||||
''' Deselects a possibly selected node.
|
||||
|
||||
It is called by the controller when it deselects a node and can also
|
||||
be called from the outside to deselect a node directly. The derived
|
||||
widget should overwrite this method and change the node to its
|
||||
unselected state when this is called
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node to be deselected.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method must be called by the derived widget using super if it
|
||||
is overwritten.
|
||||
'''
|
||||
if node in self.selected_nodes:
|
||||
self.selected_nodes.remove(node)
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,160 @@
|
||||
'''
|
||||
Cover Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.cover.CoverBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ is intended for rendering
|
||||
textures to full widget size keeping the aspect ratio of the original texture.
|
||||
|
||||
Use cases are i.e. rendering full size background images or video content in
|
||||
a dynamic layout.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following examples add cover behavior to an image:
|
||||
|
||||
In python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import CoverBehavior
|
||||
from kivy.uix.image import Image
|
||||
|
||||
|
||||
class CoverImage(CoverBehavior, Image):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverImage, self).__init__(**kwargs)
|
||||
texture = self._coreimage.texture
|
||||
self.reference_size = texture.size
|
||||
self.texture = texture
|
||||
|
||||
|
||||
class MainApp(App):
|
||||
|
||||
def build(self):
|
||||
return CoverImage(source='image.jpg')
|
||||
|
||||
MainApp().run()
|
||||
|
||||
In Kivy Language:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
CoverImage:
|
||||
source: 'image.png'
|
||||
|
||||
<CoverImage@CoverBehavior+Image>:
|
||||
reference_size: self.texture_size
|
||||
|
||||
See :class:`~kivy.uix.behaviors.cover.CoverBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('CoverBehavior', )
|
||||
|
||||
from decimal import Decimal
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ListProperty
|
||||
|
||||
|
||||
Builder.load_string("""
|
||||
<-CoverBehavior>:
|
||||
canvas.before:
|
||||
StencilPush
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilUse
|
||||
canvas:
|
||||
Rectangle:
|
||||
texture: self.texture
|
||||
size: self.cover_size
|
||||
pos: self.cover_pos
|
||||
canvas.after:
|
||||
StencilUnUse
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilPop
|
||||
""")
|
||||
|
||||
|
||||
class CoverBehavior(object):
|
||||
'''The CoverBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
provides rendering a texture covering full widget size keeping aspect ratio
|
||||
of the original texture.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
reference_size = ListProperty([])
|
||||
'''Reference size used for aspect ratio approximation calculation.
|
||||
|
||||
:attr:`reference_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[]`.
|
||||
'''
|
||||
|
||||
cover_size = ListProperty([0, 0])
|
||||
'''Size of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
cover_pos = ListProperty([0, 0])
|
||||
'''Position of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_pos` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverBehavior, self).__init__(**kwargs)
|
||||
# bind covering
|
||||
self.bind(
|
||||
size=self.calculate_cover,
|
||||
pos=self.calculate_cover
|
||||
)
|
||||
|
||||
def _aspect_ratio_approximate(self, size):
|
||||
# return a decimal approximation of an aspect ratio.
|
||||
return Decimal('%.2f' % (float(size[0]) / size[1]))
|
||||
|
||||
def _scale_size(self, size, sizer):
|
||||
# return scaled size based on sizer, where sizer (n, None) scales x
|
||||
# to n and (None, n) scales y to n
|
||||
size_new = list(sizer)
|
||||
i = size_new.index(None)
|
||||
j = i * -1 + 1
|
||||
size_new[i] = (size_new[j] * size[i]) / size[j]
|
||||
return tuple(size_new)
|
||||
|
||||
def calculate_cover(self, *args):
|
||||
# return if no reference size yet
|
||||
if not self.reference_size:
|
||||
return
|
||||
size = self.size
|
||||
origin_appr = self._aspect_ratio_approximate(self.reference_size)
|
||||
crop_appr = self._aspect_ratio_approximate(size)
|
||||
# same aspect ratio
|
||||
if origin_appr == crop_appr:
|
||||
crop_size = self.size
|
||||
offset = (0, 0)
|
||||
# scale x
|
||||
elif origin_appr < crop_appr:
|
||||
crop_size = self._scale_size(self.reference_size, (size[0], None))
|
||||
offset = (0, ((crop_size[1] - size[1]) / 2) * -1)
|
||||
# scale y
|
||||
else:
|
||||
crop_size = self._scale_size(self.reference_size, (None, size[1]))
|
||||
offset = (((crop_size[0] - size[0]) / 2) * -1, 0)
|
||||
# set background size and position
|
||||
self.cover_size = crop_size
|
||||
self.cover_pos = offset
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Drag Behavior
|
||||
=============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.drag.DragBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides Drag behavior.
|
||||
When combined with a widget, dragging in the rectangle defined by the
|
||||
:attr:`~kivy.uix.behaviors.drag.DragBehavior.drag_rectangle` will drag the
|
||||
widget.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example creates a draggable label::
|
||||
|
||||
from kivy.uix.label import Label
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import DragBehavior
|
||||
from kivy.lang import Builder
|
||||
|
||||
# You could also put the following in your kv file...
|
||||
kv = '''
|
||||
<DragLabel>:
|
||||
# Define the properties for the DragLabel
|
||||
drag_rectangle: self.x, self.y, self.width, self.height
|
||||
drag_timeout: 10000000
|
||||
drag_distance: 0
|
||||
|
||||
FloatLayout:
|
||||
# Define the root widget
|
||||
DragLabel:
|
||||
size_hint: 0.25, 0.2
|
||||
text: 'Drag me'
|
||||
'''
|
||||
|
||||
|
||||
class DragLabel(DragBehavior, Label):
|
||||
pass
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return Builder.load_string(kv)
|
||||
|
||||
TestApp().run()
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ('DragBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import NumericProperty, ReferenceListProperty
|
||||
from kivy.config import Config
|
||||
from kivy.metrics import sp
|
||||
from functools import partial
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_scroll_timeout = _scroll_distance = 0
|
||||
if Config:
|
||||
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
|
||||
_scroll_distance = Config.getint('widgets', 'scroll_distance')
|
||||
|
||||
|
||||
class DragBehavior(object):
|
||||
'''
|
||||
The DragBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_ provides
|
||||
Drag behavior. When combined with a widget, dragging in the rectangle
|
||||
defined by :attr:`drag_rectangle` will drag the widget. Please see
|
||||
the :mod:`drag behaviors module <kivy.uix.behaviors.drag>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
drag_distance = NumericProperty(_scroll_distance)
|
||||
'''Distance to move before dragging the :class:`DragBehavior`, in pixels.
|
||||
As soon as the distance has been traveled, the :class:`DragBehavior` will
|
||||
start to drag, and no touch event will be dispatched to the children.
|
||||
It is advisable that you base this value on the dpi of your target device's
|
||||
screen.
|
||||
|
||||
:attr:`drag_distance` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_distance` as defined in the user
|
||||
:class:`~kivy.config.Config` (20 pixels by default).
|
||||
'''
|
||||
|
||||
drag_timeout = NumericProperty(_scroll_timeout)
|
||||
'''Timeout allowed to trigger the :attr:`drag_distance`, in milliseconds.
|
||||
If the user has not moved :attr:`drag_distance` within the timeout,
|
||||
dragging will be disabled, and the touch event will be dispatched to the
|
||||
children.
|
||||
|
||||
:attr:`drag_timeout` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_timeout` as defined in the user
|
||||
:class:`~kivy.config.Config` (55 milliseconds by default).
|
||||
'''
|
||||
|
||||
drag_rect_x = NumericProperty(0)
|
||||
'''X position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_x` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_y = NumericProperty(0)
|
||||
'''Y position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_Y` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_width = NumericProperty(100)
|
||||
'''Width of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rect_height = NumericProperty(100)
|
||||
'''Height of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rectangle = ReferenceListProperty(drag_rect_x, drag_rect_y,
|
||||
drag_rect_width, drag_rect_height)
|
||||
'''Position and size of the axis aligned bounding rectangle where dragging
|
||||
is allowed.
|
||||
|
||||
:attr:`drag_rectangle` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`drag_rect_x`, :attr:`drag_rect_y`, :attr:`drag_rect_width`,
|
||||
:attr:`drag_rect_height`) properties.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._drag_touch = None
|
||||
super(DragBehavior, self).__init__(**kwargs)
|
||||
|
||||
def _get_uid(self, prefix='sv'):
|
||||
return '{0}.{1}'.format(prefix, self.uid)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
xx, yy, w, h = self.drag_rectangle
|
||||
x, y = touch.pos
|
||||
if not self.collide_point(x, y):
|
||||
touch.ud[self._get_uid('svavoid')] = True
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
if self._drag_touch or ('button' in touch.profile and
|
||||
touch.button.startswith('scroll')) or\
|
||||
not ((xx < x <= xx + w) and (yy < y <= yy + h)):
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
|
||||
# no mouse scrolling, so the user is going to drag with this touch.
|
||||
self._drag_touch = touch
|
||||
uid = self._get_uid()
|
||||
touch.grab(self)
|
||||
touch.ud[uid] = {
|
||||
'mode': 'unknown',
|
||||
'dx': 0,
|
||||
'dy': 0}
|
||||
Clock.schedule_once(self._change_touch_mode,
|
||||
self.drag_timeout / 1000.)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud or\
|
||||
self._drag_touch is not touch:
|
||||
return super(DragBehavior, self).on_touch_move(touch) or\
|
||||
self._get_uid() in touch.ud
|
||||
if touch.grab_current is not self:
|
||||
return True
|
||||
|
||||
uid = self._get_uid()
|
||||
ud = touch.ud[uid]
|
||||
mode = ud['mode']
|
||||
if mode == 'unknown':
|
||||
ud['dx'] += abs(touch.dx)
|
||||
ud['dy'] += abs(touch.dy)
|
||||
if ud['dx'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
if ud['dy'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
ud['mode'] = mode
|
||||
if mode == 'drag':
|
||||
self.x += touch.dx
|
||||
self.y += touch.dy
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud:
|
||||
return super(DragBehavior, self).on_touch_up(touch)
|
||||
|
||||
if self._drag_touch and self in [x() for x in touch.grab_list]:
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
ud = touch.ud[self._get_uid()]
|
||||
if ud['mode'] == 'unknown':
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
|
||||
else:
|
||||
if self._drag_touch is not touch:
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
|
||||
def _do_touch_up(self, touch, *largs):
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
# don't forget about grab event!
|
||||
for x in touch.grab_list[:]:
|
||||
touch.grab_list.remove(x)
|
||||
x = x()
|
||||
if not x:
|
||||
continue
|
||||
touch.grab_current = x
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
touch.grab_current = None
|
||||
|
||||
def _change_touch_mode(self, *largs):
|
||||
if not self._drag_touch:
|
||||
return
|
||||
uid = self._get_uid()
|
||||
touch = self._drag_touch
|
||||
ud = touch.ud[uid]
|
||||
if ud['mode'] != 'unknown':
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.parent.to_widget)
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
touch.pop()
|
||||
return
|
||||
@@ -0,0 +1,140 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
Emacs Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.emacs.EmacsBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ allows you to add
|
||||
`Emacs <https://www.gnu.org/software/emacs/>`_ keyboard shortcuts for basic
|
||||
movement and editing to the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
The shortcuts currently available are listed below:
|
||||
|
||||
Emacs shortcuts
|
||||
---------------
|
||||
=============== ========================================================
|
||||
Shortcut Description
|
||||
--------------- --------------------------------------------------------
|
||||
Control + a Move cursor to the beginning of the line
|
||||
Control + e Move cursor to the end of the line
|
||||
Control + f Move cursor one character to the right
|
||||
Control + b Move cursor one character to the left
|
||||
Alt + f Move cursor to the end of the word to the right
|
||||
Alt + b Move cursor to the start of the word to the left
|
||||
Alt + Backspace Delete text left of the cursor to the beginning of word
|
||||
Alt + d Delete text right of the cursor to the end of the word
|
||||
Alt + w Copy selection
|
||||
Control + w Cut selection
|
||||
Control + y Paste selection
|
||||
=============== ========================================================
|
||||
|
||||
.. warning::
|
||||
If you have the :mod:`~kivy.modules.inspector` module enabled, the
|
||||
shortcut for opening the inspector (Control + e) conflicts with the
|
||||
Emacs shortcut to move to the end of the line (it will still move the
|
||||
cursor to the end of the line, but the inspector will open as well).
|
||||
'''
|
||||
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
|
||||
__all__ = ('EmacsBehavior', )
|
||||
|
||||
|
||||
class EmacsBehavior(object):
|
||||
'''
|
||||
A `mixin <https://en.wikipedia.org/wiki/Mixin>`_ that enables Emacs-style
|
||||
keyboard shortcuts for the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
Please see the :mod:`Emacs behaviors module <kivy.uix.behaviors.emacs>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
key_bindings = StringProperty('emacs')
|
||||
'''String name which determines the type of key bindings to use with the
|
||||
:class:`~kivy.uix.textinput.TextInput`. This allows Emacs key bindings to
|
||||
be enabled/disabled programmatically for widgets that inherit from
|
||||
:class:`EmacsBehavior`. If the value is not ``'emacs'``, Emacs bindings
|
||||
will be disabled. Use ``'default'`` for switching to the default key
|
||||
bindings of TextInput.
|
||||
|
||||
:attr:`key_bindings` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to ``'emacs'``.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EmacsBehavior, self).__init__(**kwargs)
|
||||
|
||||
self.bindings = {
|
||||
'ctrl': {
|
||||
'a': lambda: self.do_cursor_movement('cursor_home'),
|
||||
'e': lambda: self.do_cursor_movement('cursor_end'),
|
||||
'f': lambda: self.do_cursor_movement('cursor_right'),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left'),
|
||||
'w': lambda: self._cut(self.selection_text),
|
||||
'y': self.paste,
|
||||
},
|
||||
'alt': {
|
||||
'w': self.copy,
|
||||
'f': lambda: self.do_cursor_movement('cursor_right',
|
||||
control=True),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left',
|
||||
control=True),
|
||||
'd': self.delete_word_right,
|
||||
'\x08': self.delete_word_left, # alt + backspace
|
||||
},
|
||||
}
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
|
||||
key, key_str = keycode
|
||||
|
||||
# join the modifiers e.g. ['alt', 'ctrl']
|
||||
mod = '+'.join(modifiers) if modifiers else None
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if key in range(256) and self.key_bindings == 'emacs':
|
||||
if mod == 'ctrl' and chr(key) in self.bindings['ctrl'].keys():
|
||||
is_emacs_shortcut = True
|
||||
elif mod == 'alt' and chr(key) in self.bindings['alt'].keys():
|
||||
is_emacs_shortcut = True
|
||||
else: # e.g. ctrl+alt or alt+ctrl (alt-gr key)
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if is_emacs_shortcut:
|
||||
# Look up mod and key
|
||||
emacs_shortcut = self.bindings[mod][chr(key)]
|
||||
emacs_shortcut()
|
||||
else:
|
||||
super(EmacsBehavior, self).keyboard_on_key_down(window, keycode,
|
||||
text, modifiers)
|
||||
|
||||
def delete_word_right(self):
|
||||
'''Delete text right of the cursor to the end of the word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
start_cursor = self.cursor
|
||||
self.do_cursor_movement('cursor_right', control=True)
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[start_index:end_index]
|
||||
self._set_unredo_delsel(start_index, end_index, s, from_undo=False)
|
||||
self.text = self.text[:start_index] + self.text[end_index:]
|
||||
self._set_cursor(pos=start_cursor)
|
||||
|
||||
def delete_word_left(self):
|
||||
'''Delete text left of the cursor to the beginning of word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
self.do_cursor_movement('cursor_left', control=True)
|
||||
end_cursor = self.cursor
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[end_index:start_index]
|
||||
self._set_unredo_delsel(end_index, start_index, s, from_undo=False)
|
||||
self.text = self.text[:end_index] + self.text[start_index:]
|
||||
self._set_cursor(pos=end_cursor)
|
||||
@@ -0,0 +1,595 @@
|
||||
'''
|
||||
Focus Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.FocusBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
keyboard focus behavior. When combined with other
|
||||
FocusBehavior widgets it allows one to cycle focus among them by pressing
|
||||
tab. In addition, upon gaining focus, the instance will automatically
|
||||
receive keyboard input.
|
||||
|
||||
Focus, very different from selection, is intimately tied with the keyboard;
|
||||
each keyboard can focus on zero or one widgets, and each widget can only
|
||||
have the focus of one keyboard. However, multiple keyboards can focus
|
||||
simultaneously on different widgets. When escape is hit, the widget having
|
||||
the focus of that keyboard will de-focus.
|
||||
|
||||
Managing focus
|
||||
--------------
|
||||
|
||||
In essence, focus is implemented as a doubly linked list, where each
|
||||
node holds a (weak) reference to the instance before it and after it,
|
||||
as visualized when cycling through the nodes using tab (forward) or
|
||||
shift+tab (backward). If a previous or next widget is not specified,
|
||||
:attr:`focus_next` and :attr:`focus_previous` defaults to `None`. This
|
||||
means that the :attr:`~kivy.uix.widget.Widget.children` list and
|
||||
:attr:`parents <kivy.uix.widget.Widget.parent>` are
|
||||
walked to find the next focusable widget, unless :attr:`focus_next` or
|
||||
:attr:`focus_previous` is set to the `StopIteration` class, in which case
|
||||
focus stops there.
|
||||
|
||||
For example, to cycle focus between :class:`~kivy.uix.button.Button`
|
||||
elements of a :class:`~kivy.uix.gridlayout.GridLayout`::
|
||||
|
||||
class FocusButton(FocusBehavior, Button):
|
||||
pass
|
||||
|
||||
grid = GridLayout(cols=4)
|
||||
for i in range(40):
|
||||
grid.add_widget(FocusButton(text=str(i)))
|
||||
# clicking on a widget will activate focus, and tab can now be used
|
||||
# to cycle through
|
||||
|
||||
When using a software keyboard, typical on mobile and touch devices, the
|
||||
keyboard display behavior is determined by the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
|
||||
this property to ensure the focused widget is not covered or obscured by the
|
||||
keyboard.
|
||||
|
||||
Initializing focus
|
||||
------------------
|
||||
|
||||
Widgets needs to be visible before they can receive the focus. This means that
|
||||
setting their *focus* property to True before they are visible will have no
|
||||
effect. To initialize focus, you can use the 'on_parent' event::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.textinput import TextInput
|
||||
|
||||
class MyTextInput(TextInput):
|
||||
def on_parent(self, widget, parent):
|
||||
self.focus = True
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyTextInput()
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
If you are using a :class:`~kivy.uix.popup`, you can use the 'on_open' event.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
'''
|
||||
|
||||
__all__ = ('FocusBehavior', )
|
||||
|
||||
from kivy.properties import OptionProperty, ObjectProperty, BooleanProperty, \
|
||||
AliasProperty
|
||||
from kivy.config import Config
|
||||
from kivy.base import EventLoop
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_is_desktop = False
|
||||
_keyboard_mode = 'system'
|
||||
if Config:
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
_keyboard_mode = Config.get('kivy', 'keyboard_mode')
|
||||
|
||||
|
||||
class FocusBehavior(object):
|
||||
'''Provides keyboard focus behavior. When combined with other
|
||||
FocusBehavior widgets it allows one to cycle focus among them by pressing
|
||||
tab. Please see the
|
||||
:mod:`focus behavior module documentation <kivy.uix.behaviors.focus>`
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
'''
|
||||
|
||||
_requested_keyboard = False
|
||||
_keyboard = ObjectProperty(None, allownone=True)
|
||||
_keyboards = {}
|
||||
|
||||
ignored_touch = []
|
||||
'''A list of touches that should not be used to defocus. After on_touch_up,
|
||||
every touch that is not in :attr:`ignored_touch` will defocus all the
|
||||
focused widgets if the config keyboard mode is not multi. Touches on
|
||||
focusable widgets that were used to focus are automatically added here.
|
||||
|
||||
Example usage::
|
||||
|
||||
class Unfocusable(Widget):
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
FocusBehavior.ignored_touch.append(touch)
|
||||
|
||||
Notice that you need to access this as a class, not an instance variable.
|
||||
'''
|
||||
|
||||
def _set_keyboard(self, value):
|
||||
focus = self.focus
|
||||
keyboard = self._keyboard
|
||||
keyboards = FocusBehavior._keyboards
|
||||
if keyboard:
|
||||
self.focus = False # this'll unbind
|
||||
if self._keyboard: # remove assigned keyboard from dict
|
||||
del keyboards[keyboard]
|
||||
if value and value not in keyboards:
|
||||
keyboards[value] = None
|
||||
self._keyboard = value
|
||||
self.focus = focus
|
||||
|
||||
def _get_keyboard(self):
|
||||
return self._keyboard
|
||||
keyboard = AliasProperty(_get_keyboard, _set_keyboard,
|
||||
bind=('_keyboard', ))
|
||||
'''The keyboard to bind to (or bound to the widget) when focused.
|
||||
|
||||
When None, a keyboard is requested and released whenever the widget comes
|
||||
into and out of focus. If not None, it must be a keyboard, which gets
|
||||
bound and unbound from the widget whenever it's in or out of focus. It is
|
||||
useful only when more than one keyboard is available, so it is recommended
|
||||
to be set to None when only one keyboard is available.
|
||||
|
||||
If more than one keyboard is available, whenever an instance gets focused
|
||||
a new keyboard will be requested if None. Unless the other instances lose
|
||||
focus (e.g. if tab was used), a new keyboard will appear. When this is
|
||||
undesired, the keyboard property can be used. For example, if there are
|
||||
two users with two keyboards, then each keyboard can be assigned to
|
||||
different groups of instances of FocusBehavior, ensuring that within
|
||||
each group, only one FocusBehavior will have focus, and will receive input
|
||||
from the correct keyboard. See `keyboard_mode` in :mod:`~kivy.config` for
|
||||
more information on the keyboard modes.
|
||||
|
||||
**Keyboard and focus behavior**
|
||||
|
||||
When using the keyboard, there are some important default behaviors you
|
||||
should keep in mind.
|
||||
|
||||
* When Config's `keyboard_mode` is multi, each new touch is considered
|
||||
a touch by a different user and will set the focus (if clicked on a
|
||||
focusable) with a new keyboard. Already focused elements will not lose
|
||||
their focus (even if an unfocusable widget is touched).
|
||||
|
||||
* If the keyboard property is set, that keyboard will be used when the
|
||||
instance gets focused. If widgets with different keyboards are linked
|
||||
through :attr:`focus_next` and :attr:`focus_previous`, then as they are
|
||||
tabbed through, different keyboards will become active. Therefore,
|
||||
typically it's undesirable to link instances which are assigned
|
||||
different keyboards.
|
||||
|
||||
* When a widget has focus, setting its keyboard to None will remove its
|
||||
keyboard, but the widget will then immediately try to get
|
||||
another keyboard. In order to remove its keyboard, rather set its
|
||||
:attr:`focus` to False.
|
||||
|
||||
* When using a software keyboard, typical on mobile and touch devices, the
|
||||
keyboard display behavior is determined by the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
|
||||
this property to ensure the focused widget is not covered or obscured.
|
||||
|
||||
:attr:`keyboard` is an :class:`~kivy.properties.AliasProperty` and defaults
|
||||
to None.
|
||||
|
||||
.. warning:
|
||||
|
||||
When assigning a keyboard, the keyboard must not be released while
|
||||
it is still assigned to an instance. Similarly, the keyboard created
|
||||
by the instance on focus and assigned to :attr:`keyboard` if None,
|
||||
will be released by the instance when the instance loses focus.
|
||||
Therefore, it is not safe to assign this keyboard to another instance's
|
||||
:attr:`keyboard`.
|
||||
'''
|
||||
|
||||
is_focusable = BooleanProperty(_is_desktop)
|
||||
'''Whether the instance can become focused. If focused, it'll lose focus
|
||||
when set to False.
|
||||
|
||||
:attr:`is_focusable` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True on a desktop (i.e. `desktop` is True in
|
||||
:mod:`~kivy.config`), False otherwise.
|
||||
'''
|
||||
|
||||
focus = BooleanProperty(False)
|
||||
'''Whether the instance currently has focus.
|
||||
|
||||
Setting it to True will bind to and/or request the keyboard, and input
|
||||
will be forwarded to the instance. Setting it to False will unbind
|
||||
and/or release the keyboard. For a given keyboard, only one widget can
|
||||
have its focus, so focusing one will automatically unfocus the other
|
||||
instance holding its focus.
|
||||
|
||||
When using a software keyboard, please refer to the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property to determine
|
||||
how the keyboard display is handled.
|
||||
|
||||
:attr:`focus` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
focused = focus
|
||||
'''An alias of :attr:`focus`.
|
||||
|
||||
:attr:`focused` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
|
||||
.. warning::
|
||||
:attr:`focused` is an alias of :attr:`focus` and will be removed in
|
||||
2.0.0.
|
||||
'''
|
||||
|
||||
keyboard_suggestions = BooleanProperty(True)
|
||||
'''If True provides auto suggestions on top of keyboard.
|
||||
This will only work if :attr:`input_type` is set to `text`, `url`, `mail` or
|
||||
`address`.
|
||||
|
||||
.. warning::
|
||||
On Android, `keyboard_suggestions` relies on
|
||||
`InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS` to work, but some keyboards
|
||||
just ignore this flag. If you want to disable suggestions at all on
|
||||
Android, you can set `input_type` to `null`, which will request the
|
||||
input method to run in a limited "generate key events" mode.
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
:attr:`keyboard_suggestions` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True
|
||||
'''
|
||||
|
||||
def _set_on_focus_next(self, instance, value):
|
||||
'''If changing focus, ensure your code does not create an infinite loop.
|
||||
eg:
|
||||
```python
|
||||
widget.focus_next = widget
|
||||
widget.focus_previous = widget
|
||||
```
|
||||
'''
|
||||
next_ = self._old_focus_next
|
||||
if next_ is value: # prevent infinite loop
|
||||
return
|
||||
|
||||
if isinstance(next_, FocusBehavior):
|
||||
next_.focus_previous = None
|
||||
self._old_focus_next = value
|
||||
if value is None or value is StopIteration:
|
||||
return
|
||||
if not isinstance(value, FocusBehavior):
|
||||
raise ValueError('focus_next accepts only objects based on'
|
||||
' FocusBehavior, or the `StopIteration` class.')
|
||||
value.focus_previous = self
|
||||
|
||||
focus_next = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`FocusBehavior` instance to acquire focus when
|
||||
tab is pressed and this instance has focus, if not `None` or
|
||||
`StopIteration`.
|
||||
|
||||
When tab is pressed, focus cycles through all the :class:`FocusBehavior`
|
||||
widgets that are linked through :attr:`focus_next` and are focusable. If
|
||||
:attr:`focus_next` is `None`, it instead walks the children lists to find
|
||||
the next focusable widget. Finally, if :attr:`focus_next` is
|
||||
the `StopIteration` class, focus won't move forward, but end here.
|
||||
|
||||
.. note:
|
||||
|
||||
Setting :attr:`focus_next` automatically sets :attr:`focus_previous`
|
||||
of the other instance to point to this instance, if not None or
|
||||
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
|
||||
also sets the :attr:`focus_previous` property of the instance
|
||||
previously in :attr:`focus_next` to `None`. Therefore, it is only
|
||||
required to set one of the :attr:`focus_previous` or
|
||||
:attr:`focus_next` links since the other side will be set
|
||||
automatically.
|
||||
|
||||
:attr:`focus_next` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
def _set_on_focus_previous(self, instance, value):
|
||||
prev = self._old_focus_previous
|
||||
if prev is value:
|
||||
return
|
||||
|
||||
if isinstance(prev, FocusBehavior):
|
||||
prev.focus_next = None
|
||||
self._old_focus_previous = value
|
||||
if value is None or value is StopIteration:
|
||||
return
|
||||
if not isinstance(value, FocusBehavior):
|
||||
raise ValueError('focus_previous accepts only objects based'
|
||||
'on FocusBehavior, or the `StopIteration` class.')
|
||||
value.focus_next = self
|
||||
|
||||
focus_previous = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`FocusBehavior` instance to acquire focus when
|
||||
shift+tab is pressed on this instance, if not None or `StopIteration`.
|
||||
|
||||
When shift+tab is pressed, focus cycles through all the
|
||||
:class:`FocusBehavior` widgets that are linked through
|
||||
:attr:`focus_previous` and are focusable. If :attr:`focus_previous` is
|
||||
`None`, it instead walks the children tree to find the
|
||||
previous focusable widget. Finally, if :attr:`focus_previous` is the
|
||||
`StopIteration` class, focus won't move backward, but end here.
|
||||
|
||||
.. note:
|
||||
|
||||
Setting :attr:`focus_previous` automatically sets :attr:`focus_next`
|
||||
of the other instance to point to this instance, if not None or
|
||||
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
|
||||
also sets the :attr:`focus_next` property of the instance previously in
|
||||
:attr:`focus_previous` to `None`. Therefore, it is only required
|
||||
to set one of the :attr:`focus_previous` or :attr:`focus_next`
|
||||
links since the other side will be set automatically.
|
||||
|
||||
:attr:`focus_previous` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
keyboard_mode = OptionProperty('auto', options=('auto', 'managed'))
|
||||
'''Determines how the keyboard visibility should be managed. 'auto' will
|
||||
result in the standard behavior of showing/hiding on focus. 'managed'
|
||||
requires setting the keyboard visibility manually, or calling the helper
|
||||
functions :meth:`show_keyboard` and :meth:`hide_keyboard`.
|
||||
|
||||
:attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and
|
||||
defaults to 'auto'. Can be one of 'auto' or 'managed'.
|
||||
'''
|
||||
|
||||
input_type = OptionProperty('null', options=('null', 'text', 'number',
|
||||
'url', 'mail', 'datetime',
|
||||
'tel', 'address'))
|
||||
'''The kind of input keyboard to request.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. versionchanged:: 2.1.0
|
||||
Changed default value from `text` to `null`. Added `null` to options.
|
||||
|
||||
.. warning::
|
||||
As the default value has been changed, you may need to adjust
|
||||
`input_type` in your code.
|
||||
|
||||
:attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and
|
||||
defaults to 'null'. Can be one of 'null', 'text', 'number', 'url', 'mail',
|
||||
'datetime', 'tel' or 'address'.
|
||||
'''
|
||||
|
||||
unfocus_on_touch = BooleanProperty(_keyboard_mode not in
|
||||
('multi', 'systemandmulti'))
|
||||
'''Whether a instance should lose focus when clicked outside the instance.
|
||||
|
||||
When a user clicks on a widget that is focus aware and shares the same
|
||||
keyboard as this widget (which in the case with only one keyboard),
|
||||
then as the other widgets gain focus, this widget loses focus. In addition
|
||||
to that, if this property is `True`, clicking on any widget other than this
|
||||
widget, will remove focus from this widget.
|
||||
|
||||
:attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False` if the `keyboard_mode` in :attr:`~kivy.config.Config`
|
||||
is `'multi'` or `'systemandmulti'`, otherwise it defaults to `True`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._old_focus_next = None
|
||||
self._old_focus_previous = None
|
||||
super(FocusBehavior, self).__init__(**kwargs)
|
||||
|
||||
self._keyboard_mode = _keyboard_mode
|
||||
fbind = self.fbind
|
||||
fbind('focus', self._on_focus)
|
||||
fbind('disabled', self._on_focusable)
|
||||
fbind('is_focusable', self._on_focusable)
|
||||
fbind('focus_next', self._set_on_focus_next)
|
||||
fbind('focus_previous', self._set_on_focus_previous)
|
||||
|
||||
def _on_focusable(self, instance, value):
|
||||
if self.disabled or not self.is_focusable:
|
||||
self.focus = False
|
||||
|
||||
def _on_focus(self, instance, value, *largs):
|
||||
if self.keyboard_mode == 'auto':
|
||||
if value:
|
||||
self._bind_keyboard()
|
||||
else:
|
||||
self._unbind_keyboard()
|
||||
|
||||
def _ensure_keyboard(self):
|
||||
if self._keyboard is None:
|
||||
self._requested_keyboard = True
|
||||
keyboard = self._keyboard = EventLoop.window.request_keyboard(
|
||||
self._keyboard_released,
|
||||
self,
|
||||
input_type=self.input_type,
|
||||
keyboard_suggestions=self.keyboard_suggestions,
|
||||
)
|
||||
keyboards = FocusBehavior._keyboards
|
||||
if keyboard not in keyboards:
|
||||
keyboards[keyboard] = None
|
||||
|
||||
def _bind_keyboard(self):
|
||||
self._ensure_keyboard()
|
||||
keyboard = self._keyboard
|
||||
|
||||
if not keyboard or self.disabled or not self.is_focusable:
|
||||
self.focus = False
|
||||
return
|
||||
keyboards = FocusBehavior._keyboards
|
||||
old_focus = keyboards[keyboard] # keyboard should be in dict
|
||||
if old_focus:
|
||||
old_focus.focus = False
|
||||
# keyboard shouldn't have been released here, see keyboard warning
|
||||
keyboards[keyboard] = self
|
||||
keyboard.bind(on_key_down=self.keyboard_on_key_down,
|
||||
on_key_up=self.keyboard_on_key_up,
|
||||
on_textinput=self.keyboard_on_textinput)
|
||||
|
||||
def _unbind_keyboard(self):
|
||||
keyboard = self._keyboard
|
||||
if keyboard:
|
||||
keyboard.unbind(on_key_down=self.keyboard_on_key_down,
|
||||
on_key_up=self.keyboard_on_key_up,
|
||||
on_textinput=self.keyboard_on_textinput)
|
||||
if self._requested_keyboard:
|
||||
keyboard.release()
|
||||
self._keyboard = None
|
||||
self._requested_keyboard = False
|
||||
del FocusBehavior._keyboards[keyboard]
|
||||
else:
|
||||
FocusBehavior._keyboards[keyboard] = None
|
||||
|
||||
def keyboard_on_textinput(self, window, text):
|
||||
pass
|
||||
|
||||
def _keyboard_released(self):
|
||||
self.focus = False
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
if (not self.disabled and self.is_focusable and
|
||||
('button' not in touch.profile or
|
||||
not touch.button.startswith('scroll'))):
|
||||
self.focus = True
|
||||
FocusBehavior.ignored_touch.append(touch)
|
||||
return super(FocusBehavior, self).on_touch_down(touch)
|
||||
|
||||
@staticmethod
|
||||
def _handle_post_on_touch_up(touch):
|
||||
''' Called by window after each touch has finished.
|
||||
'''
|
||||
touches = FocusBehavior.ignored_touch
|
||||
if touch in touches:
|
||||
touches.remove(touch)
|
||||
return
|
||||
if 'button' in touch.profile and touch.button in\
|
||||
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
|
||||
return
|
||||
for focusable in list(FocusBehavior._keyboards.values()):
|
||||
if focusable is None or not focusable.unfocus_on_touch:
|
||||
continue
|
||||
focusable.focus = False
|
||||
|
||||
def _get_focus_next(self, focus_dir):
|
||||
current = self
|
||||
walk_tree = 'walk' if focus_dir == 'focus_next' else 'walk_reverse'
|
||||
|
||||
while 1:
|
||||
# if we hit a focusable, walk through focus_xxx
|
||||
while getattr(current, focus_dir) is not None:
|
||||
current = getattr(current, focus_dir)
|
||||
if current is self or current is StopIteration:
|
||||
return None # make sure we don't loop forever
|
||||
if current.is_focusable and not current.disabled:
|
||||
return current
|
||||
|
||||
# hit unfocusable, walk widget tree
|
||||
itr = getattr(current, walk_tree)(loopback=True)
|
||||
if focus_dir == 'focus_next':
|
||||
next(itr) # current is returned first when walking forward
|
||||
for current in itr:
|
||||
if isinstance(current, FocusBehavior):
|
||||
break
|
||||
# why did we stop
|
||||
if isinstance(current, FocusBehavior):
|
||||
if current is self:
|
||||
return None
|
||||
if current.is_focusable and not current.disabled:
|
||||
return current
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_focus_next(self):
|
||||
'''Returns the next focusable widget using either :attr:`focus_next`
|
||||
or the :attr:`children` similar to the order when tabbing forwards
|
||||
with the ``tab`` key.
|
||||
'''
|
||||
return self._get_focus_next('focus_next')
|
||||
|
||||
def get_focus_previous(self):
|
||||
'''Returns the previous focusable widget using either
|
||||
:attr:`focus_previous` or the :attr:`children` similar to the
|
||||
order when the ``tab`` + ``shift`` keys are triggered together.
|
||||
'''
|
||||
return self._get_focus_next('focus_previous')
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
'''The method bound to the keyboard when the instance has focus.
|
||||
|
||||
When the instance becomes focused, this method is bound to the
|
||||
keyboard and will be called for every input press. The parameters are
|
||||
the same as :meth:`kivy.core.window.WindowBase.on_key_down`.
|
||||
|
||||
When overwriting the method in the derived widget, super should be
|
||||
called to enable tab cycling. If the derived widget wishes to use tab
|
||||
for its own purposes, it can call super after it has processed the
|
||||
character (if it does not wish to consume the tab).
|
||||
|
||||
Similar to other keyboard functions, it should return True if the
|
||||
key was consumed.
|
||||
'''
|
||||
if keycode[1] == 'tab': # deal with cycle
|
||||
modifiers = set(modifiers)
|
||||
if {'ctrl', 'alt', 'meta', 'super', 'compose'} & modifiers:
|
||||
return False
|
||||
if 'shift' in modifiers:
|
||||
next = self.get_focus_previous()
|
||||
else:
|
||||
next = self.get_focus_next()
|
||||
if next:
|
||||
self.focus = False
|
||||
|
||||
next.focus = True
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
'''The method bound to the keyboard when the instance has focus.
|
||||
|
||||
When the instance becomes focused, this method is bound to the
|
||||
keyboard and will be called for every input release. The parameters are
|
||||
the same as :meth:`kivy.core.window.WindowBase.on_key_up`.
|
||||
|
||||
When overwriting the method in the derived widget, super should be
|
||||
called to enable de-focusing on escape. If the derived widget wishes
|
||||
to use escape for its own purposes, it can call super after it has
|
||||
processed the character (if it does not wish to consume the escape).
|
||||
|
||||
See :meth:`keyboard_on_key_down`
|
||||
'''
|
||||
if keycode[1] == 'escape':
|
||||
self.focus = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_keyboard(self):
|
||||
'''
|
||||
Convenience function to show the keyboard in managed mode.
|
||||
'''
|
||||
if self.keyboard_mode == 'managed':
|
||||
self._bind_keyboard()
|
||||
|
||||
def hide_keyboard(self):
|
||||
'''
|
||||
Convenience function to hide the keyboard in managed mode.
|
||||
'''
|
||||
if self.keyboard_mode == 'managed':
|
||||
self._unbind_keyboard()
|
||||
@@ -0,0 +1,590 @@
|
||||
'''
|
||||
Kivy Namespaces
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
The :class:`KNSpaceBehavior` `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
class provides namespace functionality for Kivy objects. It allows kivy objects
|
||||
to be named and then accessed using namespaces.
|
||||
|
||||
:class:`KNSpace` instances are the namespaces that store the named objects
|
||||
in Kivy :class:`~kivy.properties.ObjectProperty` instances.
|
||||
In addition, when inheriting from :class:`KNSpaceBehavior`, if the derived
|
||||
object is named, the name will automatically be added to the associated
|
||||
namespace and will point to a :attr:`~kivy.uix.widget.proxy_ref` of the
|
||||
derived object.
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
|
||||
By default, there's only a single namespace: the :attr:`knspace` namespace. The
|
||||
simplest example is adding a widget to the namespace:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.uix.behaviors.knspace import knspace
|
||||
widget = Widget()
|
||||
knspace.my_widget = widget
|
||||
|
||||
This adds a kivy :class:`~kivy.properties.ObjectProperty` with `rebind=True`
|
||||
and `allownone=True` to the :attr:`knspace` namespace with a property name
|
||||
`my_widget`. And the property now also points to this widget.
|
||||
|
||||
This can be done automatically with:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knsname: 'my_widget'
|
||||
|
||||
Now, `knspace.my_widget` will point to that widget.
|
||||
|
||||
When one creates a second widget with the same name, the namespace will
|
||||
also change to point to the new widget. E.g.:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget
|
||||
widget2 = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget2
|
||||
|
||||
Setting the namespace
|
||||
---------------------
|
||||
|
||||
One can also create ones own namespace rather than using the default
|
||||
:attr:`knspace` by directly setting :attr:`KNSpaceBehavior.knspace`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
my_new_namespace = KNSpace()
|
||||
widget.knspace = my_new_namespace
|
||||
|
||||
Initially, `my_widget` is added to the default namespace, but when the widget's
|
||||
namespace is changed to `my_new_namespace`, the reference to `my_widget` is
|
||||
moved to that namespace. We could have also of course first set the namespace
|
||||
to `my_new_namespace` and then have named the widget `my_widget`, thereby
|
||||
avoiding the initial assignment to the default namespace.
|
||||
|
||||
Similarly, in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knspace: KNSpace()
|
||||
knsname: 'my_widget'
|
||||
|
||||
Inheriting the namespace
|
||||
------------------------
|
||||
|
||||
In the previous example, we directly set the namespace we wished to use.
|
||||
In the following example, we inherit it from the parent, so we only have to set
|
||||
it once:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
<MyLabel@KNSpaceBehavior+Label>
|
||||
|
||||
<MyComplexWidget@MyWidget>:
|
||||
knsname: 'my_complex'
|
||||
MyLabel:
|
||||
knsname: 'label1'
|
||||
MyLabel:
|
||||
knsname: 'label2'
|
||||
|
||||
Then, we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
widget.knspace = new_knspace
|
||||
|
||||
The rule is that if no knspace has been assigned to a widget, it looks for a
|
||||
namespace in its parent and parent's parent and so on until it find one to
|
||||
use. If none are found, it uses the default :attr:`knspace`.
|
||||
|
||||
When `MyComplexWidget` is created, it still used the default namespace.
|
||||
However, when we assigned the root widget its new namespace, all its
|
||||
children switched to using that new namespace as well. So `new_knspace` now
|
||||
contains `label1` and `label2` as well as `my_complex`.
|
||||
|
||||
If we had first done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
knspace.label1.knspace = knspace
|
||||
widget.knspace = new_knspace
|
||||
|
||||
Then `label1` would remain stored in the default :attr:`knspace` since it was
|
||||
directly set, but `label2` and `my_complex` would still be added to the new
|
||||
namespace.
|
||||
|
||||
One can customize the attribute used to search the parent tree by changing
|
||||
:attr:`KNSpaceBehavior.knspace_key`. If the desired knspace is not reachable
|
||||
through a widgets parent tree, e.g. in a popup that is not a widget's child,
|
||||
:attr:`KNSpaceBehavior.knspace_key` can be used to establish a different
|
||||
search order.
|
||||
|
||||
Accessing the namespace
|
||||
-----------------------
|
||||
|
||||
As seen in the previous example, if not directly assigned, the namespace is
|
||||
found by searching the parent tree. Consequently, if a namespace was assigned
|
||||
further up the parent tree, all its children and below could access that
|
||||
namespace through their :attr:`KNSpaceBehavior.knspace` property.
|
||||
|
||||
This allows the creation of multiple widgets with identically given names
|
||||
if each root widget instance is assigned a new namespace. For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Now, when we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
knspace1, knspace2 = KNSpace(), KNSpace()
|
||||
composite1 = MyCompositeWidget()
|
||||
composite1.knspace = knspace1
|
||||
|
||||
composite2 = MyCompositeWidget()
|
||||
composite2.knspace = knspace2
|
||||
|
||||
knspace1.pretty = "Here's the ladder, now fix the roof!"
|
||||
knspace2.pretty = "Get that raccoon off me!"
|
||||
|
||||
Because each of the `MyCompositeWidget` instances have a different namespace
|
||||
their children also use different namespaces. Consequently, the
|
||||
pretty and complex widgets of each instance will have different text.
|
||||
|
||||
Further, because both the namespace :class:`~kivy.properties.ObjectProperty`
|
||||
references, and :attr:`KNSpaceBehavior.knspace` have `rebind=True`, the
|
||||
text of the `MyComplexWidget` label is rebound to match the text of
|
||||
`MyPrettyWidget` when either the root's namespace changes or when the
|
||||
`root.knspace.pretty` property changes, as expected.
|
||||
|
||||
Forking a namespace
|
||||
-------------------
|
||||
|
||||
Forking a namespace provides the opportunity to create a new namespace
|
||||
from a parent namespace so that the forked namespace will contain everything
|
||||
in the origin namespace, but the origin namespace will not have access to
|
||||
anything added to the forked namespace.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
child = knspace.fork()
|
||||
grandchild = child.fork()
|
||||
|
||||
child.label = Label()
|
||||
grandchild.button = Button()
|
||||
|
||||
Now label is accessible by both child and grandchild, but not by knspace. And
|
||||
button is only accessible by the grandchild but not by the child or by knspace.
|
||||
Finally, doing `grandchild.label = Label()` will leave `grandchild.label`
|
||||
and `child.label` pointing to different labels.
|
||||
|
||||
A motivating example is the example from above:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
knspace: 'fork'
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Notice the addition of `knspace: 'fork'`. This is identical to doing
|
||||
`knspace: self.knspace.fork()`. However, doing that would lead to infinite
|
||||
recursion as that kv rule would be executed recursively because `self.knspace`
|
||||
will keep on changing. However, allowing `knspace: 'fork'` cirumvents that.
|
||||
See :attr:`KNSpaceBehavior.knspace`.
|
||||
|
||||
Now, having forked, we just need to do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
composite1 = MyCompositeWidget()
|
||||
composite2 = MyCompositeWidget()
|
||||
|
||||
composite1.knspace.pretty = "Here's the ladder, now fix the roof!"
|
||||
composite2.knspace.pretty = "Get that raccoon off me!"
|
||||
|
||||
Since by forking we automatically created a unique namespace for each
|
||||
`MyCompositeWidget` instance.
|
||||
'''
|
||||
|
||||
__all__ = ('KNSpace', 'KNSpaceBehavior', 'knspace')
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.properties import StringProperty, ObjectProperty, AliasProperty
|
||||
from kivy.context import register_context
|
||||
|
||||
|
||||
class KNSpace(EventDispatcher):
|
||||
'''Each :class:`KNSpace` instance is a namespace that stores the named Kivy
|
||||
objects associated with this namespace. Each named object is
|
||||
stored as the value of a Kivy :class:`~kivy.properties.ObjectProperty` of
|
||||
this instance whose property name is the object's given name. Both `rebind`
|
||||
and `allownone` are set to `True` for the property.
|
||||
|
||||
See :attr:`KNSpaceBehavior.knspace` for details on how a namespace is
|
||||
associated with a named object.
|
||||
|
||||
When storing an object in the namespace, the object's `proxy_ref` is
|
||||
stored if the object has such an attribute.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`parent`: (internal) A :class:`KNSpace` instance or None.
|
||||
If specified, it's a parent namespace, in which case, the current
|
||||
namespace will have in its namespace all its named objects
|
||||
as well as the named objects of its parent and parent's parent
|
||||
etc. See :meth:`fork` for more details.
|
||||
'''
|
||||
|
||||
parent = None
|
||||
'''(internal) The parent namespace instance, :class:`KNSpace`, or None. See
|
||||
:meth:`fork`.
|
||||
'''
|
||||
__has_applied = None
|
||||
|
||||
keep_ref = False
|
||||
'''Whether a direct reference should be kept to the stored objects.
|
||||
If ``True``, we use the direct object, otherwise we use
|
||||
:attr:`~kivy.uix.widget.proxy_ref` when present.
|
||||
|
||||
Defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, parent=None, keep_ref=False, **kwargs):
|
||||
self.keep_ref = keep_ref
|
||||
super(KNSpace, self).__init__(**kwargs)
|
||||
self.parent = parent
|
||||
self.__has_applied = set(self.properties().keys())
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
has_applied = self.__has_applied
|
||||
if prop is None:
|
||||
if hasattr(self, name):
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
self.apply_property(
|
||||
**{name:
|
||||
ObjectProperty(None, rebind=True, allownone=True)}
|
||||
)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
has_applied.add(name)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
elif name not in has_applied:
|
||||
self.apply_property(**{name: prop})
|
||||
has_applied.add(name)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in super(KNSpace, self).__getattribute__('__dict__'):
|
||||
return super(KNSpace, self).__getattribute__(name)
|
||||
|
||||
try:
|
||||
value = super(KNSpace, self).__getattribute__(name)
|
||||
except AttributeError:
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
raise AttributeError(name)
|
||||
return getattr(parent, name)
|
||||
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return getattr(parent, name) # if parent doesn't have it
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def property(self, name, quiet=False):
|
||||
# needs to overwrite EventDispatcher.property so kv lang will work
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
if prop is not None:
|
||||
return prop
|
||||
|
||||
prop = ObjectProperty(None, rebind=True, allownone=True)
|
||||
self.apply_property(**{name: prop})
|
||||
self.__has_applied.add(name)
|
||||
return prop
|
||||
|
||||
def fork(self):
|
||||
'''Returns a new :class:`KNSpace` instance which will have access to
|
||||
all the named objects in the current namespace but will also have a
|
||||
namespace of its own that is unique to it.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
forked_knspace1 = knspace.fork()
|
||||
forked_knspace2 = knspace.fork()
|
||||
|
||||
Now, any names added to `knspace` will be accessible by the
|
||||
`forked_knspace1` and `forked_knspace2` namespaces by the normal means.
|
||||
However, any names added to `forked_knspace1` will not be accessible
|
||||
from `knspace` or `forked_knspace2`. Similar for `forked_knspace2`.
|
||||
'''
|
||||
return KNSpace(parent=self)
|
||||
|
||||
|
||||
class KNSpaceBehavior(object):
|
||||
'''Inheriting from this class allows naming of the inherited objects, which
|
||||
are then added to the associated namespace :attr:`knspace` and accessible
|
||||
through it.
|
||||
|
||||
Please see the :mod:`knspace behaviors module <kivy.uix.behaviors.knspace>`
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
_knspace = ObjectProperty(None, allownone=True)
|
||||
_knsname = StringProperty('')
|
||||
__last_knspace = None
|
||||
__callbacks = None
|
||||
|
||||
def __init__(self, knspace=None, **kwargs):
|
||||
self.knspace = knspace
|
||||
super(KNSpaceBehavior, self).__init__(**kwargs)
|
||||
|
||||
def __knspace_clear_callbacks(self, *largs):
|
||||
for obj, name, uid in self.__callbacks:
|
||||
obj.unbind_uid(name, uid)
|
||||
last = self.__last_knspace
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
assert self._knspace is None
|
||||
assert last
|
||||
|
||||
new = self.__set_parent_knspace()
|
||||
if new is last:
|
||||
return
|
||||
self.property('_knspace').dispatch(self)
|
||||
|
||||
name = self.knsname
|
||||
if not name:
|
||||
return
|
||||
|
||||
if getattr(last, name) == self:
|
||||
setattr(last, name, None)
|
||||
|
||||
if new:
|
||||
setattr(new, name, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
|
||||
def __set_parent_knspace(self):
|
||||
callbacks = self.__callbacks = []
|
||||
fbind = self.fbind
|
||||
append = callbacks.append
|
||||
parent_key = self.knspace_key
|
||||
clear = self.__knspace_clear_callbacks
|
||||
|
||||
append((self, 'knspace_key', fbind('knspace_key', clear)))
|
||||
if not parent_key:
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
append((self, parent_key, fbind(parent_key, clear)))
|
||||
parent = getattr(self, parent_key, None)
|
||||
while parent is not None:
|
||||
fbind = parent.fbind
|
||||
|
||||
parent_knspace = getattr(parent, 'knspace', 0)
|
||||
if parent_knspace != 0:
|
||||
append((parent, 'knspace', fbind('knspace', clear)))
|
||||
self.__last_knspace = parent_knspace
|
||||
return parent_knspace
|
||||
|
||||
append((parent, parent_key, fbind(parent_key, clear)))
|
||||
new_parent = getattr(parent, parent_key, None)
|
||||
if new_parent is parent:
|
||||
break
|
||||
parent = new_parent
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
def _get_knspace(self):
|
||||
_knspace = self._knspace
|
||||
if _knspace is not None:
|
||||
return _knspace
|
||||
|
||||
if self.__callbacks is not None:
|
||||
return self.__last_knspace
|
||||
|
||||
# we only get here if we never accessed our knspace
|
||||
return self.__set_parent_knspace()
|
||||
|
||||
def _set_knspace(self, value):
|
||||
if value is self._knspace:
|
||||
return
|
||||
|
||||
knspace = self._knspace or self.__last_knspace
|
||||
name = self.knsname
|
||||
if name and knspace and getattr(knspace, name) == self:
|
||||
setattr(knspace, name, None) # reset old namespace
|
||||
|
||||
if value == 'fork':
|
||||
if not knspace:
|
||||
knspace = self.knspace # get parents in case we haven't before
|
||||
if knspace:
|
||||
value = knspace.fork()
|
||||
else:
|
||||
raise ValueError('Cannot fork with no namespace')
|
||||
|
||||
for obj, prop_name, uid in self.__callbacks or []:
|
||||
obj.unbind_uid(prop_name, uid)
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
if name:
|
||||
if value is None: # if None, first update the recursive knspace
|
||||
knspace = self.__set_parent_knspace()
|
||||
if knspace:
|
||||
setattr(knspace, name, self)
|
||||
self._knspace = None # cause a kv trigger
|
||||
else:
|
||||
setattr(value, name, self)
|
||||
knspace = self._knspace = value
|
||||
|
||||
if not knspace:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
else:
|
||||
if value is None:
|
||||
self.__set_parent_knspace() # update before trigger below
|
||||
self._knspace = value
|
||||
|
||||
knspace = AliasProperty(
|
||||
_get_knspace, _set_knspace, bind=('_knspace', ), cache=False,
|
||||
rebind=True, allownone=True)
|
||||
'''The namespace instance, :class:`KNSpace`, associated with this widget.
|
||||
The :attr:`knspace` namespace stores this widget when naming this widget
|
||||
with :attr:`knsname`.
|
||||
|
||||
If the namespace has been set with a :class:`KNSpace` instance, e.g. with
|
||||
`self.knspace = KNSpace()`, then that instance is returned (setting with
|
||||
`None` doesn't count). Otherwise, if :attr:`knspace_key` is not None, we
|
||||
look for a namespace to use in the object that is stored in the property
|
||||
named :attr:`knspace_key`, of this instance. I.e.
|
||||
`object = getattr(self, self.knspace_key)`.
|
||||
|
||||
If that object has a knspace property, then we return its value. Otherwise,
|
||||
we go further up, e.g. with `getattr(object, self.knspace_key)` and look
|
||||
for its `knspace` property.
|
||||
|
||||
Finally, if we reach a value of `None`, or :attr:`knspace_key` was `None`,
|
||||
the default :attr:`~kivy.uix.behaviors.knspace.knspace` namespace is
|
||||
returned.
|
||||
|
||||
If :attr:`knspace` is set to the string `'fork'`, the current namespace
|
||||
in :attr:`knspace` will be forked with :meth:`KNSpace.fork` and the
|
||||
resulting namespace will be assigned to this instance's :attr:`knspace`.
|
||||
See the module examples for a motivating example.
|
||||
|
||||
Both `rebind` and `allownone` are `True`.
|
||||
'''
|
||||
|
||||
knspace_key = StringProperty('parent', allownone=True)
|
||||
'''The name of the property of this instance, to use to search upwards for
|
||||
a namespace to use by this instance. Defaults to `'parent'` so that we'll
|
||||
search the parent tree. See :attr:`knspace`.
|
||||
|
||||
When `None`, we won't search the parent tree for the namespace.
|
||||
`allownone` is `True`.
|
||||
'''
|
||||
|
||||
def _get_knsname(self):
|
||||
return self._knsname
|
||||
|
||||
def _set_knsname(self, value):
|
||||
old_name = self._knsname
|
||||
knspace = self.knspace
|
||||
if old_name and knspace and getattr(knspace, old_name) == self:
|
||||
setattr(knspace, old_name, None)
|
||||
|
||||
self._knsname = value
|
||||
if value:
|
||||
if knspace:
|
||||
setattr(knspace, value, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(value))
|
||||
|
||||
knsname = AliasProperty(
|
||||
_get_knsname, _set_knsname, bind=('_knsname', ), cache=False)
|
||||
'''The name given to this instance. If named, the name will be added to the
|
||||
associated :attr:`knspace` namespace, which will then point to the
|
||||
`proxy_ref` of this instance.
|
||||
|
||||
When named, one can access this object by e.g. self.knspace.name, where
|
||||
`name` is the given name of this instance. See :attr:`knspace` and the
|
||||
module description for more details.
|
||||
'''
|
||||
|
||||
|
||||
knspace = register_context('knspace', KNSpace)
|
||||
'''The default :class:`KNSpace` namespace. See :attr:`KNSpaceBehavior.knspace`
|
||||
for more details.
|
||||
'''
|
||||
@@ -0,0 +1,156 @@
|
||||
'''
|
||||
ToggleButton Behavior
|
||||
=====================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.togglebutton.ToggleButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.togglebutton.ToggleButton` behavior. You can combine this
|
||||
class with other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative togglebuttons that preserve Kivy togglebutton behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds togglebutton behavior to an image to make a checkbox
|
||||
that behaves like a togglebutton::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ToggleButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ToggleButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_state(self, widget, value):
|
||||
if value == 'down':
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
else:
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
'''
|
||||
|
||||
__all__ = ('ToggleButtonBehavior', )
|
||||
|
||||
from kivy.properties import ObjectProperty, BooleanProperty
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from weakref import ref
|
||||
|
||||
|
||||
class ToggleButtonBehavior(ButtonBehavior):
|
||||
'''This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:mod:`~kivy.uix.togglebutton` behavior. Please see the
|
||||
:mod:`togglebutton behaviors module <kivy.uix.behaviors.togglebutton>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
__groups = {}
|
||||
|
||||
group = ObjectProperty(None, allownone=True)
|
||||
'''Group of the button. If `None`, no group will be used (the button will be
|
||||
independent). If specified, :attr:`group` must be a hashable object, like
|
||||
a string. Only one button in a group can be in a 'down' state.
|
||||
|
||||
:attr:`group` is a :class:`~kivy.properties.ObjectProperty` and defaults to
|
||||
`None`.
|
||||
'''
|
||||
|
||||
allow_no_selection = BooleanProperty(True)
|
||||
'''This specifies whether the widgets in a group allow no selection i.e.
|
||||
everything to be deselected.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`allow_no_selection` is a :class:`BooleanProperty` and defaults to
|
||||
`True`
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._previous_group = None
|
||||
super(ToggleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_group(self, *largs):
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if self._previous_group:
|
||||
group = groups[self._previous_group]
|
||||
for item in group[:]:
|
||||
if item() is self:
|
||||
group.remove(item)
|
||||
break
|
||||
group = self._previous_group = self.group
|
||||
if group not in groups:
|
||||
groups[group] = []
|
||||
r = ref(self, ToggleButtonBehavior._clear_groups)
|
||||
groups[group].append(r)
|
||||
|
||||
def _release_group(self, current):
|
||||
if self.group is None:
|
||||
return
|
||||
group = self.__groups[self.group]
|
||||
for item in group[:]:
|
||||
widget = item()
|
||||
if widget is None:
|
||||
group.remove(item)
|
||||
if widget is current:
|
||||
continue
|
||||
widget.state = 'normal'
|
||||
|
||||
def _do_press(self):
|
||||
if (not self.allow_no_selection and
|
||||
self.group and self.state == 'down'):
|
||||
return
|
||||
|
||||
self._release_group(self)
|
||||
self.state = 'normal' if self.state == 'down' else 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _clear_groups(wk):
|
||||
# auto flush the element when the weak reference have been deleted
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
for group in list(groups.values()):
|
||||
if wk in group:
|
||||
group.remove(wk)
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def get_widgets(groupname):
|
||||
'''Return a list of the widgets contained in a specific group. If the
|
||||
group doesn't exist, an empty list will be returned.
|
||||
|
||||
.. note::
|
||||
|
||||
Always release the result of this method! Holding a reference to
|
||||
any of these widgets can prevent them from being garbage collected.
|
||||
If in doubt, do::
|
||||
|
||||
l = ToggleButtonBehavior.get_widgets('mygroup')
|
||||
# do your job
|
||||
del l
|
||||
|
||||
.. warning::
|
||||
|
||||
It's possible that some widgets that you have previously
|
||||
deleted are still in the list. The garbage collector might need
|
||||
to release other objects before flushing them.
|
||||
'''
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if groupname not in groups:
|
||||
return []
|
||||
return [x() for x in groups[groupname] if x()][:]
|
||||
@@ -0,0 +1,318 @@
|
||||
'''
|
||||
Touch Ripple
|
||||
============
|
||||
|
||||
.. versionadded:: 1.10.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
This module contains `mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes
|
||||
to add a touch ripple visual effect known from `Google Material Design
|
||||
<https://en.wikipedia.org/wiki/Material_Design>_` to widgets.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleBehavior` provides
|
||||
rendering the ripple animation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleButtonBehavior`
|
||||
basically provides the same functionality as
|
||||
:class:`~kivy.uix.behaviors.button.ButtonBehavior` but rendering the ripple
|
||||
animation instead of default press/release visualization.
|
||||
'''
|
||||
from kivy.animation import Animation
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import CanvasBase, Color, Ellipse, ScissorPush, ScissorPop
|
||||
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, \
|
||||
ObjectProperty, StringProperty
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TouchRippleBehavior',
|
||||
'TouchRippleButtonBehavior'
|
||||
)
|
||||
|
||||
|
||||
class TouchRippleBehavior(object):
|
||||
'''Touch ripple behavior.
|
||||
|
||||
Supposed to be used as mixin on widget classes.
|
||||
|
||||
Ripple behavior does not trigger automatically, concrete implementation
|
||||
needs to call :func:`ripple_show` respective :func:`ripple_fade` manually.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here we create a Label which renders the touch ripple animation on
|
||||
interaction::
|
||||
|
||||
class RippleLabel(TouchRippleBehavior, Label):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RippleLabel, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
collide_point = self.collide_point(touch.x, touch.y)
|
||||
if collide_point:
|
||||
touch.grab(self)
|
||||
self.ripple_show(touch)
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is self:
|
||||
touch.ungrab(self)
|
||||
self.ripple_fade()
|
||||
return True
|
||||
return False
|
||||
'''
|
||||
|
||||
ripple_rad_default = NumericProperty(10)
|
||||
'''Default radius the animation starts from.
|
||||
|
||||
:attr:`ripple_rad_default` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `10`.
|
||||
'''
|
||||
|
||||
ripple_duration_in = NumericProperty(.5)
|
||||
'''Animation duration taken to show the overlay.
|
||||
|
||||
:attr:`ripple_duration_in` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_duration_out = NumericProperty(.2)
|
||||
'''Animation duration taken to fade the overlay.
|
||||
|
||||
:attr:`ripple_duration_out` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.2`.
|
||||
'''
|
||||
|
||||
ripple_fade_from_alpha = NumericProperty(.5)
|
||||
'''Alpha channel for ripple color the animation starts with.
|
||||
|
||||
:attr:`ripple_fade_from_alpha` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_fade_to_alpha = NumericProperty(.8)
|
||||
'''Alpha channel for ripple color the animation targets to.
|
||||
|
||||
:attr:`ripple_fade_to_alpha` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.8`.
|
||||
'''
|
||||
|
||||
ripple_scale = NumericProperty(2.)
|
||||
'''Max scale of the animation overlay calculated from max(width/height) of
|
||||
the decorated widget.
|
||||
|
||||
:attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `2.0`.
|
||||
'''
|
||||
|
||||
ripple_func_in = StringProperty('in_cubic')
|
||||
'''Animation callback for showing the overlay.
|
||||
|
||||
:attr:`ripple_func_in` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `in_cubic`.
|
||||
'''
|
||||
|
||||
ripple_func_out = StringProperty('out_quad')
|
||||
'''Animation callback for hiding the overlay.
|
||||
|
||||
:attr:`ripple_func_out` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `out_quad`.
|
||||
'''
|
||||
|
||||
ripple_rad = NumericProperty(10)
|
||||
ripple_pos = ListProperty([0, 0])
|
||||
ripple_color = ListProperty((1., 1., 1., .5))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(TouchRippleBehavior, self).__init__(**kwargs)
|
||||
self.ripple_pane = CanvasBase()
|
||||
self.canvas.add(self.ripple_pane)
|
||||
self.bind(
|
||||
ripple_color=self._ripple_set_color,
|
||||
ripple_pos=self._ripple_set_ellipse,
|
||||
ripple_rad=self._ripple_set_ellipse
|
||||
)
|
||||
self.ripple_ellipse = None
|
||||
self.ripple_col_instruction = None
|
||||
|
||||
def ripple_show(self, touch):
|
||||
'''Begin ripple animation on current widget.
|
||||
|
||||
Expects touch event as argument.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
self._ripple_reset_pane()
|
||||
x, y = self.to_window(*self.pos)
|
||||
width, height = self.size
|
||||
if isinstance(self, RelativeLayout):
|
||||
self.ripple_pos = ripple_pos = (touch.x - x, touch.y - y)
|
||||
else:
|
||||
self.ripple_pos = ripple_pos = (touch.x, touch.y)
|
||||
rc = self.ripple_color
|
||||
ripple_rad = self.ripple_rad
|
||||
self.ripple_color = [rc[0], rc[1], rc[2], self.ripple_fade_from_alpha]
|
||||
with self.ripple_pane:
|
||||
ScissorPush(
|
||||
x=int(round(x)),
|
||||
y=int(round(y)),
|
||||
width=int(round(width)),
|
||||
height=int(round(height))
|
||||
)
|
||||
self.ripple_col_instruction = Color(rgba=self.ripple_color)
|
||||
self.ripple_ellipse = Ellipse(
|
||||
size=(ripple_rad, ripple_rad),
|
||||
pos=(
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
)
|
||||
ScissorPop()
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
t=self.ripple_func_in,
|
||||
ripple_color=[rc[0], rc[1], rc[2], self.ripple_fade_to_alpha],
|
||||
duration=self.ripple_duration_in
|
||||
)
|
||||
anim.start(self)
|
||||
|
||||
def ripple_fade(self):
|
||||
'''Finish ripple animation on current widget.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
width, height = self.size
|
||||
rc = self.ripple_color
|
||||
duration = self.ripple_duration_out
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
ripple_color=[rc[0], rc[1], rc[2], 0.],
|
||||
t=self.ripple_func_out,
|
||||
duration=duration
|
||||
)
|
||||
anim.bind(on_complete=self._ripple_anim_complete)
|
||||
anim.start(self)
|
||||
|
||||
def _ripple_set_ellipse(self, instance, value):
|
||||
ellipse = self.ripple_ellipse
|
||||
if not ellipse:
|
||||
return
|
||||
ripple_pos = self.ripple_pos
|
||||
ripple_rad = self.ripple_rad
|
||||
ellipse.size = (ripple_rad, ripple_rad)
|
||||
ellipse.pos = (
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
|
||||
def _ripple_set_color(self, instance, value):
|
||||
if not self.ripple_col_instruction:
|
||||
return
|
||||
self.ripple_col_instruction.rgba = value
|
||||
|
||||
def _ripple_anim_complete(self, anim, instance):
|
||||
self._ripple_reset_pane()
|
||||
|
||||
def _ripple_reset_pane(self):
|
||||
self.ripple_rad = self.ripple_rad_default
|
||||
self.ripple_pane.clear()
|
||||
|
||||
|
||||
class TouchRippleButtonBehavior(TouchRippleBehavior):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
a similar behavior to :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
but provides touch ripple animation instead of button pressed/released as
|
||||
visual effect.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
super(TouchRippleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.ripple_show(touch)
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(TouchRippleButtonBehavior, self).on_touch_up(touch)
|
||||
assert self in touch.ud
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
if self.disabled:
|
||||
return
|
||||
self.ripple_fade()
|
||||
if not self.always_release and not self.collide_point(*touch.pos):
|
||||
return
|
||||
|
||||
# defer on_release until ripple_fade has completed
|
||||
def defer_release(dt):
|
||||
self.dispatch('on_release')
|
||||
Clock.schedule_once(defer_release, self.ripple_duration_out)
|
||||
return True
|
||||
|
||||
def on_disabled(self, instance, value):
|
||||
# ensure ripple animation completes if disabled gets set to True
|
||||
if value:
|
||||
self.ripple_fade()
|
||||
return super(TouchRippleButtonBehavior, self).on_disabled(
|
||||
instance, value)
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
@@ -0,0 +1,331 @@
|
||||
'''
|
||||
Box Layout
|
||||
==========
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: images/boxlayout.gif
|
||||
:align: right
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: images/boxlayout.png
|
||||
:align: right
|
||||
|
||||
:class:`BoxLayout` arranges children in a vertical or horizontal box.
|
||||
|
||||
To position widgets above/below each other, use a vertical BoxLayout::
|
||||
|
||||
layout = BoxLayout(orientation='vertical')
|
||||
btn1 = Button(text='Hello')
|
||||
btn2 = Button(text='World')
|
||||
layout.add_widget(btn1)
|
||||
layout.add_widget(btn2)
|
||||
|
||||
To position widgets next to each other, use a horizontal BoxLayout. In this
|
||||
example, we use 10 pixel spacing between children; the first button covers
|
||||
70% of the horizontal space, the second covers 30%::
|
||||
|
||||
layout = BoxLayout(spacing=10)
|
||||
btn1 = Button(text='Hello', size_hint=(.7, 1))
|
||||
btn2 = Button(text='World', size_hint=(.3, 1))
|
||||
layout.add_widget(btn1)
|
||||
layout.add_widget(btn2)
|
||||
|
||||
Position hints are partially working, depending on the orientation:
|
||||
|
||||
* If the orientation is `vertical`: `x`, `right` and `center_x` will be used.
|
||||
* If the orientation is `horizontal`: `y`, `top` and `center_y` will be used.
|
||||
|
||||
Kv Example::
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
Label:
|
||||
text: 'this on top'
|
||||
Label:
|
||||
text: 'this right aligned'
|
||||
size_hint_x: None
|
||||
size: self.texture_size
|
||||
pos_hint: {'right': 1}
|
||||
Label:
|
||||
text: 'this on bottom'
|
||||
|
||||
You can check the `examples/widgets/boxlayout_poshint.py` for a live example.
|
||||
|
||||
.. note::
|
||||
|
||||
The `size_hint` uses the available space after subtracting all the
|
||||
fixed-size widgets. For example, if you have a layout that is 800px
|
||||
wide, and add three buttons like this::
|
||||
|
||||
btn1 = Button(text='Hello', size=(200, 100), size_hint=(None, None))
|
||||
btn2 = Button(text='Kivy', size_hint=(.5, 1))
|
||||
btn3 = Button(text='World', size_hint=(.5, 1))
|
||||
|
||||
The first button will be 200px wide as specified, the second and third
|
||||
will be 300px each, e.g. (800-200) * 0.5
|
||||
|
||||
|
||||
.. versionchanged:: 1.4.1
|
||||
Added support for `pos_hint`.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('BoxLayout', )
|
||||
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import (NumericProperty, OptionProperty,
|
||||
VariableListProperty, ReferenceListProperty)
|
||||
|
||||
|
||||
class BoxLayout(Layout):
|
||||
'''Box layout class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
spacing = NumericProperty(0)
|
||||
'''Spacing between children, in pixels.
|
||||
|
||||
:attr:`spacing` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 0.
|
||||
'''
|
||||
|
||||
padding = VariableListProperty([0, 0, 0, 0])
|
||||
'''Padding between layout box and children: [padding_left, padding_top,
|
||||
padding_right, padding_bottom].
|
||||
|
||||
padding also accepts a two argument form [padding_horizontal,
|
||||
padding_vertical] and a one argument form [padding].
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
Replaced NumericProperty with VariableListProperty.
|
||||
|
||||
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
|
||||
defaults to [0, 0, 0, 0].
|
||||
'''
|
||||
|
||||
orientation = OptionProperty('horizontal', options=(
|
||||
'horizontal', 'vertical'))
|
||||
'''Orientation of the layout.
|
||||
|
||||
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'horizontal'. Can be 'vertical' or 'horizontal'.
|
||||
'''
|
||||
|
||||
minimum_width = NumericProperty(0)
|
||||
'''Automatically computed minimum width needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0. It is read only.
|
||||
'''
|
||||
|
||||
minimum_height = NumericProperty(0)
|
||||
'''Automatically computed minimum height needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0. It is read only.
|
||||
'''
|
||||
|
||||
minimum_size = ReferenceListProperty(minimum_width, minimum_height)
|
||||
'''Automatically computed minimum size needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`minimum_size` is a
|
||||
:class:`~kivy.properties.ReferenceListProperty` of
|
||||
(:attr:`minimum_width`, :attr:`minimum_height`) properties. It is read
|
||||
only.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(BoxLayout, self).__init__(**kwargs)
|
||||
update = self._trigger_layout
|
||||
fbind = self.fbind
|
||||
fbind('spacing', update)
|
||||
fbind('padding', update)
|
||||
fbind('children', update)
|
||||
fbind('orientation', update)
|
||||
fbind('parent', update)
|
||||
fbind('size', update)
|
||||
fbind('pos', update)
|
||||
|
||||
def _iterate_layout(self, sizes):
|
||||
# optimize layout by preventing looking at the same attribute in a loop
|
||||
len_children = len(sizes)
|
||||
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
||||
spacing = self.spacing
|
||||
orientation = self.orientation
|
||||
padding_x = padding_left + padding_right
|
||||
padding_y = padding_top + padding_bottom
|
||||
|
||||
# calculate maximum space used by size_hint
|
||||
stretch_sum = 0.
|
||||
has_bound = False
|
||||
hint = [None] * len_children
|
||||
# min size from all the None hint, and from those with sh_min
|
||||
minimum_size_bounded = 0
|
||||
if orientation == 'horizontal':
|
||||
minimum_size_y = 0
|
||||
minimum_size_none = padding_x + spacing * (len_children - 1)
|
||||
|
||||
for i, ((w, h), (shw, shh), _, (shw_min, shh_min),
|
||||
(shw_max, _)) in enumerate(sizes):
|
||||
if shw is None:
|
||||
minimum_size_none += w
|
||||
else:
|
||||
hint[i] = shw
|
||||
if shw_min:
|
||||
has_bound = True
|
||||
minimum_size_bounded += shw_min
|
||||
elif shw_max is not None:
|
||||
has_bound = True
|
||||
stretch_sum += shw
|
||||
|
||||
if shh is None:
|
||||
minimum_size_y = max(minimum_size_y, h)
|
||||
elif shh_min:
|
||||
minimum_size_y = max(minimum_size_y, shh_min)
|
||||
|
||||
minimum_size_x = minimum_size_bounded + minimum_size_none
|
||||
minimum_size_y += padding_y
|
||||
else:
|
||||
minimum_size_x = 0
|
||||
minimum_size_none = padding_y + spacing * (len_children - 1)
|
||||
|
||||
for i, ((w, h), (shw, shh), _, (shw_min, shh_min),
|
||||
(_, shh_max)) in enumerate(sizes):
|
||||
if shh is None:
|
||||
minimum_size_none += h
|
||||
else:
|
||||
hint[i] = shh
|
||||
if shh_min:
|
||||
has_bound = True
|
||||
minimum_size_bounded += shh_min
|
||||
elif shh_max is not None:
|
||||
has_bound = True
|
||||
stretch_sum += shh
|
||||
|
||||
if shw is None:
|
||||
minimum_size_x = max(minimum_size_x, w)
|
||||
elif shw_min:
|
||||
minimum_size_x = max(minimum_size_x, shw_min)
|
||||
|
||||
minimum_size_y = minimum_size_bounded + minimum_size_none
|
||||
minimum_size_x += padding_x
|
||||
|
||||
self.minimum_size = minimum_size_x, minimum_size_y
|
||||
# do not move the w/h get above, it's likely to change on above line
|
||||
selfx = self.x
|
||||
selfy = self.y
|
||||
|
||||
if orientation == 'horizontal':
|
||||
stretch_space = max(0.0, self.width - minimum_size_none)
|
||||
dim = 0
|
||||
else:
|
||||
stretch_space = max(0.0, self.height - minimum_size_none)
|
||||
dim = 1
|
||||
|
||||
if has_bound:
|
||||
# make sure the size_hint_min/max are not violated
|
||||
if stretch_space < 1e-9:
|
||||
# there's no space, so just set to min size or zero
|
||||
stretch_sum = stretch_space = 1.
|
||||
|
||||
for i, val in enumerate(sizes):
|
||||
sh = val[1][dim]
|
||||
if sh is None:
|
||||
continue
|
||||
|
||||
sh_min = val[3][dim]
|
||||
if sh_min is not None:
|
||||
hint[i] = sh_min
|
||||
else:
|
||||
hint[i] = 0. # everything else is zero
|
||||
else:
|
||||
# hint gets updated in place
|
||||
self.layout_hint_with_bounds(
|
||||
stretch_sum, stretch_space, minimum_size_bounded,
|
||||
(val[3][dim] for val in sizes),
|
||||
(elem[4][dim] for elem in sizes), hint)
|
||||
|
||||
if orientation == 'horizontal':
|
||||
x = padding_left + selfx
|
||||
size_y = self.height - padding_y
|
||||
for i, (sh, ((w, h), (_, shh), pos_hint, _, _)) in enumerate(
|
||||
zip(reversed(hint), reversed(sizes))):
|
||||
cy = selfy + padding_bottom
|
||||
|
||||
if sh:
|
||||
w = max(0., stretch_space * sh / stretch_sum)
|
||||
if shh:
|
||||
h = max(0, shh * size_y)
|
||||
|
||||
for key, value in pos_hint.items():
|
||||
posy = value * size_y
|
||||
if key == 'y':
|
||||
cy += posy
|
||||
elif key == 'top':
|
||||
cy += posy - h
|
||||
elif key == 'center_y':
|
||||
cy += posy - (h / 2.)
|
||||
|
||||
yield len_children - i - 1, x, cy, w, h
|
||||
x += w + spacing
|
||||
|
||||
else:
|
||||
y = padding_bottom + selfy
|
||||
size_x = self.width - padding_x
|
||||
for i, (sh, ((w, h), (shw, _), pos_hint, _, _)) in enumerate(
|
||||
zip(hint, sizes)):
|
||||
cx = selfx + padding_left
|
||||
|
||||
if sh:
|
||||
h = max(0., stretch_space * sh / stretch_sum)
|
||||
if shw:
|
||||
w = max(0, shw * size_x)
|
||||
|
||||
for key, value in pos_hint.items():
|
||||
posx = value * size_x
|
||||
if key == 'x':
|
||||
cx += posx
|
||||
elif key == 'right':
|
||||
cx += posx - w
|
||||
elif key == 'center_x':
|
||||
cx += posx - (w / 2.)
|
||||
|
||||
yield i, cx, y, w, h
|
||||
y += h + spacing
|
||||
|
||||
def do_layout(self, *largs):
|
||||
children = self.children
|
||||
if not children:
|
||||
l, t, r, b = self.padding
|
||||
self.minimum_size = l + r, t + b
|
||||
return
|
||||
|
||||
for i, x, y, w, h in self._iterate_layout(
|
||||
[(c.size, c.size_hint, c.pos_hint, c.size_hint_min,
|
||||
c.size_hint_max) for c in children]):
|
||||
c = children[i]
|
||||
c.pos = x, y
|
||||
shw, shh = c.size_hint
|
||||
if shw is None:
|
||||
if shh is not None:
|
||||
c.height = h
|
||||
else:
|
||||
if shh is None:
|
||||
c.width = w
|
||||
else:
|
||||
c.size = (w, h)
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
widget.fbind('pos_hint', self._trigger_layout)
|
||||
return super(BoxLayout, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
widget.funbind('pos_hint', self._trigger_layout)
|
||||
return super(BoxLayout, self).remove_widget(widget, *args, **kwargs)
|
||||
@@ -0,0 +1,576 @@
|
||||
'''
|
||||
Bubble
|
||||
======
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
.. image:: images/bubble.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`Bubble` widget is a form of menu or a small popup with an arrow
|
||||
arranged on one side of it's content.
|
||||
|
||||
The :class:`Bubble` contains an arrow attached to the content
|
||||
(e.g., :class:`BubbleContent`) pointing in the direction you choose. It can
|
||||
be placed either at a predefined location or flexibly by specifying a relative
|
||||
position on the border of the widget.
|
||||
|
||||
The :class:`BubbleContent` is a styled BoxLayout and is thought to be added to
|
||||
the :class:`Bubble` as a child widget. The :class:`Bubble` will then arrange
|
||||
an arrow around the content as desired. Instead of the class:`BubbleContent`,
|
||||
you can theoretically use any other :class:`Widget` as well as long as it
|
||||
supports the 'bind' and 'unbind' function of the :class:`EventDispatcher` and
|
||||
is compatible with Kivy to be placed inside a :class:`BoxLayout`.
|
||||
|
||||
The :class:`BubbleButton`is a styled Button. It suits to the style of
|
||||
:class:`Bubble` and :class:`BubbleContent`. Feel free to place other Widgets
|
||||
inside the 'content' of the :class:`Bubble`.
|
||||
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
The properties :attr:`background_image`, :attr:`background_color`,
|
||||
:attr:`border` and :attr:`border_auto_scale` were removed from :class:`Bubble`.
|
||||
These properties had only been used by the content widget that now uses it's
|
||||
own properties instead. The color of the arrow is now changed with
|
||||
:attr:`arrow_color` instead of :attr:`background_color`.
|
||||
These changes makes the :class:`Bubble` transparent to use with other layouts
|
||||
as content without any side-effects due to property inheritance.
|
||||
|
||||
The property :attr:`flex_arrow_pos` has been added to allow further
|
||||
customization of the arrow positioning.
|
||||
|
||||
The properties :attr:`arrow_margin`, :attr:`arrow_margin_x`,
|
||||
:attr:`arrow_margin_y`, :attr:`content_size`, :attr:`content_width` and
|
||||
:attr:`content_height` have been added to ease proper sizing of a
|
||||
:class:`Bubble` e.g., based on it's content size.
|
||||
|
||||
BubbleContent
|
||||
=============
|
||||
|
||||
The :class:`BubbleContent` is a styled BoxLayout that can be used to
|
||||
add e.g., :class:`BubbleButtons` as menu items.
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
The properties :attr:`background_image`, :attr:`background_color`,
|
||||
:attr:`border` and :attr:`border_auto_scale` were added to the
|
||||
:class:`BubbleContent`. The :class:`BubbleContent` does no longer rely on these
|
||||
properties being present in the parent class.
|
||||
|
||||
BubbleButton
|
||||
============
|
||||
|
||||
The :class:`BubbleButton` is a styled :class:`Button` that can be used to be
|
||||
added to the :class:`BubbleContent`.
|
||||
|
||||
Simple example
|
||||
--------------
|
||||
|
||||
.. include:: ../../examples/widgets/bubble_test.py
|
||||
:literal:
|
||||
|
||||
Customize the Bubble
|
||||
--------------------
|
||||
|
||||
You can choose the direction in which the arrow points::
|
||||
|
||||
Bubble(arrow_pos='top_mid')
|
||||
or
|
||||
Bubble(size=(200, 40), flex_arrow_pos=(175, 40))
|
||||
|
||||
Similarly, the corresponding properties in the '.kv' language can be used
|
||||
as well.
|
||||
|
||||
You can change the appearance of the bubble::
|
||||
|
||||
Bubble(
|
||||
arrow_image='/path/to/arrow/image',
|
||||
arrow_color=(1, 0, 0, .5)),
|
||||
)
|
||||
BubbleContent(
|
||||
background_image='/path/to/background/image',
|
||||
background_color=(1, 0, 0, .5), # 50% translucent red
|
||||
border=(0,0,0,0),
|
||||
)
|
||||
|
||||
Similarly, the corresponding properties in the '.kv' language can be used
|
||||
as well.
|
||||
|
||||
-----------------------------
|
||||
'''
|
||||
|
||||
__all__ = ('Bubble', 'BubbleButton', 'BubbleContent')
|
||||
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.scatter import Scatter
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.properties import StringProperty
|
||||
from kivy.properties import OptionProperty
|
||||
from kivy.properties import ListProperty
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.properties import ColorProperty
|
||||
from kivy.properties import NumericProperty
|
||||
from kivy.properties import ReferenceListProperty
|
||||
from kivy.base import EventLoop
|
||||
from kivy.metrics import dp
|
||||
|
||||
|
||||
class BubbleException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BubbleButton(Button):
|
||||
'''A button intended for use in a BubbleContent widget.
|
||||
You can use a "normal" button class, but it will not look good unless the
|
||||
background is changed.
|
||||
|
||||
Rather use this BubbleButton widget that is already defined and provides a
|
||||
suitable background for you.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class BubbleContent(BoxLayout):
|
||||
'''A styled BoxLayout that can be used as the content widget of a Bubble.
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
The graphical appearance of :class:`BubbleContent` is now based on it's
|
||||
own properties :attr:`background_image`, :attr:`background_color`,
|
||||
:attr:`border` and :attr:`border_auto_scale`. The parent widget properties
|
||||
are no longer considered. This makes the BubbleContent a standalone themed
|
||||
BoxLayout.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a). To use it you have to set
|
||||
:attr:`background_image` first.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
'''
|
||||
|
||||
background_image = StringProperty('atlas://data/images/defaulttheme/bubble')
|
||||
'''Background image of the bubble.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/bubble'.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used with the :attr:`background_image`.
|
||||
It should be used when using custom backgrounds.
|
||||
|
||||
It must be a list of 4 values: (bottom, right, top, left). Read the
|
||||
BorderImage instructions for more information about how to use it.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16)
|
||||
'''
|
||||
|
||||
border_auto_scale = OptionProperty(
|
||||
'both_lower',
|
||||
options=[
|
||||
'off', 'both', 'x_only', 'y_only', 'y_full_x_lower',
|
||||
'x_full_y_lower', 'both_lower'
|
||||
]
|
||||
)
|
||||
'''Specifies the :attr:`kivy.graphics.BorderImage.auto_scale`
|
||||
value on the background BorderImage.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`border_auto_scale` is a
|
||||
:class:`~kivy.properties.OptionProperty` and defaults to
|
||||
'both_lower'.
|
||||
'''
|
||||
|
||||
|
||||
class Bubble(BoxLayout):
|
||||
'''Bubble class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
content = ObjectProperty(allownone=True)
|
||||
'''This is the object where the main content of the bubble is held.
|
||||
|
||||
The content of the Bubble set by 'add_widget' and removed with
|
||||
'remove_widget' similarly to the :class:`ActionView` which is placed into
|
||||
a class:`ActionBar`
|
||||
|
||||
:attr:`content` is a :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
arrow_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/bubble_arrow'
|
||||
)
|
||||
''' Image of the arrow pointing to the bubble.
|
||||
|
||||
:attr:`arrow_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/bubble_arrow'.
|
||||
'''
|
||||
|
||||
arrow_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Arrow color, in the format (r, g, b, a). To use it you have to set
|
||||
:attr:`arrow_image` first.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
'''
|
||||
|
||||
show_arrow = BooleanProperty(True)
|
||||
''' Indicates whether to show arrow.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`show_arrow` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `True`.
|
||||
'''
|
||||
|
||||
arrow_pos = OptionProperty(
|
||||
'bottom_mid',
|
||||
options=(
|
||||
'left_top', 'left_mid', 'left_bottom',
|
||||
'top_left', 'top_mid', 'top_right',
|
||||
'right_top', 'right_mid', 'right_bottom',
|
||||
'bottom_left', 'bottom_mid', 'bottom_right',
|
||||
)
|
||||
)
|
||||
'''Specifies the position of the arrow as predefined relative position to
|
||||
the bubble.
|
||||
Can be one of: left_top, left_mid, left_bottom top_left, top_mid, top_right
|
||||
right_top, right_mid, right_bottom bottom_left, bottom_mid, bottom_right.
|
||||
|
||||
:attr:`arrow_pos` is a :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'bottom_mid'.
|
||||
'''
|
||||
|
||||
flex_arrow_pos = ListProperty(None)
|
||||
'''Specifies the position of the arrow as flex coordinate around the
|
||||
border of the :class:`Bubble` Widget.
|
||||
If this property is set to a proper position (relative pixel coordinates
|
||||
within the :class:`Bubble` widget, it overwrites the setting
|
||||
:attr:`arrow_pos`.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`flex_arrow_pos` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
limit_to = ObjectProperty(None, allownone=True)
|
||||
'''Specifies the widget to which the bubbles position is restricted.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
:attr:`limit_to` is a :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to 'None'.
|
||||
'''
|
||||
|
||||
arrow_margin_x = NumericProperty(0)
|
||||
'''Automatically computed margin in x direction that the arrow widget
|
||||
occupies in pixel.
|
||||
|
||||
In combination with the :attr:`content_width`, this property can be used
|
||||
to determine the correct width of the Bubble to exactly enclose the
|
||||
arrow + content by adding :attr:`content_width` and :attr:`arrow_margin_x`
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_margin_x` is a :class:`~kivy.properties.NumericProperty` and
|
||||
represents the added margin in x direction due to the arrow widget.
|
||||
It defaults to 0 and is read only.
|
||||
'''
|
||||
|
||||
arrow_margin_y = NumericProperty(0)
|
||||
'''Automatically computed margin in y direction that the arrow widget
|
||||
occupies in pixel.
|
||||
|
||||
In combination with the :attr:`content_height`, this property can be used
|
||||
to determine the correct height of the Bubble to exactly enclose the
|
||||
arrow + content by adding :attr:`content_height` and :attr:`arrow_margin_y`
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_margin_y` is a :class:`~kivy.properties.NumericProperty` and
|
||||
represents the added margin in y direction due to the arrow widget.
|
||||
It defaults to 0 and is read only.
|
||||
'''
|
||||
|
||||
arrow_margin = ReferenceListProperty(arrow_margin_x, arrow_margin_y)
|
||||
'''Automatically computed margin that the arrow widget occupies in
|
||||
x and y direction in pixel.
|
||||
|
||||
Check the description of :attr:`arrow_margin_x` and :attr:`arrow_margin_y`.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_margin` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`arrow_margin_x`, :attr:`arrow_margin_y`) properties.
|
||||
It is read only.
|
||||
'''
|
||||
|
||||
content_width = NumericProperty(0)
|
||||
'''The width of the content Widget.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`content_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
is the same as self.content.width if content is not None, else it defaults
|
||||
to 0. It is read only.
|
||||
'''
|
||||
|
||||
content_height = NumericProperty(0)
|
||||
'''The height of the content Widget.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`content_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
is the same as self.content.height if content is not None, else it defaults
|
||||
to 0. It is read only.
|
||||
'''
|
||||
|
||||
content_size = ReferenceListProperty(content_width, content_height)
|
||||
''' The size of the content Widget.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`content_size` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`content_width`, :attr:`content_height`) properties.
|
||||
It is read only.
|
||||
'''
|
||||
|
||||
# Internal map that specifies the different parameters for fixed arrow
|
||||
# position layouts. The flex_arrow_pos uses these parameter sets
|
||||
# as a template.
|
||||
# 0: orientation of the children of Bubble ([content, arrow])
|
||||
# 1: order of widgets to add to the BoxLayout (default: [content, arrow])
|
||||
# 2: size_hint of _arrow_image_layout
|
||||
# 3: rotation of the _arrow_image
|
||||
# 4: pos_hint of the _arrow_image_layout
|
||||
ARROW_LAYOUTS = {
|
||||
"bottom_left": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "x": 0.05}), # noqa: E201,E241,E501
|
||||
"bottom_mid": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "center_x": 0.50}), # noqa: E201,E241,E501
|
||||
"bottom_right": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "right": 0.95}), # noqa: E201,E241,E501
|
||||
"right_bottom": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "y": 0.05}), # noqa: E201,E241,E501
|
||||
"right_mid": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "center_y": 0.50}), # noqa: E201,E241,E501
|
||||
"right_top": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "top": 0.95}), # noqa: E201,E241,E501
|
||||
"top_left": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "x": 0.05}), # noqa: E201,E241,E501
|
||||
"top_mid": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "center_x": 0.50}), # noqa: E201,E241,E501
|
||||
"top_right": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "right": 0.95}), # noqa: E201,E241,E501
|
||||
"left_bottom": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "y": 0.05}), # noqa: E201,E241,E501
|
||||
"left_mid": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "center_y": 0.50}), # noqa: E201,E241,E501
|
||||
"left_top": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "top": 0.95}), # noqa: E201,E241,E501
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.content = None
|
||||
|
||||
self._flex_arrow_layout_params = None
|
||||
self._temporarily_ignore_limits = False
|
||||
|
||||
self._arrow_image = Image(
|
||||
source=self.arrow_image,
|
||||
fit_mode="scale-down",
|
||||
color=self.arrow_color
|
||||
)
|
||||
self._arrow_image.width = self._arrow_image.texture_size[0]
|
||||
self._arrow_image.height = dp(self._arrow_image.texture_size[1])
|
||||
self._arrow_image_scatter = Scatter(
|
||||
size_hint=(None, None),
|
||||
do_scale=False,
|
||||
do_rotation=False,
|
||||
do_translation=False,
|
||||
)
|
||||
self._arrow_image_scatter.add_widget(self._arrow_image)
|
||||
self._arrow_image_scatter.size = self._arrow_image.texture_size
|
||||
self._arrow_image_scatter_wrapper = BoxLayout(
|
||||
size_hint=(None, None),
|
||||
)
|
||||
self._arrow_image_scatter_wrapper.add_widget(self._arrow_image_scatter)
|
||||
self._arrow_image_layout = RelativeLayout()
|
||||
self._arrow_image_layout.add_widget(self._arrow_image_scatter_wrapper)
|
||||
|
||||
self._arrow_layout = None
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
if self.content is None:
|
||||
self.content = widget
|
||||
self.content_size = widget.size
|
||||
self.content.bind(size=self.update_content_size)
|
||||
self.reposition_inner_widgets()
|
||||
else:
|
||||
raise BubbleException(
|
||||
"Bubble can only contain a single Widget or Layout"
|
||||
)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
if widget == self.content:
|
||||
self.content.unbind(size=self.update_content_size)
|
||||
self.content = None
|
||||
self.content_size = [0, 0]
|
||||
self.reposition_inner_widgets()
|
||||
return
|
||||
super().remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def on_content_size(self, instance, value):
|
||||
self.adjust_position()
|
||||
|
||||
def on_limit_to(self, instance, value):
|
||||
self.adjust_position()
|
||||
|
||||
def on_pos(self, instance, value):
|
||||
self.adjust_position()
|
||||
|
||||
def on_size(self, instance, value):
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def on_arrow_image(self, instance, value):
|
||||
self._arrow_image.source = self.arrow_image
|
||||
self._arrow_image.width = self._arrow_image.texture_size[0]
|
||||
self._arrow_image.height = dp(self._arrow_image.texture_size[1])
|
||||
self._arrow_image_scatter.size = self._arrow_image.texture_size
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def on_arrow_color(self, instance, value):
|
||||
self._arrow_image.color = self.arrow_color
|
||||
|
||||
def on_arrow_pos(self, instance, value):
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def on_flex_arrow_pos(self, instance, value):
|
||||
self._flex_arrow_layout_params = self.get_flex_arrow_layout_params()
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def get_flex_arrow_layout_params(self):
|
||||
pos = self.flex_arrow_pos
|
||||
|
||||
if pos is None:
|
||||
return None
|
||||
|
||||
x, y = pos
|
||||
if not (0 <= x <= self.width and 0 <= y <= self.height):
|
||||
return None
|
||||
|
||||
# the order of the following list defines the side that the arrow
|
||||
# will be attached to in case of ambiguity (same distances)
|
||||
base_layouts_map = [
|
||||
("bottom_mid", y),
|
||||
("top_mid", self.height - y),
|
||||
("left_mid", x),
|
||||
("right_mid", self.width - x),
|
||||
]
|
||||
base_layout_key = min(base_layouts_map, key=lambda val: val[1])[0]
|
||||
arrow_layout = list(Bubble.ARROW_LAYOUTS[base_layout_key])
|
||||
|
||||
arrow_width = self._arrow_image.width
|
||||
|
||||
# This function calculates the proper value for pos_hint, i.e., the
|
||||
# arrow texture does not 'overflow' and stays entirely connected to
|
||||
# the side of the content.
|
||||
def calc_x0(x, length):
|
||||
return x * (length - arrow_width) / (length * length)
|
||||
|
||||
if base_layout_key == "bottom_mid":
|
||||
arrow_layout[-1] = {"top": 1.0, "x": calc_x0(x, self.width)}
|
||||
elif base_layout_key == "top_mid":
|
||||
arrow_layout[-1] = {"bottom": 0.0, "x": calc_x0(x, self.width)}
|
||||
elif base_layout_key == "left_mid":
|
||||
arrow_layout[-1] = {"right": 1.0, "y": calc_x0(y, self.height)}
|
||||
elif base_layout_key == "right_mid":
|
||||
arrow_layout[-1] = {"left": 0.0, "y": calc_x0(y, self.height)}
|
||||
return arrow_layout
|
||||
|
||||
def update_content_size(self, instance, value):
|
||||
self.content_size = self.content.size
|
||||
|
||||
def adjust_position(self):
|
||||
if self.limit_to is not None and not self._temporarily_ignore_limits:
|
||||
if self.limit_to is EventLoop.window:
|
||||
lim_x, lim_y = 0, 0
|
||||
lim_top, lim_right = self.limit_to.size
|
||||
else:
|
||||
lim_x = self.limit_to.x
|
||||
lim_y = self.limit_to.y
|
||||
lim_top = self.limit_to.top
|
||||
lim_right = self.limit_to.right
|
||||
|
||||
self._temporarily_ignore_limits = True
|
||||
|
||||
if not (lim_x > self.x and lim_right < self.right):
|
||||
self.x = max(lim_x, min(lim_right - self.width, self.x))
|
||||
|
||||
if not (lim_y > self.y and lim_right < self.right):
|
||||
self.y = min(lim_top - self.height, max(lim_y, self.y))
|
||||
|
||||
self._temporarily_ignore_limits = False
|
||||
|
||||
def reposition_inner_widgets(self):
|
||||
arrow_image_layout = self._arrow_image_layout
|
||||
arrow_image_scatter = self._arrow_image_scatter
|
||||
arrow_image_scatter_wrapper = self._arrow_image_scatter_wrapper
|
||||
content = self.content
|
||||
|
||||
# Remove the children of the Bubble (BoxLayout) as a first step
|
||||
for child in list(self.children):
|
||||
super().remove_widget(child)
|
||||
|
||||
if self.canvas is None or content is None:
|
||||
return
|
||||
|
||||
# find the layout parameters that define a specific bubble setup
|
||||
if self._flex_arrow_layout_params is not None:
|
||||
layout_params = self._flex_arrow_layout_params
|
||||
else:
|
||||
layout_params = Bubble.ARROW_LAYOUTS[self.arrow_pos]
|
||||
(bubble_orientation,
|
||||
widget_order,
|
||||
arrow_size_hint,
|
||||
arrow_rotation,
|
||||
arrow_pos_hint) = layout_params
|
||||
|
||||
# rotate the arrow, place it at the right pos and setup the size
|
||||
# of the widget, so the BoxLayout can do the rest.
|
||||
arrow_image_scatter.rotation = arrow_rotation
|
||||
arrow_image_scatter_wrapper.size = arrow_image_scatter.bbox[1]
|
||||
arrow_image_scatter_wrapper.pos_hint = arrow_pos_hint
|
||||
arrow_image_layout.size_hint = arrow_size_hint
|
||||
arrow_image_layout.size = arrow_image_scatter.bbox[1]
|
||||
|
||||
# set the orientation of the Bubble (BoxLayout)
|
||||
self.orientation = bubble_orientation
|
||||
|
||||
# Add the updated children of the Bubble (BoxLayout) and update
|
||||
# properties
|
||||
widgets_to_add = [content, arrow_image_layout]
|
||||
|
||||
# Set the arrow_margin, so we can use this property for proper sizing
|
||||
# of the Bubble Widget.
|
||||
# Determine whether to add the arrow_image_layout to the
|
||||
# Bubble (BoxLayout) or not.
|
||||
arrow_margin_x, arrow_margin_y = (0, 0)
|
||||
if self.show_arrow:
|
||||
if bubble_orientation[0] == "h":
|
||||
arrow_margin_x = arrow_image_layout.width
|
||||
elif bubble_orientation[0] == "v":
|
||||
arrow_margin_y = arrow_image_layout.height
|
||||
else:
|
||||
widgets_to_add.pop(1)
|
||||
|
||||
for widget in widgets_to_add[::widget_order]:
|
||||
super().add_widget(widget)
|
||||
|
||||
self.arrow_margin = (arrow_margin_x, arrow_margin_y)
|
||||
@@ -0,0 +1,137 @@
|
||||
'''
|
||||
Button
|
||||
======
|
||||
|
||||
.. image:: images/button.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`Button` is a :class:`~kivy.uix.label.Label` with associated actions
|
||||
that are triggered when the button is pressed (or released after a
|
||||
click/touch). To configure the button, the same properties (padding,
|
||||
font_size, etc) and
|
||||
:ref:`sizing system <kivy-uix-label-sizing-and-text-content>`
|
||||
are used as for the :class:`~kivy.uix.label.Label` class::
|
||||
|
||||
button = Button(text='Hello world', font_size=14)
|
||||
|
||||
To attach a callback when the button is pressed (clicked/touched), use
|
||||
:class:`~kivy.uix.widget.Widget.bind`::
|
||||
|
||||
def callback(instance):
|
||||
print('The button <%s> is being pressed' % instance.text)
|
||||
|
||||
btn1 = Button(text='Hello world 1')
|
||||
btn1.bind(on_press=callback)
|
||||
btn2 = Button(text='Hello world 2')
|
||||
btn2.bind(on_press=callback)
|
||||
|
||||
If you want to be notified every time the button state changes, you can bind
|
||||
to the :attr:`Button.state` property::
|
||||
|
||||
def callback(instance, value):
|
||||
print('My button <%s> state is <%s>' % (instance, value))
|
||||
btn1 = Button(text='Hello world 1')
|
||||
btn1.bind(state=callback)
|
||||
|
||||
Kv Example::
|
||||
|
||||
Button:
|
||||
text: 'press me'
|
||||
on_press: print("ouch! More gently please")
|
||||
on_release: print("ahhh")
|
||||
on_state:
|
||||
print("my current state is {}".format(self.state))
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Button', )
|
||||
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import StringProperty, ListProperty, ColorProperty
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
|
||||
|
||||
class Button(ButtonBehavior, Label):
|
||||
'''Button class, see module documentation for more information.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
The behavior / logic of the button has been moved to
|
||||
:class:`~kivy.uix.behaviors.ButtonBehaviors`.
|
||||
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a).
|
||||
|
||||
This acts as a *multiplier* to the texture color. The default
|
||||
texture is grey, so just setting the background color will give
|
||||
a darker result. To set a plain color, set the
|
||||
:attr:`background_normal` to ``''``.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
The :attr:`background_color` is a
|
||||
:class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is not pressed.
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/button'.
|
||||
'''
|
||||
|
||||
background_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_pressed')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is pressed.
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
:attr:`background_down` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/button_pressed'.
|
||||
'''
|
||||
|
||||
background_disabled_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_disabled')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is disabled and not pressed.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/button_disabled'.
|
||||
'''
|
||||
|
||||
background_disabled_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_disabled_pressed')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is disabled and pressed.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled_down` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/button_disabled_pressed'.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used with :attr:`background_normal` and
|
||||
:attr:`background_down`. Can be used for custom backgrounds.
|
||||
|
||||
It must be a list of four values: (bottom, right, top, left). Read the
|
||||
BorderImage instruction for more information about how to use it.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16)
|
||||
'''
|
||||
@@ -0,0 +1,118 @@
|
||||
'''
|
||||
Camera
|
||||
======
|
||||
|
||||
The :class:`Camera` widget is used to capture and display video from a camera.
|
||||
Once the widget is created, the texture inside the widget will be automatically
|
||||
updated. Our :class:`~kivy.core.camera.CameraBase` implementation is used under
|
||||
the hood::
|
||||
|
||||
cam = Camera()
|
||||
|
||||
By default, the first camera found on your system is used. To use a different
|
||||
camera, set the index property::
|
||||
|
||||
cam = Camera(index=1)
|
||||
|
||||
You can also select the camera resolution::
|
||||
|
||||
cam = Camera(resolution=(320, 240))
|
||||
|
||||
.. warning::
|
||||
|
||||
The camera texture is not updated as soon as you have created the object.
|
||||
The camera initialization is asynchronous, so there may be a delay before
|
||||
the requested texture is created.
|
||||
'''
|
||||
|
||||
__all__ = ('Camera', )
|
||||
|
||||
from kivy.uix.image import Image
|
||||
from kivy.core.camera import Camera as CoreCamera
|
||||
from kivy.properties import NumericProperty, ListProperty, \
|
||||
BooleanProperty
|
||||
|
||||
|
||||
class Camera(Image):
|
||||
'''Camera class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
play = BooleanProperty(False)
|
||||
'''Boolean indicating whether the camera is playing or not.
|
||||
You can start/stop the camera by setting this property::
|
||||
|
||||
# start the camera playing at creation
|
||||
cam = Camera(play=True)
|
||||
|
||||
# create the camera, and start later (default)
|
||||
cam = Camera(play=False)
|
||||
# and later
|
||||
cam.play = True
|
||||
|
||||
:attr:`play` is a :class:`~kivy.properties.BooleanProperty` and defaults to
|
||||
False.
|
||||
'''
|
||||
|
||||
index = NumericProperty(-1)
|
||||
'''Index of the used camera, starting from 0.
|
||||
|
||||
:attr:`index` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to -1 to allow auto selection.
|
||||
'''
|
||||
|
||||
resolution = ListProperty([-1, -1])
|
||||
'''Preferred resolution to use when invoking the camera. If you are using
|
||||
[-1, -1], the resolution will be the default one::
|
||||
|
||||
# create a camera object with the best image available
|
||||
cam = Camera()
|
||||
|
||||
# create a camera object with an image of 320x240 if possible
|
||||
cam = Camera(resolution=(320, 240))
|
||||
|
||||
.. warning::
|
||||
|
||||
Depending on the implementation, the camera may not respect this
|
||||
property.
|
||||
|
||||
:attr:`resolution` is a :class:`~kivy.properties.ListProperty` and defaults
|
||||
to [-1, -1].
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._camera = None
|
||||
super(Camera, self).__init__(**kwargs)
|
||||
if self.index == -1:
|
||||
self.index = 0
|
||||
on_index = self._on_index
|
||||
fbind = self.fbind
|
||||
fbind('index', on_index)
|
||||
fbind('resolution', on_index)
|
||||
on_index()
|
||||
|
||||
def on_tex(self, camera):
|
||||
self.texture = texture = camera.texture
|
||||
self.texture_size = list(texture.size)
|
||||
self.canvas.ask_update()
|
||||
|
||||
def _on_index(self, *largs):
|
||||
self._camera = None
|
||||
if self.index < 0:
|
||||
return
|
||||
if self.resolution[0] < 0 or self.resolution[1] < 0:
|
||||
self._camera = CoreCamera(index=self.index, stopped=True)
|
||||
else:
|
||||
self._camera = CoreCamera(index=self.index,
|
||||
resolution=self.resolution, stopped=True)
|
||||
if self.play:
|
||||
self._camera.start()
|
||||
|
||||
self._camera.bind(on_texture=self.on_tex)
|
||||
|
||||
def on_play(self, instance, value):
|
||||
if not self._camera:
|
||||
return
|
||||
if value:
|
||||
self._camera.start()
|
||||
else:
|
||||
self._camera.stop()
|
||||
@@ -0,0 +1,695 @@
|
||||
'''
|
||||
Carousel
|
||||
========
|
||||
|
||||
.. image:: images/carousel.gif
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
The :class:`Carousel` widget provides the classic mobile-friendly carousel view
|
||||
where you can swipe between slides.
|
||||
You can add any content to the carousel and have it move horizontally or
|
||||
vertically. The carousel can display pages in a sequence or a loop.
|
||||
|
||||
Example::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.carousel import Carousel
|
||||
from kivy.uix.image import AsyncImage
|
||||
|
||||
|
||||
class CarouselApp(App):
|
||||
def build(self):
|
||||
carousel = Carousel(direction='right')
|
||||
for i in range(10):
|
||||
src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i
|
||||
image = AsyncImage(source=src, fit_mode="contain")
|
||||
carousel.add_widget(image)
|
||||
return carousel
|
||||
|
||||
|
||||
CarouselApp().run()
|
||||
|
||||
|
||||
Kv Example::
|
||||
|
||||
Carousel:
|
||||
direction: 'right'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-1.png'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-2.png'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-3.png'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-4.png'
|
||||
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The carousel now supports active children, like the
|
||||
:class:`~kivy.uix.scrollview.ScrollView`. It will detect a swipe gesture
|
||||
according to the :attr:`Carousel.scroll_timeout` and
|
||||
:attr:`Carousel.scroll_distance` properties.
|
||||
|
||||
In addition, the slide container is no longer exposed by the API.
|
||||
The impacted properties are
|
||||
:attr:`Carousel.slides`, :attr:`Carousel.current_slide`,
|
||||
:attr:`Carousel.previous_slide` and :attr:`Carousel.next_slide`.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Carousel', )
|
||||
|
||||
from functools import partial
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.stencilview import StencilView
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.properties import BooleanProperty, OptionProperty, AliasProperty, \
|
||||
NumericProperty, ListProperty, ObjectProperty, StringProperty
|
||||
|
||||
|
||||
class Carousel(StencilView):
|
||||
'''Carousel class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
slides = ListProperty([])
|
||||
'''List of slides inside the Carousel. The slides are the
|
||||
widgets added to the Carousel using the :attr:`add_widget` method.
|
||||
|
||||
:attr:`slides` is a :class:`~kivy.properties.ListProperty` and is
|
||||
read-only.
|
||||
'''
|
||||
|
||||
def _get_slides_container(self):
|
||||
return [x.parent for x in self.slides]
|
||||
|
||||
slides_container = AliasProperty(_get_slides_container, bind=('slides',))
|
||||
|
||||
direction = OptionProperty('right',
|
||||
options=('right', 'left', 'top', 'bottom'))
|
||||
'''Specifies the direction in which the slides are ordered. This
|
||||
corresponds to the direction from which the user swipes to go from one
|
||||
slide to the next. It
|
||||
can be `right`, `left`, `top`, or `bottom`. For example, with
|
||||
the default value of `right`, the second slide is to the right
|
||||
of the first and the user would swipe from the right towards the
|
||||
left to get to the second slide.
|
||||
|
||||
:attr:`direction` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'right'.
|
||||
'''
|
||||
|
||||
min_move = NumericProperty(0.2)
|
||||
'''Defines the minimum distance to be covered before the touch is
|
||||
considered a swipe gesture and the Carousel content changed.
|
||||
This is a expressed as a fraction of the Carousel's width.
|
||||
If the movement doesn't reach this minimum value, the movement is
|
||||
cancelled and the content is restored to its original position.
|
||||
|
||||
:attr:`min_move` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.2.
|
||||
'''
|
||||
|
||||
anim_move_duration = NumericProperty(0.5)
|
||||
'''Defines the duration of the Carousel animation between pages.
|
||||
|
||||
:attr:`anim_move_duration` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.5.
|
||||
'''
|
||||
|
||||
anim_cancel_duration = NumericProperty(0.3)
|
||||
'''Defines the duration of the animation when a swipe movement is not
|
||||
accepted. This is generally when the user does not make a large enough
|
||||
swipe. See :attr:`min_move`.
|
||||
|
||||
:attr:`anim_cancel_duration` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.3.
|
||||
'''
|
||||
|
||||
loop = BooleanProperty(False)
|
||||
'''Allow the Carousel to loop infinitely. If True, when the user tries to
|
||||
swipe beyond last page, it will return to the first. If False, it will
|
||||
remain on the last page.
|
||||
|
||||
:attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def _get_index(self):
|
||||
if self.slides:
|
||||
return self._index % len(self.slides)
|
||||
return None
|
||||
|
||||
def _set_index(self, value):
|
||||
if self.slides:
|
||||
self._index = value % len(self.slides)
|
||||
else:
|
||||
self._index = None
|
||||
|
||||
index = AliasProperty(_get_index, _set_index,
|
||||
bind=('_index', 'slides'),
|
||||
cache=True)
|
||||
'''Get/Set the current slide based on the index.
|
||||
|
||||
:attr:`index` is an :class:`~kivy.properties.AliasProperty` and defaults
|
||||
to 0 (the first item).
|
||||
'''
|
||||
|
||||
def _prev_slide(self):
|
||||
slides = self.slides
|
||||
len_slides = len(slides)
|
||||
index = self.index
|
||||
if len_slides < 2: # None, or 1 slide
|
||||
return None
|
||||
if self.loop and index == 0:
|
||||
return slides[-1]
|
||||
if index > 0:
|
||||
return slides[index - 1]
|
||||
|
||||
previous_slide = AliasProperty(_prev_slide,
|
||||
bind=('slides', 'index', 'loop'),
|
||||
cache=True)
|
||||
'''The previous slide in the Carousel. It is None if the current slide is
|
||||
the first slide in the Carousel. This ordering reflects the order in which
|
||||
the slides are added: their presentation varies according to the
|
||||
:attr:`direction` property.
|
||||
|
||||
:attr:`previous_slide` is an :class:`~kivy.properties.AliasProperty`.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
This property no longer exposes the slides container. It returns
|
||||
the widget you have added.
|
||||
'''
|
||||
|
||||
def _curr_slide(self):
|
||||
if len(self.slides):
|
||||
return self.slides[self.index or 0]
|
||||
|
||||
current_slide = AliasProperty(_curr_slide,
|
||||
bind=('slides', 'index'),
|
||||
cache=True)
|
||||
'''The currently shown slide.
|
||||
|
||||
:attr:`current_slide` is an :class:`~kivy.properties.AliasProperty`.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The property no longer exposes the slides container. It returns
|
||||
the widget you have added.
|
||||
'''
|
||||
|
||||
def _next_slide(self):
|
||||
if len(self.slides) < 2: # None, or 1 slide
|
||||
return None
|
||||
if self.loop and self.index == len(self.slides) - 1:
|
||||
return self.slides[0]
|
||||
if self.index < len(self.slides) - 1:
|
||||
return self.slides[self.index + 1]
|
||||
|
||||
next_slide = AliasProperty(_next_slide,
|
||||
bind=('slides', 'index', 'loop'),
|
||||
cache=True)
|
||||
'''The next slide in the Carousel. It is None if the current slide is
|
||||
the last slide in the Carousel. This ordering reflects the order in which
|
||||
the slides are added: their presentation varies according to the
|
||||
:attr:`direction` property.
|
||||
|
||||
:attr:`next_slide` is an :class:`~kivy.properties.AliasProperty`.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The property no longer exposes the slides container.
|
||||
It returns the widget you have added.
|
||||
'''
|
||||
|
||||
scroll_timeout = NumericProperty(200)
|
||||
'''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
|
||||
If the user has not moved :attr:`scroll_distance` within the timeout,
|
||||
no scrolling will occur and the touch event will go to the children.
|
||||
|
||||
:attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 200 (milliseconds)
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
'''
|
||||
|
||||
scroll_distance = NumericProperty('20dp')
|
||||
'''Distance to move before scrolling the :class:`Carousel` in pixels. As
|
||||
soon as the distance has been traveled, the :class:`Carousel` will start
|
||||
to scroll, and no touch event will go to children.
|
||||
It is advisable that you base this value on the dpi of your target device's
|
||||
screen.
|
||||
|
||||
:attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 20dp.
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
'''
|
||||
|
||||
anim_type = StringProperty('out_quad')
|
||||
'''Type of animation to use while animating to the next/previous slide.
|
||||
This should be the name of an
|
||||
:class:`~kivy.animation.AnimationTransition` function.
|
||||
|
||||
:attr:`anim_type` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'out_quad'.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
ignore_perpendicular_swipes = BooleanProperty(False)
|
||||
'''Ignore swipes on axis perpendicular to direction.
|
||||
|
||||
:attr:`ignore_perpendicular_swipes` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to False.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
# private properties, for internal use only ###
|
||||
_index = NumericProperty(0, allownone=True)
|
||||
_prev = ObjectProperty(None, allownone=True)
|
||||
_current = ObjectProperty(None, allownone=True)
|
||||
_next = ObjectProperty(None, allownone=True)
|
||||
_offset = NumericProperty(0)
|
||||
_touch = ObjectProperty(None, allownone=True)
|
||||
|
||||
_change_touch_mode_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._trigger_position_visible_slides = Clock.create_trigger(
|
||||
self._position_visible_slides, -1)
|
||||
super(Carousel, self).__init__(**kwargs)
|
||||
self._skip_slide = None
|
||||
self.touch_mode_change = False
|
||||
self._prioritize_next = False
|
||||
self.fbind('loop', lambda *args: self._insert_visible_slides())
|
||||
|
||||
def load_slide(self, slide):
|
||||
'''Animate to the slide that is passed as the argument.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
'''
|
||||
slides = self.slides
|
||||
start, stop = slides.index(self.current_slide), slides.index(slide)
|
||||
if start == stop:
|
||||
return
|
||||
|
||||
self._skip_slide = stop
|
||||
if stop > start:
|
||||
self._prioritize_next = True
|
||||
self._insert_visible_slides(_next_slide=slide)
|
||||
self.load_next()
|
||||
else:
|
||||
self._prioritize_next = False
|
||||
self._insert_visible_slides(_prev_slide=slide)
|
||||
self.load_previous()
|
||||
|
||||
def load_previous(self):
|
||||
'''Animate to the previous slide.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
'''
|
||||
self.load_next(mode='prev')
|
||||
|
||||
def load_next(self, mode='next'):
|
||||
'''Animate to the next slide.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
'''
|
||||
if self.index is not None:
|
||||
w, h = self.size
|
||||
_direction = {
|
||||
'top': -h / 2,
|
||||
'bottom': h / 2,
|
||||
'left': w / 2,
|
||||
'right': -w / 2}
|
||||
_offset = _direction[self.direction]
|
||||
if mode == 'prev':
|
||||
_offset = -_offset
|
||||
|
||||
self._start_animation(min_move=0, offset=_offset)
|
||||
|
||||
def get_slide_container(self, slide):
|
||||
return slide.parent
|
||||
|
||||
@property
|
||||
def _prev_equals_next(self):
|
||||
return self.loop and len(self.slides) == 2
|
||||
|
||||
def _insert_visible_slides(self, _next_slide=None, _prev_slide=None):
|
||||
get_slide_container = self.get_slide_container
|
||||
|
||||
previous_slide = _prev_slide if _prev_slide else self.previous_slide
|
||||
if previous_slide:
|
||||
self._prev = get_slide_container(previous_slide)
|
||||
else:
|
||||
self._prev = None
|
||||
|
||||
current_slide = self.current_slide
|
||||
if current_slide:
|
||||
self._current = get_slide_container(current_slide)
|
||||
else:
|
||||
self._current = None
|
||||
|
||||
next_slide = _next_slide if _next_slide else self.next_slide
|
||||
if next_slide:
|
||||
self._next = get_slide_container(next_slide)
|
||||
else:
|
||||
self._next = None
|
||||
|
||||
if self._prev_equals_next:
|
||||
setattr(self, '_prev' if self._prioritize_next else '_next', None)
|
||||
|
||||
super_remove = super(Carousel, self).remove_widget
|
||||
for container in self.slides_container:
|
||||
super_remove(container)
|
||||
|
||||
if self._prev and self._prev.parent is not self:
|
||||
super(Carousel, self).add_widget(self._prev)
|
||||
if self._next and self._next.parent is not self:
|
||||
super(Carousel, self).add_widget(self._next)
|
||||
if self._current:
|
||||
super(Carousel, self).add_widget(self._current)
|
||||
|
||||
def _position_visible_slides(self, *args):
|
||||
slides, index = self.slides, self.index
|
||||
no_of_slides = len(slides) - 1
|
||||
if not slides:
|
||||
return
|
||||
x, y, width, height = self.x, self.y, self.width, self.height
|
||||
_offset, direction = self._offset, self.direction[0]
|
||||
_prev, _next, _current = self._prev, self._next, self._current
|
||||
get_slide_container = self.get_slide_container
|
||||
last_slide = get_slide_container(slides[-1])
|
||||
first_slide = get_slide_container(slides[0])
|
||||
skip_next = False
|
||||
_loop = self.loop
|
||||
|
||||
if direction in 'rl':
|
||||
xoff = x + _offset
|
||||
x_prev = {'l': xoff + width, 'r': xoff - width}
|
||||
x_next = {'l': xoff - width, 'r': xoff + width}
|
||||
if _prev:
|
||||
_prev.pos = (x_prev[direction], y)
|
||||
elif _loop and _next and index == 0:
|
||||
# if first slide is moving to right with direction set to right
|
||||
# or toward left with direction set to left
|
||||
if ((_offset > 0 and direction == 'r') or
|
||||
(_offset < 0 and direction == 'l')):
|
||||
# put last_slide before first slide
|
||||
last_slide.pos = (x_prev[direction], y)
|
||||
skip_next = True
|
||||
if _current:
|
||||
_current.pos = (xoff, y)
|
||||
if skip_next:
|
||||
return
|
||||
if _next:
|
||||
_next.pos = (x_next[direction], y)
|
||||
elif _loop and _prev and index == no_of_slides:
|
||||
if ((_offset < 0 and direction == 'r') or
|
||||
(_offset > 0 and direction == 'l')):
|
||||
first_slide.pos = (x_next[direction], y)
|
||||
if direction in 'tb':
|
||||
yoff = y + _offset
|
||||
y_prev = {'t': yoff - height, 'b': yoff + height}
|
||||
y_next = {'t': yoff + height, 'b': yoff - height}
|
||||
if _prev:
|
||||
_prev.pos = (x, y_prev[direction])
|
||||
elif _loop and _next and index == 0:
|
||||
if ((_offset > 0 and direction == 't') or
|
||||
(_offset < 0 and direction == 'b')):
|
||||
last_slide.pos = (x, y_prev[direction])
|
||||
skip_next = True
|
||||
if _current:
|
||||
_current.pos = (x, yoff)
|
||||
if skip_next:
|
||||
return
|
||||
if _next:
|
||||
_next.pos = (x, y_next[direction])
|
||||
elif _loop and _prev and index == no_of_slides:
|
||||
if ((_offset < 0 and direction == 't') or
|
||||
(_offset > 0 and direction == 'b')):
|
||||
first_slide.pos = (x, y_next[direction])
|
||||
|
||||
def on_size(self, *args):
|
||||
size = self.size
|
||||
for slide in self.slides_container:
|
||||
slide.size = size
|
||||
self._trigger_position_visible_slides()
|
||||
|
||||
def on_pos(self, *args):
|
||||
self._trigger_position_visible_slides()
|
||||
|
||||
def on_index(self, *args):
|
||||
self._insert_visible_slides()
|
||||
self._trigger_position_visible_slides()
|
||||
self._offset = 0
|
||||
|
||||
def on_slides(self, *args):
|
||||
if self.slides:
|
||||
self.index = self.index % len(self.slides)
|
||||
self._insert_visible_slides()
|
||||
self._trigger_position_visible_slides()
|
||||
|
||||
def on__offset(self, *args):
|
||||
self._trigger_position_visible_slides()
|
||||
# if reached full offset, switch index to next or prev
|
||||
direction = self.direction[0]
|
||||
_offset = self._offset
|
||||
width = self.width
|
||||
height = self.height
|
||||
index = self.index
|
||||
if self._skip_slide is not None or index is None:
|
||||
return
|
||||
|
||||
# Move to next slide?
|
||||
if (direction == 'r' and _offset <= -width) or \
|
||||
(direction == 'l' and _offset >= width) or \
|
||||
(direction == 't' and _offset <= - height) or \
|
||||
(direction == 'b' and _offset >= height):
|
||||
if self.next_slide:
|
||||
self.index += 1
|
||||
|
||||
# Move to previous slide?
|
||||
elif (direction == 'r' and _offset >= width) or \
|
||||
(direction == 'l' and _offset <= -width) or \
|
||||
(direction == 't' and _offset >= height) or \
|
||||
(direction == 'b' and _offset <= -height):
|
||||
if self.previous_slide:
|
||||
self.index -= 1
|
||||
|
||||
elif self._prev_equals_next:
|
||||
new_value = (_offset < 0) is (direction in 'rt')
|
||||
if self._prioritize_next is not new_value:
|
||||
self._prioritize_next = new_value
|
||||
if new_value is (self._next is None):
|
||||
self._prev, self._next = self._next, self._prev
|
||||
|
||||
def _start_animation(self, *args, **kwargs):
|
||||
# compute target offset for ease back, next or prev
|
||||
new_offset = 0
|
||||
direction = kwargs.get('direction', self.direction)[0]
|
||||
is_horizontal = direction in 'rl'
|
||||
extent = self.width if is_horizontal else self.height
|
||||
min_move = kwargs.get('min_move', self.min_move)
|
||||
_offset = kwargs.get('offset', self._offset)
|
||||
|
||||
if _offset < min_move * -extent:
|
||||
new_offset = -extent
|
||||
elif _offset > min_move * extent:
|
||||
new_offset = extent
|
||||
|
||||
# if new_offset is 0, it wasn't enough to go next/prev
|
||||
dur = self.anim_move_duration
|
||||
if new_offset == 0:
|
||||
dur = self.anim_cancel_duration
|
||||
|
||||
# detect edge cases if not looping
|
||||
len_slides = len(self.slides)
|
||||
index = self.index
|
||||
if not self.loop or len_slides == 1:
|
||||
is_first = (index == 0)
|
||||
is_last = (index == len_slides - 1)
|
||||
if direction in 'rt':
|
||||
towards_prev = (new_offset > 0)
|
||||
towards_next = (new_offset < 0)
|
||||
else:
|
||||
towards_prev = (new_offset < 0)
|
||||
towards_next = (new_offset > 0)
|
||||
if (is_first and towards_prev) or (is_last and towards_next):
|
||||
new_offset = 0
|
||||
|
||||
anim = Animation(_offset=new_offset, d=dur, t=self.anim_type)
|
||||
anim.cancel_all(self)
|
||||
|
||||
def _cmp(*l):
|
||||
if self._skip_slide is not None:
|
||||
self.index = self._skip_slide
|
||||
self._skip_slide = None
|
||||
|
||||
anim.bind(on_complete=_cmp)
|
||||
anim.start(self)
|
||||
|
||||
def _get_uid(self, prefix='sv'):
|
||||
return '{0}.{1}'.format(prefix, self.uid)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
touch.ud[self._get_uid('cavoid')] = True
|
||||
return
|
||||
if self.disabled:
|
||||
return True
|
||||
if self._touch:
|
||||
return super(Carousel, self).on_touch_down(touch)
|
||||
Animation.cancel_all(self)
|
||||
self._touch = touch
|
||||
uid = self._get_uid()
|
||||
touch.grab(self)
|
||||
touch.ud[uid] = {
|
||||
'mode': 'unknown',
|
||||
'time': touch.time_start}
|
||||
self._change_touch_mode_ev = Clock.schedule_once(
|
||||
self._change_touch_mode, self.scroll_timeout / 1000.)
|
||||
self.touch_mode_change = False
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if not self.touch_mode_change:
|
||||
if self.ignore_perpendicular_swipes and \
|
||||
self.direction in ('top', 'bottom'):
|
||||
if abs(touch.oy - touch.y) < self.scroll_distance:
|
||||
if abs(touch.ox - touch.x) > self.scroll_distance:
|
||||
self._change_touch_mode()
|
||||
self.touch_mode_change = True
|
||||
elif self.ignore_perpendicular_swipes and \
|
||||
self.direction in ('right', 'left'):
|
||||
if abs(touch.ox - touch.x) < self.scroll_distance:
|
||||
if abs(touch.oy - touch.y) > self.scroll_distance:
|
||||
self._change_touch_mode()
|
||||
self.touch_mode_change = True
|
||||
|
||||
if self._get_uid('cavoid') in touch.ud:
|
||||
return
|
||||
if self._touch is not touch:
|
||||
super(Carousel, self).on_touch_move(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
if touch.grab_current is not self:
|
||||
return True
|
||||
ud = touch.ud[self._get_uid()]
|
||||
direction = self.direction[0]
|
||||
if ud['mode'] == 'unknown':
|
||||
if direction in 'rl':
|
||||
distance = abs(touch.ox - touch.x)
|
||||
else:
|
||||
distance = abs(touch.oy - touch.y)
|
||||
if distance > self.scroll_distance:
|
||||
ev = self._change_touch_mode_ev
|
||||
if ev is not None:
|
||||
ev.cancel()
|
||||
ud['mode'] = 'scroll'
|
||||
else:
|
||||
if direction in 'rl':
|
||||
self._offset += touch.dx
|
||||
if direction in 'tb':
|
||||
self._offset += touch.dy
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self._get_uid('cavoid') in touch.ud:
|
||||
return
|
||||
if self in [x() for x in touch.grab_list]:
|
||||
touch.ungrab(self)
|
||||
self._touch = None
|
||||
ud = touch.ud[self._get_uid()]
|
||||
if ud['mode'] == 'unknown':
|
||||
ev = self._change_touch_mode_ev
|
||||
if ev is not None:
|
||||
ev.cancel()
|
||||
super(Carousel, self).on_touch_down(touch)
|
||||
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
|
||||
else:
|
||||
self._start_animation()
|
||||
|
||||
else:
|
||||
if self._touch is not touch and self.uid not in touch.ud:
|
||||
super(Carousel, self).on_touch_up(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
|
||||
def _do_touch_up(self, touch, *largs):
|
||||
super(Carousel, 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(Carousel, self).on_touch_up(touch)
|
||||
touch.grab_current = None
|
||||
|
||||
def _change_touch_mode(self, *largs):
|
||||
if not self._touch:
|
||||
return
|
||||
self._start_animation()
|
||||
uid = self._get_uid()
|
||||
touch = self._touch
|
||||
ud = touch.ud[uid]
|
||||
if ud['mode'] == 'unknown':
|
||||
touch.ungrab(self)
|
||||
self._touch = None
|
||||
super(Carousel, self).on_touch_down(touch)
|
||||
return
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
container = RelativeLayout(
|
||||
size=self.size, x=self.x - self.width, y=self.y)
|
||||
container.add_widget(widget)
|
||||
super(Carousel, self).add_widget(container, index, *args, **kwargs)
|
||||
if index != 0:
|
||||
self.slides.insert(index - len(self.slides), widget)
|
||||
else:
|
||||
self.slides.append(widget)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
# XXX be careful, the widget.parent refer to the RelativeLayout
|
||||
# added in add_widget(). But it will break if RelativeLayout
|
||||
# implementation change.
|
||||
# if we passed the real widget
|
||||
slides = self.slides
|
||||
if widget in slides:
|
||||
if self.index >= slides.index(widget):
|
||||
self.index = max(0, self.index - 1)
|
||||
container = widget.parent
|
||||
slides.remove(widget)
|
||||
super(Carousel, self).remove_widget(container, *args, **kwargs)
|
||||
container.remove_widget(widget)
|
||||
return
|
||||
super(Carousel, self).remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def clear_widgets(self, children=None, *args, **kwargs):
|
||||
# `children` must be a list of slides or None
|
||||
if children is None:
|
||||
children = self.slides[:]
|
||||
remove_widget = self.remove_widget
|
||||
for widget in children:
|
||||
remove_widget(widget)
|
||||
super(Carousel, self).clear_widgets()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.app import App
|
||||
|
||||
class Example1(App):
|
||||
|
||||
def build(self):
|
||||
carousel = Carousel(direction='left',
|
||||
loop=True)
|
||||
for i in range(4):
|
||||
src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i
|
||||
image = Factory.AsyncImage(source=src, fit_mode="contain")
|
||||
carousel.add_widget(image)
|
||||
return carousel
|
||||
|
||||
Example1().run()
|
||||
@@ -0,0 +1,197 @@
|
||||
'''
|
||||
CheckBox
|
||||
========
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
.. image:: images/checkbox.png
|
||||
:align: right
|
||||
|
||||
:class:`CheckBox` is a specific two-state button that can be either checked or
|
||||
unchecked. If the CheckBox is in a Group, it becomes a Radio button.
|
||||
As with the :class:`~kivy.uix.togglebutton.ToggleButton`, only one Radio button
|
||||
at a time can be selected when the :attr:`CheckBox.group` is set.
|
||||
|
||||
An example usage::
|
||||
|
||||
from kivy.uix.checkbox import CheckBox
|
||||
|
||||
# ...
|
||||
|
||||
def on_checkbox_active(checkbox, value):
|
||||
if value:
|
||||
print('The checkbox', checkbox, 'is active')
|
||||
else:
|
||||
print('The checkbox', checkbox, 'is inactive')
|
||||
|
||||
checkbox = CheckBox()
|
||||
checkbox.bind(active=on_checkbox_active)
|
||||
'''
|
||||
|
||||
__all__ = ('CheckBox', )
|
||||
|
||||
from kivy.properties import AliasProperty, StringProperty, ColorProperty
|
||||
from kivy.uix.behaviors import ToggleButtonBehavior
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
|
||||
class CheckBox(ToggleButtonBehavior, Widget):
|
||||
'''CheckBox class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
def _get_active(self):
|
||||
return self.state == 'down'
|
||||
|
||||
def _set_active(self, value):
|
||||
self.state = 'down' if value else 'normal'
|
||||
|
||||
active = AliasProperty(
|
||||
_get_active, _set_active, bind=('state', ), cache=True)
|
||||
'''Indicates if the switch is active or inactive.
|
||||
|
||||
:attr:`active` is a boolean and reflects and sets whether the underlying
|
||||
:attr:`~kivy.uix.button.Button.state` is 'down' (True) or 'normal' (False).
|
||||
It is a :class:`~kivy.properties.AliasProperty`, which accepts boolean
|
||||
values and defaults to False.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
|
||||
It changed from a BooleanProperty to a AliasProperty.
|
||||
'''
|
||||
|
||||
background_checkbox_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_off')
|
||||
'''Background image of the checkbox used for the default graphical
|
||||
representation when the checkbox is not active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_checkbox_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_off'.
|
||||
'''
|
||||
|
||||
background_checkbox_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_on')
|
||||
'''Background image of the checkbox used for the default graphical
|
||||
representation when the checkbox is active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_checkbox_down` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_on'.
|
||||
'''
|
||||
|
||||
background_checkbox_disabled_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_disabled_off')
|
||||
'''Background image of the checkbox used for the default graphical
|
||||
representation when the checkbox is disabled and not active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_checkbox_disabled_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_disabled_off'.
|
||||
'''
|
||||
|
||||
background_checkbox_disabled_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_disabled_on')
|
||||
'''Background image of the checkbox used for the default graphical
|
||||
representation when the checkbox is disabled and active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_checkbox_disabled_down` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_disabled_on'.
|
||||
'''
|
||||
|
||||
background_radio_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_off')
|
||||
'''Background image of the radio button used for the default graphical
|
||||
representation when the radio button is not active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_radio_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_off'.
|
||||
'''
|
||||
|
||||
background_radio_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_on')
|
||||
'''Background image of the radio button used for the default graphical
|
||||
representation when the radio button is active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_radio_down` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_on'.
|
||||
'''
|
||||
|
||||
background_radio_disabled_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_disabled_off')
|
||||
'''Background image of the radio button used for the default graphical
|
||||
representation when the radio button is disabled and not active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_radio_disabled_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_disabled_off'.
|
||||
'''
|
||||
|
||||
background_radio_disabled_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_disabled_on')
|
||||
'''Background image of the radio button used for the default graphical
|
||||
representation when the radio button is disabled and active.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`background_radio_disabled_down` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/checkbox_radio_disabled_on'.
|
||||
'''
|
||||
|
||||
color = ColorProperty([1, 1, 1, 1])
|
||||
'''Color is used for tinting the default graphical representation
|
||||
of checkbox and radio button (images).
|
||||
|
||||
Color is in the format (r, g, b, a).
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`color` is a
|
||||
:class:`~kivy.properties.ColorProperty` and defaults to
|
||||
'[1, 1, 1, 1]'.
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fbind('state', self._on_state)
|
||||
super(CheckBox, self).__init__(**kwargs)
|
||||
|
||||
def _on_state(self, instance, value):
|
||||
if self.group and self.state == 'down':
|
||||
self._release_group(self)
|
||||
|
||||
def on_group(self, *largs):
|
||||
super(CheckBox, self).on_group(*largs)
|
||||
if self.active:
|
||||
self._release_group(self)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from random import uniform
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
x = GridLayout(cols=4)
|
||||
for i in range(36):
|
||||
r, g, b = [uniform(0.2, 1.0) for j in range(3)]
|
||||
x.add_widget(CheckBox(group='1' if i % 2 else '', color=[r, g, b, 2]))
|
||||
runTouchApp(x)
|
||||
@@ -0,0 +1,238 @@
|
||||
'''
|
||||
Code Input
|
||||
==========
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. image:: images/codeinput.jpg
|
||||
|
||||
.. note::
|
||||
|
||||
This widget requires ``pygments`` package to run. Install it with ``pip``.
|
||||
|
||||
The :class:`CodeInput` provides a box of editable highlighted text like the one
|
||||
shown in the image.
|
||||
|
||||
It supports all the features provided by the :class:`~kivy.uix.textinput` as
|
||||
well as code highlighting for `languages supported by pygments
|
||||
<http://pygments.org/docs/lexers/>`_ along with `KivyLexer` for
|
||||
:mod:`kivy.lang` highlighting.
|
||||
|
||||
Usage example
|
||||
-------------
|
||||
|
||||
To create a CodeInput with highlighting for `KV language`::
|
||||
|
||||
from kivy.uix.codeinput import CodeInput
|
||||
from kivy.extras.highlight import KivyLexer
|
||||
codeinput = CodeInput(lexer=KivyLexer())
|
||||
|
||||
To create a CodeInput with highlighting for `Cython`::
|
||||
|
||||
from kivy.uix.codeinput import CodeInput
|
||||
from pygments.lexers import CythonLexer
|
||||
codeinput = CodeInput(lexer=CythonLexer())
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('CodeInput', )
|
||||
|
||||
from pygments import highlight
|
||||
from pygments import lexers
|
||||
from pygments import styles
|
||||
from pygments.formatters import BBCodeFormatter
|
||||
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.core.text.markup import MarkupLabel as Label
|
||||
from kivy.cache import Cache
|
||||
from kivy.properties import ObjectProperty, OptionProperty
|
||||
from kivy.utils import get_hex_from_color, get_color_from_hex
|
||||
from kivy.uix.behaviors import CodeNavigationBehavior
|
||||
|
||||
Cache_get = Cache.get
|
||||
Cache_append = Cache.append
|
||||
|
||||
# TODO: color chooser for keywords/strings/...
|
||||
|
||||
|
||||
class CodeInput(CodeNavigationBehavior, TextInput):
|
||||
'''CodeInput class, used for displaying highlighted code.
|
||||
'''
|
||||
|
||||
lexer = ObjectProperty(None)
|
||||
'''This holds the selected Lexer used by pygments to highlight the code.
|
||||
|
||||
|
||||
:attr:`lexer` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `PythonLexer`.
|
||||
'''
|
||||
|
||||
style_name = OptionProperty(
|
||||
'default', options=list(styles.get_all_styles())
|
||||
)
|
||||
'''Name of the pygments style to use for formatting.
|
||||
|
||||
:attr:`style_name` is an :class:`~kivy.properties.OptionProperty`
|
||||
and defaults to ``'default'``.
|
||||
|
||||
'''
|
||||
|
||||
style = ObjectProperty(None)
|
||||
'''The pygments style object to use for formatting.
|
||||
|
||||
When ``style_name`` is set, this will be changed to the
|
||||
corresponding style object.
|
||||
|
||||
:attr:`style` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to ``None``
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
stylename = kwargs.get('style_name', 'default')
|
||||
style = kwargs['style'] if 'style' in kwargs \
|
||||
else styles.get_style_by_name(stylename)
|
||||
self.formatter = BBCodeFormatter(style=style)
|
||||
self.lexer = lexers.PythonLexer()
|
||||
self.text_color = '#000000'
|
||||
self._label_cached = Label()
|
||||
self.use_text_color = True
|
||||
|
||||
super(CodeInput, self).__init__(**kwargs)
|
||||
|
||||
self._line_options = kw = self._get_line_options()
|
||||
self._label_cached = Label(**kw)
|
||||
# use text_color as foreground color
|
||||
text_color = kwargs.get('foreground_color')
|
||||
if text_color:
|
||||
self.text_color = get_hex_from_color(text_color)
|
||||
# set foreground to white to allow text colors to show
|
||||
# use text_color as the default color in bbcodes
|
||||
self.use_text_color = False
|
||||
self.foreground_color = [1, 1, 1, .999]
|
||||
if not kwargs.get('background_color'):
|
||||
self.background_color = [.9, .92, .92, 1]
|
||||
|
||||
def on_style_name(self, *args):
|
||||
self.style = styles.get_style_by_name(self.style_name)
|
||||
self.background_color = get_color_from_hex(self.style.background_color)
|
||||
self._trigger_refresh_text()
|
||||
|
||||
def on_style(self, *args):
|
||||
self.formatter = BBCodeFormatter(style=self.style)
|
||||
self._trigger_update_graphics()
|
||||
|
||||
def _create_line_label(self, text, hint=False):
|
||||
# Create a label from a text, using line options
|
||||
ntext = text.replace(u'\n', u'').replace(u'\t', u' ' * self.tab_width)
|
||||
if self.password and not hint: # Don't replace hint_text with *
|
||||
ntext = u'*' * len(ntext)
|
||||
ntext = self._get_bbcode(ntext)
|
||||
kw = self._get_line_options()
|
||||
cid = u'{}\0{}\0{}'.format(text, self.password, kw)
|
||||
texture = Cache_get('textinput.label', cid)
|
||||
|
||||
if texture is None:
|
||||
# FIXME right now, we can't render very long line...
|
||||
# if we move on "VBO" version as fallback, we won't need to
|
||||
# do this.
|
||||
# try to find the maximum text we can handle
|
||||
label = Label(text=ntext, **kw)
|
||||
if text.find(u'\n') > 0:
|
||||
label.text = u''
|
||||
else:
|
||||
label.text = ntext
|
||||
label.refresh()
|
||||
|
||||
# ok, we found it.
|
||||
texture = label.texture
|
||||
Cache_append('textinput.label', cid, texture)
|
||||
label.text = ''
|
||||
return texture
|
||||
|
||||
def _get_line_options(self):
|
||||
kw = super(CodeInput, self)._get_line_options()
|
||||
kw['markup'] = True
|
||||
kw['valign'] = 'top'
|
||||
kw['codeinput'] = repr(self.lexer)
|
||||
return kw
|
||||
|
||||
def _get_text_width(self, text, tab_width, _label_cached):
|
||||
# Return the width of a text, according to the current line options.
|
||||
cid = u'{}\0{}\0{}'.format(text, self.password,
|
||||
self._get_line_options())
|
||||
width = Cache_get('textinput.width', cid)
|
||||
if width is not None:
|
||||
return width
|
||||
lbl = self._create_line_label(text)
|
||||
width = lbl.width
|
||||
Cache_append('textinput.width', cid, width)
|
||||
return width
|
||||
|
||||
def _get_bbcode(self, ntext):
|
||||
# get bbcoded text for python
|
||||
try:
|
||||
ntext[0]
|
||||
# replace brackets with special chars that aren't highlighted
|
||||
# by pygment. can't use &bl; ... cause & is highlighted
|
||||
ntext = ntext.replace(u'[', u'\x01').replace(u']', u'\x02')
|
||||
ntext = highlight(ntext, self.lexer, self.formatter)
|
||||
ntext = ntext.replace(u'\x01', u'&bl;').replace(u'\x02', u'&br;')
|
||||
# replace special chars with &bl; and &br;
|
||||
ntext = ''.join((u'[color=', str(self.text_color), u']',
|
||||
ntext, u'[/color]'))
|
||||
ntext = ntext.replace(u'\n', u'')
|
||||
# remove possible extra highlight options
|
||||
ntext = ntext.replace(u'[u]', '').replace(u'[/u]', '')
|
||||
return ntext
|
||||
except IndexError:
|
||||
return ''
|
||||
|
||||
# overridden to prevent cursor position off screen
|
||||
def _cursor_offset(self):
|
||||
'''Get the cursor x offset on the current line
|
||||
'''
|
||||
offset = 0
|
||||
try:
|
||||
if self.cursor_col:
|
||||
offset = self._get_text_width(
|
||||
self._lines[self.cursor_row][:self.cursor_col])
|
||||
return offset
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
return offset
|
||||
|
||||
def on_lexer(self, instance, value):
|
||||
self._trigger_refresh_text()
|
||||
|
||||
def on_foreground_color(self, instance, text_color):
|
||||
if not self.use_text_color:
|
||||
self.use_text_color = True
|
||||
return
|
||||
self.text_color = get_hex_from_color(text_color)
|
||||
self.use_text_color = False
|
||||
self.foreground_color = (1, 1, 1, .999)
|
||||
self._trigger_refresh_text()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.extras.highlight import KivyLexer
|
||||
from kivy.app import App
|
||||
|
||||
class CodeInputTest(App):
|
||||
def build(self):
|
||||
return CodeInput(lexer=KivyLexer(),
|
||||
font_size=12,
|
||||
text='''
|
||||
#:kivy 1.0
|
||||
|
||||
<YourWidget>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: .5, .5, .5
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size''')
|
||||
|
||||
CodeInputTest().run()
|
||||
@@ -0,0 +1,486 @@
|
||||
'''
|
||||
Color Picker
|
||||
============
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
|
||||
.. warning::
|
||||
|
||||
This widget is experimental. Its use and API can change at any time until
|
||||
this warning is removed.
|
||||
|
||||
.. image:: images/colorpicker.png
|
||||
:align: right
|
||||
|
||||
The ColorPicker widget allows a user to select a color from a chromatic
|
||||
wheel where pinch and zoom can be used to change the wheel's saturation.
|
||||
Sliders and TextInputs are also provided for entering the RGBA/HSV/HEX values
|
||||
directly.
|
||||
|
||||
Usage::
|
||||
|
||||
clr_picker = ColorPicker()
|
||||
parent.add_widget(clr_picker)
|
||||
|
||||
# To monitor changes, we can bind to color property changes
|
||||
def on_color(instance, value):
|
||||
print("RGBA = ", str(value)) # or instance.color
|
||||
print("HSV = ", str(instance.hsv))
|
||||
print("HEX = ", str(instance.hex_color))
|
||||
|
||||
clr_picker.bind(color=on_color)
|
||||
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ColorPicker', 'ColorWheel')
|
||||
|
||||
from math import cos, sin, pi, sqrt, atan
|
||||
from colorsys import rgb_to_hsv, hsv_to_rgb
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import Mesh, InstructionGroup, Color
|
||||
from kivy.logger import Logger
|
||||
from kivy.properties import (NumericProperty, BoundedNumericProperty,
|
||||
ListProperty, ObjectProperty,
|
||||
ReferenceListProperty, StringProperty,
|
||||
AliasProperty)
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.utils import get_color_from_hex, get_hex_from_color
|
||||
|
||||
|
||||
def distance(pt1, pt2):
|
||||
return sqrt((pt1[0] - pt2[0]) ** 2. + (pt1[1] - pt2[1]) ** 2.)
|
||||
|
||||
|
||||
def polar_to_rect(origin, r, theta):
|
||||
return origin[0] + r * cos(theta), origin[1] + r * sin(theta)
|
||||
|
||||
|
||||
def rect_to_polar(origin, x, y):
|
||||
if x == origin[0]:
|
||||
if y == origin[1]:
|
||||
return 0, 0
|
||||
elif y > origin[1]:
|
||||
return y - origin[1], pi / 2
|
||||
else:
|
||||
return origin[1] - y, 3 * pi / 2
|
||||
t = atan(float((y - origin[1])) / (x - origin[0]))
|
||||
if x - origin[0] < 0:
|
||||
t += pi
|
||||
|
||||
if t < 0:
|
||||
t += 2 * pi
|
||||
|
||||
return distance((x, y), origin), t
|
||||
|
||||
|
||||
class ColorWheel(Widget):
|
||||
'''Chromatic wheel for the ColorPicker.
|
||||
|
||||
.. versionchanged:: 1.7.1
|
||||
`font_size`, `font_name` and `foreground_color` have been removed. The
|
||||
sizing is now the same as others widget, based on 'sp'. Orientation is
|
||||
also automatically determined according to the width/height ratio.
|
||||
|
||||
'''
|
||||
|
||||
r = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Red value of the color currently selected.
|
||||
|
||||
:attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and
|
||||
can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
g = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Green value of the color currently selected.
|
||||
|
||||
:attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty`
|
||||
and can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
b = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Blue value of the color currently selected.
|
||||
|
||||
:attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and
|
||||
can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
a = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Alpha value of the color currently selected.
|
||||
|
||||
:attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and
|
||||
can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
color = ReferenceListProperty(r, g, b, a)
|
||||
'''The holds the color currently selected.
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and
|
||||
contains a list of `r`, `g`, `b`, `a` values. It defaults to `[0, 0, 0, 0]`.
|
||||
'''
|
||||
|
||||
_origin = ListProperty((100, 100))
|
||||
_radius = NumericProperty(100)
|
||||
|
||||
_piece_divisions = NumericProperty(10)
|
||||
_pieces_of_pie = NumericProperty(16)
|
||||
|
||||
_inertia_slowdown = 1.25
|
||||
_inertia_cutoff = .25
|
||||
|
||||
_num_touches = 0
|
||||
_pinch_flag = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.arcs = []
|
||||
self.sv_idx = 0
|
||||
|
||||
pdv = self._piece_divisions
|
||||
self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [
|
||||
(1, float(y) / pdv) for y in reversed(range(pdv))]
|
||||
|
||||
super(ColorWheel, self).__init__(**kwargs)
|
||||
|
||||
def on__origin(self, _instance, _value):
|
||||
self._reset_canvas()
|
||||
|
||||
def on__radius(self, _instance, _value):
|
||||
self._reset_canvas()
|
||||
|
||||
def _reset_canvas(self):
|
||||
# initialize list to hold all meshes
|
||||
self.canvas.clear()
|
||||
self.arcs = []
|
||||
self.sv_idx = 0
|
||||
pdv = self._piece_divisions
|
||||
ppie = self._pieces_of_pie
|
||||
|
||||
for r in range(pdv):
|
||||
for t in range(ppie):
|
||||
self.arcs.append(
|
||||
_ColorArc(
|
||||
self._radius * (float(r) / float(pdv)),
|
||||
self._radius * (float(r + 1) / float(pdv)),
|
||||
2 * pi * (float(t) / float(ppie)),
|
||||
2 * pi * (float(t + 1) / float(ppie)),
|
||||
origin=self._origin,
|
||||
color=(float(t) / ppie,
|
||||
self.sv_s[self.sv_idx + r][0],
|
||||
self.sv_s[self.sv_idx + r][1],
|
||||
1)))
|
||||
|
||||
self.canvas.add(self.arcs[-1])
|
||||
|
||||
def recolor_wheel(self):
|
||||
ppie = self._pieces_of_pie
|
||||
for idx, segment in enumerate(self.arcs):
|
||||
segment.change_color(
|
||||
sv=self.sv_s[int(self.sv_idx + idx / ppie)])
|
||||
|
||||
def change_alpha(self, val):
|
||||
for idx, segment in enumerate(self.arcs):
|
||||
segment.change_color(a=val)
|
||||
|
||||
def inertial_incr_sv_idx(self, dt):
|
||||
# if its already zoomed all the way out, cancel the inertial zoom
|
||||
if self.sv_idx == len(self.sv_s) - self._piece_divisions:
|
||||
return False
|
||||
|
||||
self.sv_idx += 1
|
||||
self.recolor_wheel()
|
||||
if dt * self._inertia_slowdown > self._inertia_cutoff:
|
||||
return False
|
||||
else:
|
||||
Clock.schedule_once(self.inertial_incr_sv_idx,
|
||||
dt * self._inertia_slowdown)
|
||||
|
||||
def inertial_decr_sv_idx(self, dt):
|
||||
# if its already zoomed all the way in, cancel the inertial zoom
|
||||
if self.sv_idx == 0:
|
||||
return False
|
||||
self.sv_idx -= 1
|
||||
self.recolor_wheel()
|
||||
if dt * self._inertia_slowdown > self._inertia_cutoff:
|
||||
return False
|
||||
else:
|
||||
Clock.schedule_once(self.inertial_decr_sv_idx,
|
||||
dt * self._inertia_slowdown)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
r = self._get_touch_r(touch.pos)
|
||||
if r > self._radius:
|
||||
return False
|
||||
|
||||
# code is still set up to allow pinch to zoom, but this is
|
||||
# disabled for now since it was fiddly with small wheels.
|
||||
# Comment out these lines and adjust on_touch_move to reenable
|
||||
# this.
|
||||
if self._num_touches != 0:
|
||||
return False
|
||||
|
||||
touch.grab(self)
|
||||
self._num_touches += 1
|
||||
touch.ud['anchor_r'] = r
|
||||
touch.ud['orig_sv_idx'] = self.sv_idx
|
||||
touch.ud['orig_time'] = Clock.get_time()
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
r = self._get_touch_r(touch.pos)
|
||||
goal_sv_idx = (touch.ud['orig_sv_idx'] -
|
||||
int((r - touch.ud['anchor_r']) /
|
||||
(float(self._radius) / self._piece_divisions)))
|
||||
|
||||
if (
|
||||
goal_sv_idx != self.sv_idx and
|
||||
0 <= goal_sv_idx <= len(self.sv_s) - self._piece_divisions
|
||||
):
|
||||
# this is a pinch to zoom
|
||||
self._pinch_flag = True
|
||||
self.sv_idx = goal_sv_idx
|
||||
self.recolor_wheel()
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self._num_touches -= 1
|
||||
if self._pinch_flag:
|
||||
if self._num_touches == 0:
|
||||
# user was pinching, and now both fingers are up. Return
|
||||
# to normal
|
||||
if self.sv_idx > touch.ud['orig_sv_idx']:
|
||||
Clock.schedule_once(
|
||||
self.inertial_incr_sv_idx,
|
||||
(Clock.get_time() - touch.ud['orig_time']) /
|
||||
(self.sv_idx - touch.ud['orig_sv_idx']))
|
||||
|
||||
if self.sv_idx < touch.ud['orig_sv_idx']:
|
||||
Clock.schedule_once(
|
||||
self.inertial_decr_sv_idx,
|
||||
(Clock.get_time() - touch.ud['orig_time']) /
|
||||
(self.sv_idx - touch.ud['orig_sv_idx']))
|
||||
|
||||
self._pinch_flag = False
|
||||
return
|
||||
else:
|
||||
# user was pinching, and at least one finger remains. We
|
||||
# don't want to treat the remaining fingers as touches
|
||||
return
|
||||
else:
|
||||
r, theta = rect_to_polar(self._origin, *touch.pos)
|
||||
# if touch up is outside the wheel, ignore
|
||||
if r >= self._radius:
|
||||
return
|
||||
# compute which ColorArc is being touched (they aren't
|
||||
# widgets so we don't get collide_point) and set
|
||||
# _hsv based on the selected ColorArc
|
||||
piece = int((theta / (2 * pi)) * self._pieces_of_pie)
|
||||
division = int((r / self._radius) * self._piece_divisions)
|
||||
hsva = list(
|
||||
self.arcs[self._pieces_of_pie * division + piece].color)
|
||||
self.color = list(hsv_to_rgb(*hsva[:3])) + hsva[-1:]
|
||||
|
||||
def _get_touch_r(self, pos):
|
||||
return distance(pos, self._origin)
|
||||
|
||||
|
||||
class _ColorArc(InstructionGroup):
|
||||
def __init__(self, r_min, r_max, theta_min, theta_max,
|
||||
color=(0, 0, 1, 1), origin=(0, 0), **kwargs):
|
||||
super(_ColorArc, self).__init__(**kwargs)
|
||||
self.origin = origin
|
||||
self.r_min = r_min
|
||||
self.r_max = r_max
|
||||
self.theta_min = theta_min
|
||||
self.theta_max = theta_max
|
||||
self.color = color
|
||||
self.color_instr = Color(*color, mode='hsv')
|
||||
self.add(self.color_instr)
|
||||
self.mesh = self.get_mesh()
|
||||
self.add(self.mesh)
|
||||
|
||||
def __str__(self):
|
||||
return "r_min: %s r_max: %s theta_min: %s theta_max: %s color: %s" % (
|
||||
self.r_min, self.r_max, self.theta_min, self.theta_max, self.color
|
||||
)
|
||||
|
||||
def get_mesh(self):
|
||||
v = []
|
||||
# first calculate the distance between endpoints of the outer
|
||||
# arc, so we know how many steps to use when calculating
|
||||
# vertices
|
||||
theta_step_outer = 0.1
|
||||
theta = self.theta_max - self.theta_min
|
||||
d_outer = int(theta / theta_step_outer)
|
||||
theta_step_outer = theta / d_outer
|
||||
|
||||
if self.r_min == 0:
|
||||
for x in range(0, d_outer, 2):
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + x * theta_step_outer
|
||||
) * 2)
|
||||
v += polar_to_rect(self.origin, 0, 0) * 2
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + (x + 1) * theta_step_outer
|
||||
) * 2)
|
||||
if not d_outer & 1: # add a last point if d_outer is even
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + d_outer * theta_step_outer
|
||||
) * 2)
|
||||
else:
|
||||
for x in range(d_outer + 1):
|
||||
v += (polar_to_rect(self.origin, self.r_min,
|
||||
self.theta_min + x * theta_step_outer
|
||||
) * 2)
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + x * theta_step_outer
|
||||
) * 2)
|
||||
|
||||
return Mesh(vertices=v, indices=range(int(len(v) / 4)),
|
||||
mode='triangle_strip')
|
||||
|
||||
def change_color(self, color=None, color_delta=None, sv=None, a=None):
|
||||
self.remove(self.color_instr)
|
||||
if color is not None:
|
||||
self.color = color
|
||||
elif color_delta is not None:
|
||||
self.color = [self.color[i] + color_delta[i] for i in range(4)]
|
||||
elif sv is not None:
|
||||
self.color = (self.color[0], sv[0], sv[1], self.color[3])
|
||||
elif a is not None:
|
||||
self.color = (self.color[0], self.color[1], self.color[2], a)
|
||||
self.color_instr = Color(*self.color, mode='hsv')
|
||||
self.insert(0, self.color_instr)
|
||||
|
||||
|
||||
class ColorPicker(RelativeLayout):
|
||||
'''
|
||||
See module documentation.
|
||||
'''
|
||||
|
||||
font_name = StringProperty('data/fonts/RobotoMono-Regular.ttf')
|
||||
'''Specifies the font used on the ColorPicker.
|
||||
|
||||
:attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'data/fonts/RobotoMono-Regular.ttf'.
|
||||
'''
|
||||
|
||||
color = ListProperty((1, 1, 1, 1))
|
||||
'''The :attr:`color` holds the color currently selected in rgba format.
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(1, 1, 1, 1).
|
||||
'''
|
||||
|
||||
def _get_hsv(self):
|
||||
return rgb_to_hsv(*self.color[:3])
|
||||
|
||||
def _set_hsv(self, value):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self.set_color(value)
|
||||
|
||||
hsv = AliasProperty(_get_hsv, _set_hsv, bind=('color', ))
|
||||
'''The :attr:`hsv` holds the color currently selected in hsv format.
|
||||
|
||||
:attr:`hsv` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(1, 1, 1).
|
||||
'''
|
||||
def _get_hex(self):
|
||||
return get_hex_from_color(self.color)
|
||||
|
||||
def _set_hex(self, value):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self.set_color(get_color_from_hex(value)[:4])
|
||||
|
||||
hex_color = AliasProperty(_get_hex, _set_hex, bind=('color',), cache=True)
|
||||
'''The :attr:`hex_color` holds the currently selected color in hex.
|
||||
|
||||
:attr:`hex_color` is an :class:`~kivy.properties.AliasProperty` and
|
||||
defaults to `#ffffffff`.
|
||||
'''
|
||||
|
||||
wheel = ObjectProperty(None)
|
||||
'''The :attr:`wheel` holds the color wheel.
|
||||
|
||||
:attr:`wheel` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
_update_clr_ev = _update_hex_ev = None
|
||||
|
||||
# now used only internally.
|
||||
foreground_color = ListProperty((1, 1, 1, 1))
|
||||
|
||||
def _trigger_update_clr(self, mode, clr_idx, text):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self._updating_clr = True
|
||||
self._upd_clr_list = mode, clr_idx, text
|
||||
ev = self._update_clr_ev
|
||||
if ev is None:
|
||||
ev = self._update_clr_ev = Clock.create_trigger(self._update_clr)
|
||||
ev()
|
||||
|
||||
def _update_clr(self, dt):
|
||||
# to prevent interaction between hsv/rgba, we work internally using rgba
|
||||
mode, clr_idx, text = self._upd_clr_list
|
||||
try:
|
||||
text = min(255.0, max(0.0, float(text)))
|
||||
if mode == 'rgb':
|
||||
self.color[clr_idx] = text / 255
|
||||
else:
|
||||
hsv = list(self.hsv[:])
|
||||
hsv[clr_idx] = text / 255
|
||||
self.color[:3] = hsv_to_rgb(*hsv)
|
||||
except ValueError:
|
||||
Logger.warning('ColorPicker: invalid value : {}'.format(text))
|
||||
finally:
|
||||
self._updating_clr = False
|
||||
|
||||
def _update_hex(self, dt):
|
||||
try:
|
||||
if len(self._upd_hex_list) != 9:
|
||||
return
|
||||
self._updating_clr = False
|
||||
self.hex_color = self._upd_hex_list
|
||||
finally:
|
||||
self._updating_clr = False
|
||||
|
||||
def _trigger_update_hex(self, text):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self._updating_clr = True
|
||||
self._upd_hex_list = text
|
||||
ev = self._update_hex_ev
|
||||
if ev is None:
|
||||
ev = self._update_hex_ev = Clock.create_trigger(self._update_hex)
|
||||
ev()
|
||||
|
||||
def set_color(self, color):
|
||||
self._updating_clr = True
|
||||
if len(color) == 3:
|
||||
self.color[:3] = color
|
||||
else:
|
||||
self.color = color
|
||||
self._updating_clr = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._updating_clr = False
|
||||
super(ColorPicker, self).__init__(**kwargs)
|
||||
|
||||
|
||||
if __name__ in ('__android__', '__main__'):
|
||||
from kivy.app import App
|
||||
|
||||
class ColorPickerApp(App):
|
||||
def build(self):
|
||||
cp = ColorPicker(pos_hint={'center_x': .5, 'center_y': .5},
|
||||
size_hint=(1, 1))
|
||||
return cp
|
||||
ColorPickerApp().run()
|
||||
@@ -0,0 +1,391 @@
|
||||
'''
|
||||
Drop-Down List
|
||||
==============
|
||||
|
||||
.. image:: images/dropdown.gif
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
A versatile drop-down list that can be used with custom widgets. It allows you
|
||||
to display a list of widgets under a displayed widget. Unlike other toolkits,
|
||||
the list of widgets can contain any type of widget: simple buttons,
|
||||
images etc.
|
||||
|
||||
The positioning of the drop-down list is fully automatic: we will always try to
|
||||
place the dropdown list in a way that the user can select an item in the list.
|
||||
|
||||
Basic example
|
||||
-------------
|
||||
|
||||
A button with a dropdown list of 10 possible values. All the buttons within the
|
||||
dropdown list will trigger the dropdown :meth:`DropDown.select` method. After
|
||||
being called, the main button text will display the selection of the
|
||||
dropdown. ::
|
||||
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.button import Button
|
||||
from kivy.base import runTouchApp
|
||||
|
||||
# create a dropdown with 10 buttons
|
||||
dropdown = DropDown()
|
||||
for index in range(10):
|
||||
# When adding widgets, we need to specify the height manually
|
||||
# (disabling the size_hint_y) so the dropdown can calculate
|
||||
# the area it needs.
|
||||
|
||||
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
|
||||
|
||||
# for each button, attach a callback that will call the select() method
|
||||
# on the dropdown. We'll pass the text of the button as the data of the
|
||||
# selection.
|
||||
btn.bind(on_release=lambda btn: dropdown.select(btn.text))
|
||||
|
||||
# then add the button inside the dropdown
|
||||
dropdown.add_widget(btn)
|
||||
|
||||
# create a big main button
|
||||
mainbutton = Button(text='Hello', size_hint=(None, None))
|
||||
|
||||
# show the dropdown menu when the main button is released
|
||||
# note: all the bind() calls pass the instance of the caller (here, the
|
||||
# mainbutton instance) as the first argument of the callback (here,
|
||||
# dropdown.open.).
|
||||
mainbutton.bind(on_release=dropdown.open)
|
||||
|
||||
# one last thing, listen for the selection in the dropdown list and
|
||||
# assign the data to the button text.
|
||||
dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
|
||||
|
||||
runTouchApp(mainbutton)
|
||||
|
||||
Extending dropdown in Kv
|
||||
------------------------
|
||||
|
||||
You could create a dropdown directly from your kv::
|
||||
|
||||
#:kivy 1.4.0
|
||||
<CustomDropDown>:
|
||||
Button:
|
||||
text: 'My first Item'
|
||||
size_hint_y: None
|
||||
height: 44
|
||||
on_release: root.select('item1')
|
||||
Label:
|
||||
text: 'Unselectable item'
|
||||
size_hint_y: None
|
||||
height: 44
|
||||
Button:
|
||||
text: 'My second Item'
|
||||
size_hint_y: None
|
||||
height: 44
|
||||
on_release: root.select('item2')
|
||||
|
||||
And then, create the associated python class and use it::
|
||||
|
||||
class CustomDropDown(DropDown):
|
||||
pass
|
||||
|
||||
dropdown = CustomDropDown()
|
||||
mainbutton = Button(text='Hello', size_hint=(None, None))
|
||||
mainbutton.bind(on_release=dropdown.open)
|
||||
dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
|
||||
'''
|
||||
|
||||
__all__ = ('DropDown', )
|
||||
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.properties import ObjectProperty, NumericProperty, BooleanProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.lang import Builder
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
|
||||
_grid_kv = '''
|
||||
GridLayout:
|
||||
size_hint_y: None
|
||||
height: self.minimum_size[1]
|
||||
cols: 1
|
||||
'''
|
||||
|
||||
|
||||
class DropDownException(Exception):
|
||||
'''DropDownException class.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class DropDown(ScrollView):
|
||||
'''DropDown class. See module documentation for more information.
|
||||
|
||||
:Events:
|
||||
`on_select`: data
|
||||
Fired when a selection is done. The data of the selection is passed
|
||||
in as the first argument and is what you pass in the :meth:`select`
|
||||
method as the first argument.
|
||||
`on_dismiss`:
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Fired when the DropDown is dismissed, either on selection or on
|
||||
touching outside the widget.
|
||||
'''
|
||||
|
||||
auto_width = BooleanProperty(True)
|
||||
'''By default, the width of the dropdown will be the same as the width of
|
||||
the attached widget. Set to False if you want to provide your own width.
|
||||
|
||||
:attr:`auto_width` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
max_height = NumericProperty(None, allownone=True)
|
||||
'''Indicate the maximum height that the dropdown can take. If None, it will
|
||||
take the maximum height available until the top or bottom of the screen
|
||||
is reached.
|
||||
|
||||
:attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
dismiss_on_select = BooleanProperty(True)
|
||||
'''By default, the dropdown will be automatically dismissed when a
|
||||
selection has been done. Set to False to prevent the dismiss.
|
||||
|
||||
:attr:`dismiss_on_select` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
auto_dismiss = BooleanProperty(True)
|
||||
'''By default, the dropdown will be automatically dismissed when a
|
||||
touch happens outside of it, this option allows to disable this
|
||||
feature
|
||||
|
||||
:attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
min_state_time = NumericProperty(0)
|
||||
'''Minimum time before the :class:`~kivy.uix.DropDown` is dismissed.
|
||||
This is used to allow for the widget inside the dropdown to display
|
||||
a down state or for the :class:`~kivy.uix.DropDown` itself to
|
||||
display a animation for closing.
|
||||
|
||||
:attr:`min_state_time` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to the `Config` value `min_state_time`.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
attach_to = ObjectProperty(allownone=True)
|
||||
'''(internal) Property that will be set to the widget to which the
|
||||
drop down list is attached.
|
||||
|
||||
The :meth:`open` method will automatically set this property whilst
|
||||
:meth:`dismiss` will set it back to None.
|
||||
'''
|
||||
|
||||
container = ObjectProperty()
|
||||
'''(internal) Property that will be set to the container of the dropdown
|
||||
list. It is a :class:`~kivy.uix.gridlayout.GridLayout` by default.
|
||||
'''
|
||||
|
||||
_touch_started_inside = None
|
||||
|
||||
__events__ = ('on_select', 'on_dismiss')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._win = None
|
||||
if 'min_state_time' not in kwargs:
|
||||
self.min_state_time = float(
|
||||
Config.get('graphics', 'min_state_time'))
|
||||
if 'container' not in kwargs:
|
||||
c = self.container = Builder.load_string(_grid_kv)
|
||||
else:
|
||||
c = None
|
||||
if 'do_scroll_x' not in kwargs:
|
||||
self.do_scroll_x = False
|
||||
if 'size_hint' not in kwargs:
|
||||
if 'size_hint_x' not in kwargs:
|
||||
self.size_hint_x = None
|
||||
if 'size_hint_y' not in kwargs:
|
||||
self.size_hint_y = None
|
||||
super(DropDown, self).__init__(**kwargs)
|
||||
if c is not None:
|
||||
super(DropDown, self).add_widget(c)
|
||||
self.on_container(self, c)
|
||||
Window.bind(
|
||||
on_key_down=self.on_key_down,
|
||||
size=self._reposition)
|
||||
self.fbind('size', self._reposition)
|
||||
|
||||
def on_key_down(self, instance, key, scancode, codepoint, modifiers):
|
||||
if key == 27 and self.get_parent_window():
|
||||
self.dismiss()
|
||||
return True
|
||||
|
||||
def on_container(self, instance, value):
|
||||
if value is not None:
|
||||
self.container.bind(minimum_size=self._reposition)
|
||||
|
||||
def open(self, widget):
|
||||
'''Open the dropdown list and attach it to a specific widget.
|
||||
Depending on the position of the widget within the window and
|
||||
the height of the dropdown, the dropdown might be above or below
|
||||
that widget.
|
||||
'''
|
||||
# ensure we are not already attached
|
||||
if self.attach_to is not None:
|
||||
self.dismiss()
|
||||
|
||||
# we will attach ourself to the main window, so ensure the
|
||||
# widget we are looking for have a window
|
||||
self._win = widget.get_parent_window()
|
||||
if self._win is None:
|
||||
raise DropDownException(
|
||||
'Cannot open a dropdown list on a hidden widget')
|
||||
|
||||
self.attach_to = widget
|
||||
widget.bind(pos=self._reposition, size=self._reposition)
|
||||
self._reposition()
|
||||
|
||||
# attach ourself to the main window
|
||||
self._win.add_widget(self)
|
||||
|
||||
def dismiss(self, *largs):
|
||||
'''Remove the dropdown widget from the window and detach it from
|
||||
the attached widget.
|
||||
'''
|
||||
Clock.schedule_once(self._real_dismiss, self.min_state_time)
|
||||
|
||||
def _real_dismiss(self, *largs):
|
||||
if self.parent:
|
||||
self.parent.remove_widget(self)
|
||||
if self.attach_to:
|
||||
self.attach_to.unbind(pos=self._reposition, size=self._reposition)
|
||||
self.attach_to = None
|
||||
self.dispatch('on_dismiss')
|
||||
|
||||
def on_dismiss(self):
|
||||
pass
|
||||
|
||||
def select(self, data):
|
||||
'''Call this method to trigger the `on_select` event with the `data`
|
||||
selection. The `data` can be anything you want.
|
||||
'''
|
||||
self.dispatch('on_select', data)
|
||||
if self.dismiss_on_select:
|
||||
self.dismiss()
|
||||
|
||||
def on_select(self, data):
|
||||
pass
|
||||
|
||||
def add_widget(self, *args, **kwargs):
|
||||
if self.container:
|
||||
return self.container.add_widget(*args, **kwargs)
|
||||
return super(DropDown, self).add_widget(*args, **kwargs)
|
||||
|
||||
def remove_widget(self, *args, **kwargs):
|
||||
if self.container:
|
||||
return self.container.remove_widget(*args, **kwargs)
|
||||
return super(DropDown, self).remove_widget(*args, **kwargs)
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
if self.container:
|
||||
return self.container.clear_widgets(*args, **kwargs)
|
||||
return super(DropDown, self).clear_widgets(*args, **kwargs)
|
||||
|
||||
def on_motion(self, etype, me):
|
||||
super().on_motion(etype, me)
|
||||
return True
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
self._touch_started_inside = self.collide_point(*touch.pos)
|
||||
if not self.auto_dismiss or self._touch_started_inside:
|
||||
super(DropDown, self).on_touch_down(touch)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if not self.auto_dismiss or self._touch_started_inside:
|
||||
super(DropDown, self).on_touch_move(touch)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
# Explicitly test for False as None occurs when shown by on_touch_down
|
||||
if self.auto_dismiss and self._touch_started_inside is False:
|
||||
self.dismiss()
|
||||
else:
|
||||
super(DropDown, self).on_touch_up(touch)
|
||||
self._touch_started_inside = None
|
||||
return True
|
||||
|
||||
def _reposition(self, *largs):
|
||||
# calculate the coordinate of the attached widget in the window
|
||||
# coordinate system
|
||||
win = self._win
|
||||
if not win:
|
||||
return
|
||||
widget = self.attach_to
|
||||
if not widget or not widget.get_parent_window():
|
||||
return
|
||||
wx, wy = widget.to_window(*widget.pos)
|
||||
wright, wtop = widget.to_window(widget.right, widget.top)
|
||||
|
||||
if self.auto_width:
|
||||
self.width = wright - wx
|
||||
|
||||
# ensure the dropdown list doesn't get out on the X axis, with a
|
||||
# preference to 0 in case the list is too wide.
|
||||
x = wx
|
||||
if x + self.width > win.width:
|
||||
x = win.width - self.width
|
||||
if x < 0:
|
||||
x = 0
|
||||
self.x = x
|
||||
|
||||
# determine if we display the dropdown upper or lower to the widget
|
||||
if self.max_height is not None:
|
||||
height = min(self.max_height, self.container.minimum_height)
|
||||
else:
|
||||
height = self.container.minimum_height
|
||||
|
||||
h_bottom = wy - height
|
||||
h_top = win.height - (wtop + height)
|
||||
if h_bottom > 0:
|
||||
self.top = wy
|
||||
self.height = height
|
||||
elif h_top > 0:
|
||||
self.y = wtop
|
||||
self.height = height
|
||||
else:
|
||||
# none of both top/bottom have enough place to display the
|
||||
# widget at the current size. Take the best side, and fit to
|
||||
# it.
|
||||
if h_top < h_bottom:
|
||||
self.top = self.height = wy
|
||||
else:
|
||||
self.y = wtop
|
||||
self.height = win.height - wtop
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.uix.button import Button
|
||||
from kivy.base import runTouchApp
|
||||
|
||||
def show_dropdown(button, *largs):
|
||||
dp = DropDown()
|
||||
dp.bind(on_select=lambda instance, x: setattr(button, 'text', x))
|
||||
for i in range(10):
|
||||
item = Button(text='hello %d' % i, size_hint_y=None, height=44)
|
||||
item.bind(on_release=lambda btn: dp.select(btn.text))
|
||||
dp.add_widget(item)
|
||||
dp.open(button)
|
||||
|
||||
def touch_move(instance, touch):
|
||||
instance.center = touch.pos
|
||||
|
||||
btn = Button(text='SHOW', size_hint=(None, None), pos=(300, 200))
|
||||
btn.bind(on_release=show_dropdown, on_touch_move=touch_move)
|
||||
|
||||
runTouchApp(btn)
|
||||
@@ -0,0 +1,772 @@
|
||||
'''
|
||||
EffectWidget
|
||||
============
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
The :class:`EffectWidget` is able to apply a variety of fancy
|
||||
graphical effects to
|
||||
its children. It works by rendering to a series of
|
||||
:class:`~kivy.graphics.Fbo` instances with custom opengl fragment shaders.
|
||||
As such, effects can freely do almost anything, from inverting the
|
||||
colors of the widget, to anti-aliasing, to emulating the appearance of a
|
||||
crt monitor!
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
The basic usage is as follows::
|
||||
|
||||
w = EffectWidget()
|
||||
w.add_widget(Button(text='Hello!')
|
||||
w.effects = [InvertEffect(), HorizontalBlurEffect(size=2.0)]
|
||||
|
||||
The equivalent in kv would be::
|
||||
|
||||
#: import ew kivy.uix.effectwidget
|
||||
EffectWidget:
|
||||
effects: ew.InvertEffect(), ew.HorizontalBlurEffect(size=2.0)
|
||||
Button:
|
||||
text: 'Hello!'
|
||||
|
||||
The effects can be a list of effects of any length, and they will be
|
||||
applied sequentially.
|
||||
|
||||
The module comes with a range of prebuilt effects, but the interface
|
||||
is designed to make it easy to create your own. Instead of writing a
|
||||
full glsl shader, you provide a single function that takes
|
||||
some inputs based on the screen (current pixel color, current widget
|
||||
texture etc.). See the sections below for more information.
|
||||
|
||||
Usage Guidelines
|
||||
----------------
|
||||
|
||||
It is not efficient to resize an :class:`EffectWidget`, as
|
||||
the :class:`~kivy.graphics.Fbo` is recreated on each resize event.
|
||||
If you need to resize frequently, consider doing things a different
|
||||
way.
|
||||
|
||||
Although some effects have adjustable parameters, it is
|
||||
*not* efficient to animate these, as the entire
|
||||
shader is reconstructed every time. You should use glsl
|
||||
uniform variables instead. The :class:`AdvancedEffectBase`
|
||||
may make this easier.
|
||||
|
||||
.. note:: The :class:`EffectWidget` *cannot* draw outside its own
|
||||
widget area (pos -> pos + size). Any child widgets
|
||||
overlapping the boundary will be cut off at this point.
|
||||
|
||||
Provided Effects
|
||||
----------------
|
||||
|
||||
The module comes with several pre-written effects. Some have
|
||||
adjustable properties (e.g. blur radius). Please see the individual
|
||||
effect documentation for more details.
|
||||
|
||||
- :class:`MonochromeEffect` - makes the widget grayscale.
|
||||
- :class:`InvertEffect` - inverts the widget colors.
|
||||
- :class:`ChannelMixEffect` - swaps color channels.
|
||||
- :class:`ScanlinesEffect` - displays flickering scanlines.
|
||||
- :class:`PixelateEffect` - pixelates the image.
|
||||
- :class:`HorizontalBlurEffect` - Gaussuan blurs horizontally.
|
||||
- :class:`VerticalBlurEffect` - Gaussuan blurs vertically.
|
||||
- :class:`FXAAEffect` - applies a very basic anti-aliasing.
|
||||
|
||||
Creating Effects
|
||||
----------------
|
||||
|
||||
Effects are designed to make it easy to create and use your own
|
||||
transformations. You do this by creating and using an instance of
|
||||
:class:`EffectBase` with your own custom :attr:`EffectBase.glsl`
|
||||
property.
|
||||
|
||||
The glsl property is a string representing part of a glsl fragment
|
||||
shader. You can include as many functions as you like (the string
|
||||
is simply spliced into the whole shader), but it
|
||||
must implement a function :code:`effect` as below::
|
||||
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{
|
||||
// ... your code here
|
||||
return something; // must be a vec4 representing the new color
|
||||
}
|
||||
|
||||
The full shader will calculate the normal pixel color at each point,
|
||||
then call your :code:`effect` function to transform it. The
|
||||
parameters are:
|
||||
|
||||
- **color**: The normal color of the current pixel (i.e. texture
|
||||
sampled at tex_coords).
|
||||
- **texture**: The texture containing the widget's normal background.
|
||||
- **tex_coords**: The normal texture_coords used to access texture.
|
||||
- **coords**: The pixel indices of the current pixel.
|
||||
|
||||
The shader code also has access to two useful uniform variables,
|
||||
:code:`time` containing the time (in seconds) since the program start,
|
||||
and :code:`resolution` containing the shape (x pixels, y pixels) of
|
||||
the widget.
|
||||
|
||||
For instance, the following simple string (taken from the `InvertEffect`)
|
||||
would invert the input color but set alpha to 1.0::
|
||||
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{
|
||||
return vec4(1.0 - color.xyz, 1.0);
|
||||
}
|
||||
|
||||
You can also set the glsl by automatically loading the string from a
|
||||
file, simply set the :attr:`EffectBase.source` property of an effect.
|
||||
|
||||
'''
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.properties import (StringProperty, ObjectProperty, ListProperty,
|
||||
NumericProperty, DictProperty)
|
||||
from kivy.graphics import (RenderContext, Fbo, Color, Rectangle,
|
||||
Translate, PushMatrix, PopMatrix, ClearColor,
|
||||
ClearBuffers)
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.base import EventLoop
|
||||
from kivy.resources import resource_find
|
||||
from kivy.logger import Logger
|
||||
|
||||
__all__ = ('EffectWidget', 'EffectBase', 'AdvancedEffectBase',
|
||||
'MonochromeEffect', 'InvertEffect', 'ChannelMixEffect',
|
||||
'ScanlinesEffect', 'PixelateEffect',
|
||||
'HorizontalBlurEffect', 'VerticalBlurEffect',
|
||||
'FXAAEffect')
|
||||
|
||||
shader_header = '''
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
/* Outputs from the vertex shader */
|
||||
varying vec4 frag_color;
|
||||
varying vec2 tex_coord0;
|
||||
|
||||
/* uniform texture samplers */
|
||||
uniform sampler2D texture0;
|
||||
'''
|
||||
|
||||
shader_uniforms = '''
|
||||
uniform vec2 resolution;
|
||||
uniform float time;
|
||||
'''
|
||||
|
||||
shader_footer_trivial = '''
|
||||
void main (void){
|
||||
gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
|
||||
}
|
||||
'''
|
||||
|
||||
shader_footer_effect = '''
|
||||
void main (void){
|
||||
vec4 normal_color = frag_color * texture2D(texture0, tex_coord0);
|
||||
vec4 effect_color = effect(normal_color, texture0, tex_coord0,
|
||||
gl_FragCoord.xy);
|
||||
gl_FragColor = effect_color;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
effect_trivial = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{
|
||||
return color;
|
||||
}
|
||||
'''
|
||||
|
||||
effect_monochrome = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{
|
||||
float mag = 1.0/3.0 * (color.x + color.y + color.z);
|
||||
return vec4(mag, mag, mag, color.w);
|
||||
}
|
||||
'''
|
||||
|
||||
effect_invert = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{
|
||||
return vec4(1.0 - color.xyz, color.w);
|
||||
}
|
||||
'''
|
||||
|
||||
effect_mix = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{{
|
||||
return vec4(color.{}, color.{}, color.{}, color.w);
|
||||
}}
|
||||
'''
|
||||
|
||||
effect_blur_h = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{{
|
||||
float dt = ({} / 4.0) * 1.0 / resolution.x;
|
||||
vec4 sum = vec4(0.0);
|
||||
sum += texture2D(texture, vec2(tex_coords.x - 4.0*dt, tex_coords.y))
|
||||
* 0.05;
|
||||
sum += texture2D(texture, vec2(tex_coords.x - 3.0*dt, tex_coords.y))
|
||||
* 0.09;
|
||||
sum += texture2D(texture, vec2(tex_coords.x - 2.0*dt, tex_coords.y))
|
||||
* 0.12;
|
||||
sum += texture2D(texture, vec2(tex_coords.x - dt, tex_coords.y))
|
||||
* 0.15;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y))
|
||||
* 0.16;
|
||||
sum += texture2D(texture, vec2(tex_coords.x + dt, tex_coords.y))
|
||||
* 0.15;
|
||||
sum += texture2D(texture, vec2(tex_coords.x + 2.0*dt, tex_coords.y))
|
||||
* 0.12;
|
||||
sum += texture2D(texture, vec2(tex_coords.x + 3.0*dt, tex_coords.y))
|
||||
* 0.09;
|
||||
sum += texture2D(texture, vec2(tex_coords.x + 4.0*dt, tex_coords.y))
|
||||
* 0.05;
|
||||
return vec4(sum.xyz, color.w);
|
||||
}}
|
||||
'''
|
||||
|
||||
effect_blur_v = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{{
|
||||
float dt = ({} / 4.0)
|
||||
* 1.0 / resolution.x;
|
||||
vec4 sum = vec4(0.0);
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 4.0*dt))
|
||||
* 0.05;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 3.0*dt))
|
||||
* 0.09;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 2.0*dt))
|
||||
* 0.12;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - dt))
|
||||
* 0.15;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y))
|
||||
* 0.16;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + dt))
|
||||
* 0.15;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 2.0*dt))
|
||||
* 0.12;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 3.0*dt))
|
||||
* 0.09;
|
||||
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 4.0*dt))
|
||||
* 0.05;
|
||||
return vec4(sum.xyz, color.w);
|
||||
}}
|
||||
'''
|
||||
|
||||
effect_postprocessing = '''
|
||||
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
|
||||
{
|
||||
vec2 q = tex_coords * vec2(1, -1);
|
||||
vec2 uv = 0.5 + (q-0.5);//*(0.9);// + 0.1*sin(0.2*time));
|
||||
|
||||
vec3 oricol = texture2D(texture,vec2(q.x,1.0-q.y)).xyz;
|
||||
vec3 col;
|
||||
|
||||
col.r = texture2D(texture,vec2(uv.x+0.003,-uv.y)).x;
|
||||
col.g = texture2D(texture,vec2(uv.x+0.000,-uv.y)).y;
|
||||
col.b = texture2D(texture,vec2(uv.x-0.003,-uv.y)).z;
|
||||
|
||||
col = clamp(col*0.5+0.5*col*col*1.2,0.0,1.0);
|
||||
|
||||
//col *= 0.5 + 0.5*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y);
|
||||
|
||||
col *= vec3(0.8,1.0,0.7);
|
||||
|
||||
col *= 0.9+0.1*sin(10.0*time+uv.y*1000.0);
|
||||
|
||||
col *= 0.97+0.03*sin(110.0*time);
|
||||
|
||||
float comp = smoothstep( 0.2, 0.7, sin(time) );
|
||||
//col = mix( col, oricol, clamp(-2.0+2.0*q.x+3.0*comp,0.0,1.0) );
|
||||
|
||||
return vec4(col, color.w);
|
||||
}
|
||||
'''
|
||||
|
||||
effect_pixelate = '''
|
||||
vec4 effect(vec4 vcolor, sampler2D texture, vec2 texcoord, vec2 pixel_coords)
|
||||
{{
|
||||
vec2 pixelSize = {} / resolution;
|
||||
|
||||
vec2 xy = floor(texcoord/pixelSize)*pixelSize + pixelSize/2.0;
|
||||
|
||||
return texture2D(texture, xy);
|
||||
}}
|
||||
'''
|
||||
|
||||
effect_fxaa = '''
|
||||
vec4 effect( vec4 color, sampler2D buf0, vec2 texCoords, vec2 coords)
|
||||
{
|
||||
|
||||
vec2 frameBufSize = resolution;
|
||||
|
||||
float FXAA_SPAN_MAX = 8.0;
|
||||
float FXAA_REDUCE_MUL = 1.0/8.0;
|
||||
float FXAA_REDUCE_MIN = 1.0/128.0;
|
||||
|
||||
vec3 rgbNW=texture2D(buf0,texCoords+(vec2(-1.0,-1.0)/frameBufSize)).xyz;
|
||||
vec3 rgbNE=texture2D(buf0,texCoords+(vec2(1.0,-1.0)/frameBufSize)).xyz;
|
||||
vec3 rgbSW=texture2D(buf0,texCoords+(vec2(-1.0,1.0)/frameBufSize)).xyz;
|
||||
vec3 rgbSE=texture2D(buf0,texCoords+(vec2(1.0,1.0)/frameBufSize)).xyz;
|
||||
vec3 rgbM=texture2D(buf0,texCoords).xyz;
|
||||
|
||||
vec3 luma=vec3(0.299, 0.587, 0.114);
|
||||
float lumaNW = dot(rgbNW, luma);
|
||||
float lumaNE = dot(rgbNE, luma);
|
||||
float lumaSW = dot(rgbSW, luma);
|
||||
float lumaSE = dot(rgbSE, luma);
|
||||
float lumaM = dot(rgbM, luma);
|
||||
|
||||
float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
|
||||
float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
|
||||
|
||||
vec2 dir;
|
||||
dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
|
||||
dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));
|
||||
|
||||
float dirReduce = max(
|
||||
(lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL),
|
||||
FXAA_REDUCE_MIN);
|
||||
|
||||
float rcpDirMin = 1.0/(min(abs(dir.x), abs(dir.y)) + dirReduce);
|
||||
|
||||
dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX),
|
||||
max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
|
||||
dir * rcpDirMin)) / frameBufSize;
|
||||
|
||||
vec3 rgbA = (1.0/2.0) * (
|
||||
texture2D(buf0, texCoords.xy + dir * (1.0/3.0 - 0.5)).xyz +
|
||||
texture2D(buf0, texCoords.xy + dir * (2.0/3.0 - 0.5)).xyz);
|
||||
vec3 rgbB = rgbA * (1.0/2.0) + (1.0/4.0) * (
|
||||
texture2D(buf0, texCoords.xy + dir * (0.0/3.0 - 0.5)).xyz +
|
||||
texture2D(buf0, texCoords.xy + dir * (3.0/3.0 - 0.5)).xyz);
|
||||
float lumaB = dot(rgbB, luma);
|
||||
|
||||
vec4 return_color;
|
||||
if((lumaB < lumaMin) || (lumaB > lumaMax)){
|
||||
return_color = vec4(rgbA, color.w);
|
||||
}else{
|
||||
return_color = vec4(rgbB, color.w);
|
||||
}
|
||||
|
||||
return return_color;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class EffectBase(EventDispatcher):
|
||||
'''The base class for GLSL effects. It simply returns its input.
|
||||
|
||||
See the module documentation for more details.
|
||||
|
||||
'''
|
||||
|
||||
glsl = StringProperty(effect_trivial)
|
||||
'''The glsl string defining your effect function. See the
|
||||
module documentation for more details.
|
||||
|
||||
:attr:`glsl` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to
|
||||
a trivial effect that returns its input.
|
||||
'''
|
||||
|
||||
source = StringProperty('')
|
||||
'''The (optional) filename from which to load the :attr:`glsl`
|
||||
string.
|
||||
|
||||
:attr:`source` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to ''.
|
||||
'''
|
||||
|
||||
fbo = ObjectProperty(None, allownone=True)
|
||||
'''The fbo currently using this effect. The :class:`EffectBase`
|
||||
automatically handles this.
|
||||
|
||||
:attr:`fbo` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EffectBase, self).__init__(*args, **kwargs)
|
||||
fbind = self.fbind
|
||||
fbo_shader = self.set_fbo_shader
|
||||
fbind('fbo', fbo_shader)
|
||||
fbind('glsl', fbo_shader)
|
||||
fbind('source', self._load_from_source)
|
||||
|
||||
def set_fbo_shader(self, *args):
|
||||
'''Sets the :class:`~kivy.graphics.Fbo`'s shader by splicing
|
||||
the :attr:`glsl` string into a full fragment shader.
|
||||
|
||||
The full shader is made up of :code:`shader_header +
|
||||
shader_uniforms + self.glsl + shader_footer_effect`.
|
||||
'''
|
||||
if self.fbo is None:
|
||||
return
|
||||
self.fbo.set_fs(shader_header + shader_uniforms + self.glsl +
|
||||
shader_footer_effect)
|
||||
|
||||
def _load_from_source(self, *args):
|
||||
'''(internal) Loads the glsl string from a source file.'''
|
||||
source = self.source
|
||||
if not source:
|
||||
return
|
||||
filename = resource_find(source)
|
||||
if filename is None:
|
||||
return Logger.error('Error reading file {filename}'.
|
||||
format(filename=source))
|
||||
with open(filename) as fileh:
|
||||
self.glsl = fileh.read()
|
||||
|
||||
|
||||
class AdvancedEffectBase(EffectBase):
|
||||
'''An :class:`EffectBase` with additional behavior to easily
|
||||
set and update uniform variables in your shader.
|
||||
|
||||
This class is provided for convenience when implementing your own
|
||||
effects: it is not used by any of those provided with Kivy.
|
||||
|
||||
In addition to your base glsl string that must be provided as
|
||||
normal, the :class:`AdvancedEffectBase` has an extra property
|
||||
:attr:`uniforms`, a dictionary of name-value pairs. Whenever
|
||||
a value is changed, the new value for the uniform variable is
|
||||
uploaded to the shader.
|
||||
|
||||
You must still manually declare your uniform variables at the top
|
||||
of your glsl string.
|
||||
'''
|
||||
|
||||
uniforms = DictProperty({})
|
||||
'''A dictionary of uniform variable names and their values. These
|
||||
are automatically uploaded to the :attr:`fbo` shader if appropriate.
|
||||
|
||||
uniforms is a :class:`~kivy.properties.DictProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdvancedEffectBase, self).__init__(*args, **kwargs)
|
||||
self.fbind('uniforms', self._update_uniforms)
|
||||
|
||||
def _update_uniforms(self, *args):
|
||||
if self.fbo is None:
|
||||
return
|
||||
for key, value in self.uniforms.items():
|
||||
self.fbo[key] = value
|
||||
|
||||
def set_fbo_shader(self, *args):
|
||||
super(AdvancedEffectBase, self).set_fbo_shader(*args)
|
||||
self._update_uniforms()
|
||||
|
||||
|
||||
class MonochromeEffect(EffectBase):
|
||||
'''Returns its input colors in monochrome.'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MonochromeEffect, self).__init__(*args, **kwargs)
|
||||
self.glsl = effect_monochrome
|
||||
|
||||
|
||||
class InvertEffect(EffectBase):
|
||||
'''Inverts the colors in the input.'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InvertEffect, self).__init__(*args, **kwargs)
|
||||
self.glsl = effect_invert
|
||||
|
||||
|
||||
class ScanlinesEffect(EffectBase):
|
||||
'''Adds scanlines to the input.'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ScanlinesEffect, self).__init__(*args, **kwargs)
|
||||
self.glsl = effect_postprocessing
|
||||
|
||||
|
||||
class ChannelMixEffect(EffectBase):
|
||||
'''Mixes the color channels of the input according to the order
|
||||
property. Channels may be arbitrarily rearranged or repeated.'''
|
||||
|
||||
order = ListProperty([1, 2, 0])
|
||||
'''The new sorted order of the rgb channels.
|
||||
|
||||
order is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
[1, 2, 0], corresponding to (g, b, r).
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChannelMixEffect, self).__init__(*args, **kwargs)
|
||||
self.do_glsl()
|
||||
|
||||
def on_order(self, *args):
|
||||
self.do_glsl()
|
||||
|
||||
def do_glsl(self):
|
||||
letters = [{0: 'x', 1: 'y', 2: 'z'}[i] for i in self.order]
|
||||
self.glsl = effect_mix.format(*letters)
|
||||
|
||||
|
||||
class PixelateEffect(EffectBase):
|
||||
'''Pixelates the input according to its
|
||||
:attr:`~PixelateEffect.pixel_size`'''
|
||||
|
||||
pixel_size = NumericProperty(10)
|
||||
'''
|
||||
Sets the size of a new 'pixel' in the effect, in terms of number of
|
||||
'real' pixels.
|
||||
|
||||
pixel_size is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 10.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PixelateEffect, self).__init__(*args, **kwargs)
|
||||
self.do_glsl()
|
||||
|
||||
def on_pixel_size(self, *args):
|
||||
self.do_glsl()
|
||||
|
||||
def do_glsl(self):
|
||||
self.glsl = effect_pixelate.format(float(self.pixel_size))
|
||||
|
||||
|
||||
class HorizontalBlurEffect(EffectBase):
|
||||
'''Blurs the input horizontally, with the width given by
|
||||
:attr:`~HorizontalBlurEffect.size`.'''
|
||||
|
||||
size = NumericProperty(4.0)
|
||||
'''The blur width in pixels.
|
||||
|
||||
size is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
4.0.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HorizontalBlurEffect, self).__init__(*args, **kwargs)
|
||||
self.do_glsl()
|
||||
|
||||
def on_size(self, *args):
|
||||
self.do_glsl()
|
||||
|
||||
def do_glsl(self):
|
||||
self.glsl = effect_blur_h.format(float(self.size))
|
||||
|
||||
|
||||
class VerticalBlurEffect(EffectBase):
|
||||
'''Blurs the input vertically, with the width given by
|
||||
:attr:`~VerticalBlurEffect.size`.'''
|
||||
|
||||
size = NumericProperty(4.0)
|
||||
'''The blur width in pixels.
|
||||
|
||||
size is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
4.0.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VerticalBlurEffect, self).__init__(*args, **kwargs)
|
||||
self.do_glsl()
|
||||
|
||||
def on_size(self, *args):
|
||||
self.do_glsl()
|
||||
|
||||
def do_glsl(self):
|
||||
self.glsl = effect_blur_v.format(float(self.size))
|
||||
|
||||
|
||||
class FXAAEffect(EffectBase):
|
||||
'''Applies very simple anti-aliasing via fxaa.'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FXAAEffect, self).__init__(*args, **kwargs)
|
||||
self.glsl = effect_fxaa
|
||||
|
||||
|
||||
class EffectFbo(Fbo):
|
||||
'''An :class:`~kivy.graphics.Fbo` with extra functionality that allows
|
||||
attempts to set a new shader. See :meth:`set_fs`.
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("with_stencilbuffer", True)
|
||||
super(EffectFbo, self).__init__(*args, **kwargs)
|
||||
self.texture_rectangle = None
|
||||
|
||||
def set_fs(self, value):
|
||||
'''Attempt to set the fragment shader to the given value.
|
||||
If setting the shader fails, the existing one is preserved and an
|
||||
exception is raised.
|
||||
'''
|
||||
shader = self.shader
|
||||
old_value = shader.fs
|
||||
shader.fs = value
|
||||
if not shader.success:
|
||||
shader.fs = old_value
|
||||
raise Exception('Setting new shader failed.')
|
||||
|
||||
|
||||
class EffectWidget(RelativeLayout):
|
||||
'''
|
||||
Widget with the ability to apply a series of graphical effects to
|
||||
its children. See the module documentation for more information on
|
||||
setting effects and creating your own.
|
||||
'''
|
||||
|
||||
background_color = ListProperty((0, 0, 0, 0))
|
||||
'''This defines the background color to be used for the fbo in the
|
||||
EffectWidget.
|
||||
|
||||
:attr:`background_color` is a :class:`ListProperty` defaults to
|
||||
(0, 0, 0, 0)
|
||||
'''
|
||||
|
||||
texture = ObjectProperty(None)
|
||||
'''The output texture of the final :class:`~kivy.graphics.Fbo` after
|
||||
all effects have been applied.
|
||||
|
||||
texture is an :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
effects = ListProperty([])
|
||||
'''List of all the effects to be applied. These should all be
|
||||
instances or subclasses of :class:`EffectBase`.
|
||||
|
||||
effects is a :class:`ListProperty` and defaults to [].
|
||||
'''
|
||||
|
||||
fbo_list = ListProperty([])
|
||||
'''(internal) List of all the fbos that are being used to apply
|
||||
the effects.
|
||||
|
||||
fbo_list is a :class:`ListProperty` and defaults to [].
|
||||
'''
|
||||
|
||||
_bound_effects = ListProperty([])
|
||||
'''(internal) List of effect classes that have been given an fbo to
|
||||
manage. This is necessary so that the fbo can be removed if the
|
||||
effect is no longer in use.
|
||||
|
||||
_bound_effects is a :class:`ListProperty` and defaults to [].
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Make sure opengl context exists
|
||||
EventLoop.ensure_window()
|
||||
|
||||
self.canvas = RenderContext(use_parent_projection=True,
|
||||
use_parent_modelview=True)
|
||||
|
||||
with self.canvas:
|
||||
self.fbo = Fbo(size=self.size)
|
||||
|
||||
with self.fbo.before:
|
||||
PushMatrix()
|
||||
with self.fbo:
|
||||
ClearColor(0, 0, 0, 0)
|
||||
ClearBuffers()
|
||||
self._background_color = Color(*self.background_color)
|
||||
self.fbo_rectangle = Rectangle(size=self.size)
|
||||
with self.fbo.after:
|
||||
PopMatrix()
|
||||
|
||||
super(EffectWidget, self).__init__(**kwargs)
|
||||
|
||||
Clock.schedule_interval(self._update_glsl, 0)
|
||||
|
||||
fbind = self.fbind
|
||||
fbo_setup = self.refresh_fbo_setup
|
||||
fbind('size', fbo_setup)
|
||||
fbind('effects', fbo_setup)
|
||||
fbind('background_color', self._refresh_background_color)
|
||||
|
||||
self.refresh_fbo_setup()
|
||||
self._refresh_background_color() # In case this was changed in kwargs
|
||||
|
||||
def _refresh_background_color(self, *args):
|
||||
self._background_color.rgba = self.background_color
|
||||
|
||||
def _update_glsl(self, *largs):
|
||||
'''(internal) Passes new time and resolution uniform
|
||||
variables to the shader.
|
||||
'''
|
||||
time = Clock.get_boottime()
|
||||
resolution = [float(size) for size in self.size]
|
||||
self.canvas['time'] = time
|
||||
self.canvas['resolution'] = resolution
|
||||
for fbo in self.fbo_list:
|
||||
fbo['time'] = time
|
||||
fbo['resolution'] = resolution
|
||||
|
||||
def refresh_fbo_setup(self, *args):
|
||||
'''(internal) Creates and assigns one :class:`~kivy.graphics.Fbo`
|
||||
per effect, and makes sure all sizes etc. are correct and
|
||||
consistent.
|
||||
'''
|
||||
# Add/remove fbos until there is one per effect
|
||||
while len(self.fbo_list) < len(self.effects):
|
||||
with self.canvas:
|
||||
new_fbo = EffectFbo(size=self.size)
|
||||
with new_fbo:
|
||||
ClearColor(0, 0, 0, 0)
|
||||
ClearBuffers()
|
||||
Color(1, 1, 1, 1)
|
||||
new_fbo.texture_rectangle = Rectangle(size=self.size)
|
||||
|
||||
new_fbo.texture_rectangle.size = self.size
|
||||
self.fbo_list.append(new_fbo)
|
||||
while len(self.fbo_list) > len(self.effects):
|
||||
old_fbo = self.fbo_list.pop()
|
||||
self.canvas.remove(old_fbo)
|
||||
|
||||
# Remove fbos from unused effects
|
||||
for effect in self._bound_effects:
|
||||
if effect not in self.effects:
|
||||
effect.fbo = None
|
||||
self._bound_effects = self.effects
|
||||
|
||||
# Do resizing etc.
|
||||
self.fbo.size = self.size
|
||||
self.fbo_rectangle.size = self.size
|
||||
for i in range(len(self.fbo_list)):
|
||||
self.fbo_list[i].size = self.size
|
||||
self.fbo_list[i].texture_rectangle.size = self.size
|
||||
|
||||
# If there are no effects, just draw our main fbo
|
||||
if len(self.fbo_list) == 0:
|
||||
self.texture = self.fbo.texture
|
||||
return
|
||||
|
||||
for i in range(1, len(self.fbo_list)):
|
||||
fbo = self.fbo_list[i]
|
||||
fbo.texture_rectangle.texture = self.fbo_list[i - 1].texture
|
||||
|
||||
# Build effect shaders
|
||||
for effect, fbo in zip(self.effects, self.fbo_list):
|
||||
effect.fbo = fbo
|
||||
|
||||
self.fbo_list[0].texture_rectangle.texture = self.fbo.texture
|
||||
self.texture = self.fbo_list[-1].texture
|
||||
|
||||
for fbo in self.fbo_list:
|
||||
fbo.draw()
|
||||
self.fbo.draw()
|
||||
|
||||
def add_widget(self, *args, **kwargs):
|
||||
# Add the widget to our Fbo instead of the normal canvas
|
||||
c = self.canvas
|
||||
self.canvas = self.fbo
|
||||
super(EffectWidget, self).add_widget(*args, **kwargs)
|
||||
self.canvas = c
|
||||
|
||||
def remove_widget(self, *args, **kwargs):
|
||||
# Remove the widget from our Fbo instead of the normal canvas
|
||||
c = self.canvas
|
||||
self.canvas = self.fbo
|
||||
super(EffectWidget, self).remove_widget(*args, **kwargs)
|
||||
self.canvas = c
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
# Clear widgets from our Fbo instead of the normal canvas
|
||||
c = self.canvas
|
||||
self.canvas = self.fbo
|
||||
super(EffectWidget, self).clear_widgets(*args, **kwargs)
|
||||
self.canvas = c
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,148 @@
|
||||
'''
|
||||
Float Layout
|
||||
============
|
||||
|
||||
:class:`FloatLayout` honors the :attr:`~kivy.uix.widget.Widget.pos_hint`
|
||||
and the :attr:`~kivy.uix.widget.Widget.size_hint` properties of its children.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: images/floatlayout.gif
|
||||
:align: right
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: images/floatlayout.png
|
||||
:align: right
|
||||
|
||||
For example, a FloatLayout with a size of (300, 300) is created::
|
||||
|
||||
layout = FloatLayout(size=(300, 300))
|
||||
|
||||
By default, all widgets have their size_hint=(1, 1), so this button will adopt
|
||||
the same size as the layout::
|
||||
|
||||
button = Button(text='Hello world')
|
||||
layout.add_widget(button)
|
||||
|
||||
To create a button 50% of the width and 25% of the height of the layout and
|
||||
positioned at (20, 20), you can do::
|
||||
|
||||
button = Button(
|
||||
text='Hello world',
|
||||
size_hint=(.5, .25),
|
||||
pos=(20, 20))
|
||||
|
||||
If you want to create a button that will always be the size of layout minus
|
||||
20% on each side::
|
||||
|
||||
button = Button(text='Hello world', size_hint=(.6, .6),
|
||||
pos_hint={'x':.2, 'y':.2})
|
||||
|
||||
.. note::
|
||||
|
||||
This layout can be used for an application. Most of the time, you will
|
||||
use the size of Window.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you are not using pos_hint, you must handle the positioning of the
|
||||
children: if the float layout is moving, you must handle moving the
|
||||
children too.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('FloatLayout', )
|
||||
|
||||
from kivy.uix.layout import Layout
|
||||
|
||||
|
||||
class FloatLayout(Layout):
|
||||
'''Float layout class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FloatLayout, self).__init__(**kwargs)
|
||||
fbind = self.fbind
|
||||
update = self._trigger_layout
|
||||
fbind('children', update)
|
||||
fbind('pos', update)
|
||||
fbind('pos_hint', update)
|
||||
fbind('size_hint', update)
|
||||
fbind('size', update)
|
||||
|
||||
def do_layout(self, *largs, **kwargs):
|
||||
# optimize layout by preventing looking at the same attribute in a loop
|
||||
w, h = kwargs.get('size', self.size)
|
||||
x, y = kwargs.get('pos', self.pos)
|
||||
for c in self.children:
|
||||
# size
|
||||
shw, shh = c.size_hint
|
||||
shw_min, shh_min = c.size_hint_min
|
||||
shw_max, shh_max = c.size_hint_max
|
||||
|
||||
if shw is not None and shh is not None:
|
||||
c_w = shw * w
|
||||
c_h = shh * h
|
||||
|
||||
if shw_min is not None and c_w < shw_min:
|
||||
c_w = shw_min
|
||||
elif shw_max is not None and c_w > shw_max:
|
||||
c_w = shw_max
|
||||
|
||||
if shh_min is not None and c_h < shh_min:
|
||||
c_h = shh_min
|
||||
elif shh_max is not None and c_h > shh_max:
|
||||
c_h = shh_max
|
||||
c.size = c_w, c_h
|
||||
elif shw is not None:
|
||||
c_w = shw * w
|
||||
|
||||
if shw_min is not None and c_w < shw_min:
|
||||
c_w = shw_min
|
||||
elif shw_max is not None and c_w > shw_max:
|
||||
c_w = shw_max
|
||||
c.width = c_w
|
||||
elif shh is not None:
|
||||
c_h = shh * h
|
||||
|
||||
if shh_min is not None and c_h < shh_min:
|
||||
c_h = shh_min
|
||||
elif shh_max is not None and c_h > shh_max:
|
||||
c_h = shh_max
|
||||
c.height = c_h
|
||||
|
||||
# pos
|
||||
for key, value in c.pos_hint.items():
|
||||
if key == 'x':
|
||||
c.x = x + value * w
|
||||
elif key == 'right':
|
||||
c.right = x + value * w
|
||||
elif key == 'pos':
|
||||
c.pos = x + value[0] * w, y + value[1] * h
|
||||
elif key == 'y':
|
||||
c.y = y + value * h
|
||||
elif key == 'top':
|
||||
c.top = y + value * h
|
||||
elif key == 'center':
|
||||
c.center = x + value[0] * w, y + value[1] * h
|
||||
elif key == 'center_x':
|
||||
c.center_x = x + value * w
|
||||
elif key == 'center_y':
|
||||
c.center_y = y + value * h
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
widget.bind(
|
||||
# size=self._trigger_layout,
|
||||
# size_hint=self._trigger_layout,
|
||||
pos=self._trigger_layout,
|
||||
pos_hint=self._trigger_layout)
|
||||
return super(FloatLayout, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
widget.unbind(
|
||||
# size=self._trigger_layout,
|
||||
# size_hint=self._trigger_layout,
|
||||
pos=self._trigger_layout,
|
||||
pos_hint=self._trigger_layout)
|
||||
return super(FloatLayout, self).remove_widget(widget, *args, **kwargs)
|
||||
@@ -0,0 +1,625 @@
|
||||
'''
|
||||
Gesture Surface
|
||||
===============
|
||||
|
||||
.. versionadded::
|
||||
1.9.0
|
||||
|
||||
.. warning::
|
||||
|
||||
This is experimental and subject to change as long as this warning notice
|
||||
is present.
|
||||
|
||||
See :file:`kivy/examples/demo/multistroke/main.py` for a complete application
|
||||
example.
|
||||
'''
|
||||
__all__ = ('GestureSurface', 'GestureContainer')
|
||||
|
||||
from random import random
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.clock import Clock
|
||||
from kivy.vector import Vector
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.graphics import Color, Line, Rectangle
|
||||
from kivy.properties import (NumericProperty, BooleanProperty,
|
||||
DictProperty, ColorProperty)
|
||||
from colorsys import hsv_to_rgb
|
||||
|
||||
# Clock undershoot margin, FIXME: this is probably too high?
|
||||
UNDERSHOOT_MARGIN = 0.1
|
||||
|
||||
|
||||
class GestureContainer(EventDispatcher):
|
||||
'''Container object that stores information about a gesture. It has
|
||||
various properties that are updated by `GestureSurface` as drawing
|
||||
progresses.
|
||||
|
||||
:Arguments:
|
||||
`touch`
|
||||
Touch object (as received by on_touch_down) used to initialize
|
||||
the gesture container. Required.
|
||||
|
||||
:Properties:
|
||||
`active`
|
||||
Set to False once the gesture is complete (meets
|
||||
`max_stroke` setting or `GestureSurface.temporal_window`)
|
||||
|
||||
:attr:`active` is a
|
||||
:class:`~kivy.properties.BooleanProperty`
|
||||
|
||||
`active_strokes`
|
||||
Number of strokes currently active in the gesture, ie
|
||||
concurrent touches associated with this gesture.
|
||||
|
||||
:attr:`active_strokes` is a
|
||||
:class:`~kivy.properties.NumericProperty`
|
||||
|
||||
`max_strokes`
|
||||
Max number of strokes allowed in the gesture. This
|
||||
is set by `GestureSurface.max_strokes` but can
|
||||
be overridden for example from `on_gesture_start`.
|
||||
|
||||
:attr:`max_strokes` is a
|
||||
:class:`~kivy.properties.NumericProperty`
|
||||
|
||||
`was_merged`
|
||||
Indicates that this gesture has been merged with another
|
||||
gesture and should be considered discarded.
|
||||
|
||||
:attr:`was_merged` is a
|
||||
:class:`~kivy.properties.BooleanProperty`
|
||||
|
||||
`bbox`
|
||||
Dictionary with keys minx, miny, maxx, maxy. Represents the size
|
||||
of the gesture bounding box.
|
||||
|
||||
:attr:`bbox` is a
|
||||
:class:`~kivy.properties.DictProperty`
|
||||
|
||||
`width`
|
||||
Represents the width of the gesture.
|
||||
|
||||
:attr:`width` is a
|
||||
:class:`~kivy.properties.NumericProperty`
|
||||
|
||||
`height`
|
||||
Represents the height of the gesture.
|
||||
|
||||
:attr:`height` is a
|
||||
:class:`~kivy.properties.NumericProperty`
|
||||
'''
|
||||
active = BooleanProperty(True)
|
||||
active_strokes = NumericProperty(0)
|
||||
max_strokes = NumericProperty(0)
|
||||
was_merged = BooleanProperty(False)
|
||||
bbox = DictProperty({'minx': float('inf'), 'miny': float('inf'),
|
||||
'maxx': float('-inf'), 'maxy': float('-inf')})
|
||||
width = NumericProperty(0)
|
||||
height = NumericProperty(0)
|
||||
|
||||
def __init__(self, touch, **kwargs):
|
||||
# The color is applied to all canvas items of this gesture
|
||||
self.color = kwargs.pop('color', [1., 1., 1.])
|
||||
|
||||
super(GestureContainer, self).__init__(**kwargs)
|
||||
|
||||
# This is the touch.uid of the oldest touch represented
|
||||
self.id = str(touch.uid)
|
||||
|
||||
# Store various timestamps for decision making
|
||||
self._create_time = Clock.get_time()
|
||||
self._update_time = None
|
||||
self._cleanup_time = None
|
||||
self._cache_time = 0
|
||||
|
||||
# We can cache the candidate here to save zip()/Vector instantiation
|
||||
self._vectors = None
|
||||
|
||||
# Key is touch.uid; value is a kivy.graphics.Line(); it's used even
|
||||
# if line_width is 0 (i.e. not actually drawn anywhere)
|
||||
self._strokes = {}
|
||||
|
||||
# Make sure the bbox is up to date with the first touch position
|
||||
self.update_bbox(touch)
|
||||
|
||||
def get_vectors(self, **kwargs):
|
||||
'''Return strokes in a format that is acceptable for
|
||||
`kivy.multistroke.Recognizer` as a gesture candidate or template. The
|
||||
result is cached automatically; the cache is invalidated at the start
|
||||
and end of a stroke and if `update_bbox` is called. If you are going
|
||||
to analyze a gesture mid-stroke, you may need to set the `no_cache`
|
||||
argument to True.'''
|
||||
if self._cache_time == self._update_time and \
|
||||
not kwargs.get('no_cache'):
|
||||
return self._vectors
|
||||
|
||||
vecs = []
|
||||
append = vecs.append
|
||||
for tuid, l in self._strokes.items():
|
||||
lpts = l.points
|
||||
append([Vector(*pts) for pts in zip(lpts[::2], lpts[1::2])])
|
||||
|
||||
self._vectors = vecs
|
||||
self._cache_time = self._update_time
|
||||
return vecs
|
||||
|
||||
def handles(self, touch):
|
||||
'''Returns True if this container handles the given touch'''
|
||||
if not self.active:
|
||||
return False
|
||||
return str(touch.uid) in self._strokes
|
||||
|
||||
def accept_stroke(self, count=1):
|
||||
'''Returns True if this container can accept `count` new strokes'''
|
||||
if not self.max_strokes:
|
||||
return True
|
||||
return len(self._strokes) + count <= self.max_strokes
|
||||
|
||||
def update_bbox(self, touch):
|
||||
'''Update gesture bbox from a touch coordinate'''
|
||||
x, y = touch.x, touch.y
|
||||
bb = self.bbox
|
||||
if x < bb['minx']:
|
||||
bb['minx'] = x
|
||||
if y < bb['miny']:
|
||||
bb['miny'] = y
|
||||
if x > bb['maxx']:
|
||||
bb['maxx'] = x
|
||||
if y > bb['maxy']:
|
||||
bb['maxy'] = y
|
||||
self.width = bb['maxx'] - bb['minx']
|
||||
self.height = bb['maxy'] - bb['miny']
|
||||
self._update_time = Clock.get_time()
|
||||
|
||||
def add_stroke(self, touch, line):
|
||||
'''Associate a list of points with a touch.uid; the line itself is
|
||||
created by the caller, but subsequent move/up events look it
|
||||
up via us. This is done to avoid problems during merge.'''
|
||||
self._update_time = Clock.get_time()
|
||||
self._strokes[str(touch.uid)] = line
|
||||
self.active_strokes += 1
|
||||
|
||||
def complete_stroke(self):
|
||||
'''Called on touch up events to keep track of how many strokes
|
||||
are active in the gesture (we only want to dispatch event when
|
||||
the *last* stroke in the gesture is released)'''
|
||||
self._update_time = Clock.get_time()
|
||||
self.active_strokes -= 1
|
||||
|
||||
def single_points_test(self):
|
||||
'''Returns True if the gesture consists only of single-point strokes,
|
||||
we must discard it in this case, or an exception will be raised'''
|
||||
for tuid, l in self._strokes.items():
|
||||
if len(l.points) > 2:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class GestureSurface(FloatLayout):
|
||||
'''Simple gesture surface to track/draw touch movements. Typically used
|
||||
to gather user input suitable for :class:`kivy.multistroke.Recognizer`.
|
||||
|
||||
:Properties:
|
||||
`temporal_window`
|
||||
Time to wait from the last touch_up event before attempting
|
||||
to recognize the gesture. If you set this to 0, the
|
||||
`on_gesture_complete` event is not fired unless the
|
||||
:attr:`max_strokes` condition is met.
|
||||
|
||||
:attr:`temporal_window` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 2.0
|
||||
|
||||
`max_strokes`
|
||||
Max number of strokes in a single gesture; if this is reached,
|
||||
recognition will start immediately on the final touch_up event.
|
||||
If this is set to 0, the `on_gesture_complete` event is not
|
||||
fired unless the :attr:`temporal_window` expires.
|
||||
|
||||
:attr:`max_strokes` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 2.0
|
||||
|
||||
`bbox_margin`
|
||||
Bounding box margin for detecting gesture collisions, in
|
||||
pixels.
|
||||
|
||||
:attr:`bbox_margin` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 30
|
||||
|
||||
`draw_timeout`
|
||||
Number of seconds to keep lines/bbox on canvas after the
|
||||
`on_gesture_complete` event is fired. If this is set to 0,
|
||||
gestures are immediately removed from the surface when
|
||||
complete.
|
||||
|
||||
:attr:`draw_timeout` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 3.0
|
||||
|
||||
`color`
|
||||
Color used to draw the gesture, in RGB. This option does not
|
||||
have an effect if :attr:`use_random_color` is True.
|
||||
|
||||
:attr:`color` is a
|
||||
:class:`~kivy.properties.ColorProperty` and defaults to
|
||||
[1, 1, 1, 1] (white)
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
|
||||
`use_random_color`
|
||||
Set to True to pick a random color for each gesture, if you do
|
||||
this then `color` is ignored. Defaults to False.
|
||||
|
||||
:attr:`use_random_color` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to False
|
||||
|
||||
`line_width`
|
||||
Line width used for tracing touches on the surface. Set to 0
|
||||
if you only want to detect gestures without drawing anything.
|
||||
If you use 1.0, OpenGL GL_LINE is used for drawing; values > 1
|
||||
will use an internal drawing method based on triangles (less
|
||||
efficient), see :mod:`kivy.graphics`.
|
||||
|
||||
:attr:`line_width` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 2
|
||||
|
||||
`draw_bbox`
|
||||
Set to True if you want to draw bounding box behind gestures.
|
||||
This only works if `line_width` >= 1. Default is False.
|
||||
|
||||
:attr:`draw_bbox` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to True
|
||||
|
||||
`bbox_alpha`
|
||||
Opacity for bounding box if `draw_bbox` is True. Default 0.1
|
||||
|
||||
:attr:`bbox_alpha` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 0.1
|
||||
|
||||
:Events:
|
||||
`on_gesture_start` :class:`GestureContainer`
|
||||
Fired when a new gesture is initiated on the surface, i.e. the
|
||||
first on_touch_down that does not collide with an existing
|
||||
gesture on the surface.
|
||||
|
||||
`on_gesture_extend` :class:`GestureContainer`
|
||||
Fired when a touch_down event occurs within an existing gesture.
|
||||
|
||||
`on_gesture_merge` :class:`GestureContainer`, :class:`GestureContainer`
|
||||
Fired when two gestures collide and get merged to one gesture.
|
||||
The first argument is the gesture that has been merged (no longer
|
||||
valid); the second is the combined (resulting) gesture.
|
||||
|
||||
`on_gesture_complete` :class:`GestureContainer`
|
||||
Fired when a set of strokes is considered a complete gesture,
|
||||
this happens when `temporal_window` expires or `max_strokes`
|
||||
is reached. Typically you will bind to this event and use
|
||||
the provided `GestureContainer` get_vectors() method to
|
||||
match against your gesture database.
|
||||
|
||||
`on_gesture_cleanup` :class:`GestureContainer`
|
||||
Fired `draw_timeout` seconds after `on_gesture_complete`,
|
||||
The gesture will be removed from the canvas (if line_width > 0 or
|
||||
draw_bbox is True) and the internal gesture list before this.
|
||||
|
||||
`on_gesture_discard` :class:`GestureContainer`
|
||||
Fired when a gesture does not meet the minimum size requirements
|
||||
for recognition (width/height < 5, or consists only of single-
|
||||
point strokes).
|
||||
'''
|
||||
|
||||
temporal_window = NumericProperty(2.0)
|
||||
draw_timeout = NumericProperty(3.0)
|
||||
max_strokes = NumericProperty(4)
|
||||
bbox_margin = NumericProperty(30)
|
||||
|
||||
line_width = NumericProperty(2)
|
||||
color = ColorProperty([1., 1., 1., 1.])
|
||||
use_random_color = BooleanProperty(False)
|
||||
draw_bbox = BooleanProperty(False)
|
||||
bbox_alpha = NumericProperty(0.1)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(GestureSurface, self).__init__(**kwargs)
|
||||
# A list of GestureContainer objects (all gestures on the surface)
|
||||
self._gestures = []
|
||||
self.register_event_type('on_gesture_start')
|
||||
self.register_event_type('on_gesture_extend')
|
||||
self.register_event_type('on_gesture_merge')
|
||||
self.register_event_type('on_gesture_complete')
|
||||
self.register_event_type('on_gesture_cleanup')
|
||||
self.register_event_type('on_gesture_discard')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Touch Events
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_touch_down(self, touch):
|
||||
'''When a new touch is registered, the first thing we do is to test if
|
||||
it collides with the bounding box of another known gesture. If so, it
|
||||
is assumed to be part of that gesture.
|
||||
'''
|
||||
# If the touch originates outside the surface, ignore it.
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return
|
||||
|
||||
touch.grab(self)
|
||||
|
||||
# Add the stroke to existing gesture, or make a new one
|
||||
g = self.find_colliding_gesture(touch)
|
||||
new = False
|
||||
if g is None:
|
||||
g = self.init_gesture(touch)
|
||||
new = True
|
||||
|
||||
# We now belong to a gesture (new or old); start a new stroke.
|
||||
self.init_stroke(g, touch)
|
||||
|
||||
if new:
|
||||
self.dispatch('on_gesture_start', g, touch)
|
||||
else:
|
||||
self.dispatch('on_gesture_extend', g, touch)
|
||||
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
'''When a touch moves, we add a point to the line on the canvas so the
|
||||
path is updated. We must also check if the new point collides with the
|
||||
bounding box of another gesture - if so, they should be merged.'''
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return
|
||||
|
||||
# Retrieve the GestureContainer object that handles this touch, and
|
||||
# test for colliding gestures. If found, merge them to one.
|
||||
g = self.get_gesture(touch)
|
||||
collision = self.find_colliding_gesture(touch)
|
||||
if collision is not None and g.accept_stroke(len(collision._strokes)):
|
||||
merge = self.merge_gestures(g, collision)
|
||||
if g.was_merged:
|
||||
self.dispatch('on_gesture_merge', g, collision)
|
||||
else:
|
||||
self.dispatch('on_gesture_merge', collision, g)
|
||||
g = merge
|
||||
else:
|
||||
g.update_bbox(touch)
|
||||
|
||||
# Add the new point to gesture stroke list and update the canvas line
|
||||
g._strokes[str(touch.uid)].points += (touch.x, touch.y)
|
||||
|
||||
# Draw the gesture bounding box; if it is a single press that
|
||||
# does not trigger a move event, we would miss it otherwise.
|
||||
if self.draw_bbox:
|
||||
self._update_canvas_bbox(g)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
|
||||
g = self.get_gesture(touch)
|
||||
g.complete_stroke()
|
||||
|
||||
# If this stroke hit the maximum limit, dispatch immediately
|
||||
if not g.accept_stroke():
|
||||
self._complete_dispatcher(0)
|
||||
|
||||
# dispatch later only if we have a window
|
||||
elif self.temporal_window > 0:
|
||||
Clock.schedule_once(self._complete_dispatcher,
|
||||
self.temporal_window)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gesture related methods
|
||||
# -----------------------------------------------------------------------------
|
||||
def init_gesture(self, touch):
|
||||
'''Create a new gesture from touch, i.e. it's the first on
|
||||
surface, or was not close enough to any existing gesture (yet)'''
|
||||
col = self.color
|
||||
if self.use_random_color:
|
||||
col = hsv_to_rgb(random(), 1., 1.)
|
||||
|
||||
g = GestureContainer(touch, max_strokes=self.max_strokes, color=col)
|
||||
|
||||
# Create the bounding box Rectangle for the gesture
|
||||
if self.draw_bbox:
|
||||
bb = g.bbox
|
||||
with self.canvas:
|
||||
Color(col[0], col[1], col[2], self.bbox_alpha, mode='rgba',
|
||||
group=g.id)
|
||||
|
||||
g._bbrect = Rectangle(
|
||||
group=g.id,
|
||||
pos=(bb['minx'], bb['miny']),
|
||||
size=(bb['maxx'] - bb['minx'],
|
||||
bb['maxy'] - bb['miny']))
|
||||
|
||||
self._gestures.append(g)
|
||||
return g
|
||||
|
||||
def init_stroke(self, g, touch):
|
||||
points = [touch.x, touch.y]
|
||||
col = g.color
|
||||
|
||||
new_line = Line(
|
||||
points=points,
|
||||
width=self.line_width,
|
||||
group=g.id)
|
||||
g._strokes[str(touch.uid)] = new_line
|
||||
|
||||
if self.line_width:
|
||||
canvas_add = self.canvas.add
|
||||
canvas_add(Color(col[0], col[1], col[2], mode='rgb', group=g.id))
|
||||
canvas_add(new_line)
|
||||
|
||||
# Update the bbox in case; this will normally be done in on_touch_move,
|
||||
# but we want to update it also for a single press, force that here:
|
||||
g.update_bbox(touch)
|
||||
if self.draw_bbox:
|
||||
self._update_canvas_bbox(g)
|
||||
|
||||
# Register the stroke in GestureContainer so we can look it up later
|
||||
g.add_stroke(touch, new_line)
|
||||
|
||||
def get_gesture(self, touch):
|
||||
'''Returns GestureContainer associated with given touch'''
|
||||
for g in self._gestures:
|
||||
if g.active and g.handles(touch):
|
||||
return g
|
||||
raise Exception('get_gesture() failed to identify ' + str(touch.uid))
|
||||
|
||||
def find_colliding_gesture(self, touch):
|
||||
'''Checks if a touch x/y collides with the bounding box of an existing
|
||||
gesture. If so, return it (otherwise returns None)
|
||||
'''
|
||||
touch_x, touch_y = touch.pos
|
||||
for g in self._gestures:
|
||||
if g.active and not g.handles(touch) and g.accept_stroke():
|
||||
bb = g.bbox
|
||||
margin = self.bbox_margin
|
||||
minx = bb['minx'] - margin
|
||||
miny = bb['miny'] - margin
|
||||
maxx = bb['maxx'] + margin
|
||||
maxy = bb['maxy'] + margin
|
||||
if minx <= touch_x <= maxx and miny <= touch_y <= maxy:
|
||||
return g
|
||||
return None
|
||||
|
||||
def merge_gestures(self, g, other):
|
||||
'''Merges two gestures together, the oldest one is retained and the
|
||||
newer one gets the `GestureContainer.was_merged` flag raised.'''
|
||||
# Swap order depending on gesture age (the merged gesture gets
|
||||
# the color from the oldest one of the two).
|
||||
swap = other._create_time < g._create_time
|
||||
a = swap and other or g
|
||||
b = swap and g or other
|
||||
|
||||
# Apply the outer limits of bbox to the merged gesture
|
||||
abbox = a.bbox
|
||||
bbbox = b.bbox
|
||||
if bbbox['minx'] < abbox['minx']:
|
||||
abbox['minx'] = bbbox['minx']
|
||||
if bbbox['miny'] < abbox['miny']:
|
||||
abbox['miny'] = bbbox['miny']
|
||||
if bbbox['maxx'] > abbox['maxx']:
|
||||
abbox['maxx'] = bbbox['maxx']
|
||||
if bbbox['maxy'] > abbox['maxy']:
|
||||
abbox['maxy'] = bbbox['maxy']
|
||||
|
||||
# Now transfer the coordinates from old to new gesture;
|
||||
# FIXME: This can probably be copied more efficiently?
|
||||
astrokes = a._strokes
|
||||
lw = self.line_width
|
||||
a_id = a.id
|
||||
col = a.color
|
||||
|
||||
self.canvas.remove_group(b.id)
|
||||
canv_add = self.canvas.add
|
||||
for uid, old in b._strokes.items():
|
||||
# FIXME: Can't figure out how to change group= for existing Line()
|
||||
new_line = Line(
|
||||
points=old.points,
|
||||
width=old.width,
|
||||
group=a_id)
|
||||
astrokes[uid] = new_line
|
||||
if lw:
|
||||
canv_add(Color(col[0], col[1], col[2], mode='rgb', group=a_id))
|
||||
canv_add(new_line)
|
||||
|
||||
b.active = False
|
||||
b.was_merged = True
|
||||
a.active_strokes += b.active_strokes
|
||||
a._update_time = Clock.get_time()
|
||||
return a
|
||||
|
||||
def _update_canvas_bbox(self, g):
|
||||
# If draw_bbox is changed while two gestures are active,
|
||||
# we might not have a bbrect member
|
||||
if not hasattr(g, '_bbrect'):
|
||||
return
|
||||
|
||||
bb = g.bbox
|
||||
g._bbrect.pos = (bb['minx'], bb['miny'])
|
||||
g._bbrect.size = (bb['maxx'] - bb['minx'],
|
||||
bb['maxy'] - bb['miny'])
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Timeout callbacks
|
||||
# -----------------------------------------------------------------------------
|
||||
def _complete_dispatcher(self, dt):
|
||||
'''This method is scheduled on all touch up events. It will dispatch
|
||||
the `on_gesture_complete` event for all completed gestures, and remove
|
||||
merged gestures from the internal gesture list.'''
|
||||
need_cleanup = False
|
||||
gest = self._gestures
|
||||
timeout = self.draw_timeout
|
||||
twin = self.temporal_window
|
||||
get_time = Clock.get_time
|
||||
|
||||
for idx, g in enumerate(gest):
|
||||
# Gesture is part of another gesture, just delete it
|
||||
if g.was_merged:
|
||||
del gest[idx]
|
||||
continue
|
||||
|
||||
# Not active == already handled, or has active strokes (it cannot
|
||||
# possibly be complete). Proceed to next gesture on surface.
|
||||
if not g.active or g.active_strokes != 0:
|
||||
continue
|
||||
|
||||
t1 = g._update_time + twin
|
||||
t2 = get_time() + UNDERSHOOT_MARGIN
|
||||
|
||||
# max_strokes reached, or temporal window has expired. The gesture
|
||||
# is complete; need to dispatch _complete or _discard event.
|
||||
if not g.accept_stroke() or t1 <= t2:
|
||||
discard = False
|
||||
if g.width < 5 and g.height < 5:
|
||||
discard = True
|
||||
elif g.single_points_test():
|
||||
discard = True
|
||||
|
||||
need_cleanup = True
|
||||
g.active = False
|
||||
g._cleanup_time = get_time() + timeout
|
||||
|
||||
if discard:
|
||||
self.dispatch('on_gesture_discard', g)
|
||||
else:
|
||||
self.dispatch('on_gesture_complete', g)
|
||||
|
||||
if need_cleanup:
|
||||
Clock.schedule_once(self._cleanup, timeout)
|
||||
|
||||
def _cleanup(self, dt):
|
||||
'''This method is scheduled from _complete_dispatcher to clean up the
|
||||
canvas and internal gesture list after a gesture is completed.'''
|
||||
m = UNDERSHOOT_MARGIN
|
||||
rg = self.canvas.remove_group
|
||||
gestures = self._gestures
|
||||
for idx, g in enumerate(gestures):
|
||||
if g._cleanup_time is None:
|
||||
continue
|
||||
if g._cleanup_time <= Clock.get_time() + m:
|
||||
rg(g.id)
|
||||
del gestures[idx]
|
||||
self.dispatch('on_gesture_cleanup', g)
|
||||
|
||||
def on_gesture_start(self, *l):
|
||||
pass
|
||||
|
||||
def on_gesture_extend(self, *l):
|
||||
pass
|
||||
|
||||
def on_gesture_merge(self, *l):
|
||||
pass
|
||||
|
||||
def on_gesture_complete(self, *l):
|
||||
pass
|
||||
|
||||
def on_gesture_discard(self, *l):
|
||||
pass
|
||||
|
||||
def on_gesture_cleanup(self, *l):
|
||||
pass
|
||||
@@ -0,0 +1,629 @@
|
||||
'''
|
||||
Grid Layout
|
||||
===========
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: images/gridlayout.gif
|
||||
:align: right
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: images/gridlayout.png
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
The :class:`GridLayout` arranges children in a matrix. It takes the available
|
||||
space and divides it into columns and rows, then adds widgets to the resulting
|
||||
"cells".
|
||||
|
||||
.. versionchanged:: 1.0.7
|
||||
The implementation has changed to use the widget size_hint for calculating
|
||||
column/row sizes. `uniform_width` and `uniform_height` have been removed
|
||||
and other properties have added to give you more control.
|
||||
|
||||
Background
|
||||
----------
|
||||
|
||||
Unlike many other toolkits, you cannot explicitly place a widget in a specific
|
||||
column/row. Each child is automatically assigned a position determined by the
|
||||
layout configuration and the child's index in the children list.
|
||||
|
||||
A GridLayout must always have at least one input constraint:
|
||||
:attr:`GridLayout.cols` or :attr:`GridLayout.rows`. If you do not specify cols
|
||||
or rows, the Layout will throw an exception.
|
||||
|
||||
Column Width and Row Height
|
||||
---------------------------
|
||||
|
||||
The column width/row height are determined in 3 steps:
|
||||
|
||||
- The initial size is given by the :attr:`col_default_width` and
|
||||
:attr:`row_default_height` properties. To customize the size of a single
|
||||
column or row, use :attr:`cols_minimum` or :attr:`rows_minimum`.
|
||||
- The `size_hint_x`/`size_hint_y` of the children are taken into account.
|
||||
If no widgets have a size hint, the maximum size is used for all
|
||||
children.
|
||||
- You can force the default size by setting the :attr:`col_force_default`
|
||||
or :attr:`row_force_default` property. This will force the layout to
|
||||
ignore the `width` and `size_hint` properties of children and use the
|
||||
default size.
|
||||
|
||||
Using a GridLayout
|
||||
------------------
|
||||
|
||||
In the example below, all widgets will have an equal size. By default, the
|
||||
`size_hint` is (1, 1), so a Widget will take the full size of the parent::
|
||||
|
||||
layout = GridLayout(cols=2)
|
||||
layout.add_widget(Button(text='Hello 1'))
|
||||
layout.add_widget(Button(text='World 1'))
|
||||
layout.add_widget(Button(text='Hello 2'))
|
||||
layout.add_widget(Button(text='World 2'))
|
||||
|
||||
.. image:: images/gridlayout_1.jpg
|
||||
|
||||
Now, let's fix the size of Hello buttons to 100px instead of using
|
||||
size_hint_x=1::
|
||||
|
||||
layout = GridLayout(cols=2)
|
||||
layout.add_widget(Button(text='Hello 1', size_hint_x=None, width=100))
|
||||
layout.add_widget(Button(text='World 1'))
|
||||
layout.add_widget(Button(text='Hello 2', size_hint_x=None, width=100))
|
||||
layout.add_widget(Button(text='World 2'))
|
||||
|
||||
.. image:: images/gridlayout_2.jpg
|
||||
|
||||
Next, let's fix the row height to a specific size::
|
||||
|
||||
layout = GridLayout(cols=2, row_force_default=True, row_default_height=40)
|
||||
layout.add_widget(Button(text='Hello 1', size_hint_x=None, width=100))
|
||||
layout.add_widget(Button(text='World 1'))
|
||||
layout.add_widget(Button(text='Hello 2', size_hint_x=None, width=100))
|
||||
layout.add_widget(Button(text='World 2'))
|
||||
|
||||
.. image:: images/gridlayout_3.jpg
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('GridLayout', 'GridLayoutException')
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import NumericProperty, BooleanProperty, DictProperty, \
|
||||
BoundedNumericProperty, ReferenceListProperty, VariableListProperty, \
|
||||
ObjectProperty, StringProperty, OptionProperty
|
||||
from math import ceil
|
||||
from itertools import accumulate, product, chain, islice
|
||||
from operator import sub
|
||||
|
||||
|
||||
def nmax(*args):
|
||||
# merge into one list
|
||||
args = [x for x in args if x is not None]
|
||||
return max(args)
|
||||
|
||||
|
||||
def nmin(*args):
|
||||
# merge into one list
|
||||
args = [x for x in args if x is not None]
|
||||
return min(args)
|
||||
|
||||
|
||||
class GridLayoutException(Exception):
|
||||
'''Exception for errors if the grid layout manipulation fails.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class GridLayout(Layout):
|
||||
'''Grid layout class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
spacing = VariableListProperty([0, 0], length=2)
|
||||
'''Spacing between children: [spacing_horizontal, spacing_vertical].
|
||||
|
||||
spacing also accepts a one argument form [spacing].
|
||||
|
||||
:attr:`spacing` is a
|
||||
:class:`~kivy.properties.VariableListProperty` and defaults to [0, 0].
|
||||
'''
|
||||
|
||||
padding = VariableListProperty([0, 0, 0, 0])
|
||||
'''Padding between the layout box and its children: [padding_left,
|
||||
padding_top, padding_right, padding_bottom].
|
||||
|
||||
padding also accepts a two argument form [padding_horizontal,
|
||||
padding_vertical] and a one argument form [padding].
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
Replaced NumericProperty with VariableListProperty.
|
||||
|
||||
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
|
||||
defaults to [0, 0, 0, 0].
|
||||
'''
|
||||
|
||||
cols = BoundedNumericProperty(None, min=0, allownone=True)
|
||||
'''Number of columns in the grid.
|
||||
|
||||
.. versionchanged:: 1.0.8
|
||||
Changed from a NumericProperty to BoundedNumericProperty. You can no
|
||||
longer set this to a negative value.
|
||||
|
||||
:attr:`cols` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
None.
|
||||
'''
|
||||
|
||||
rows = BoundedNumericProperty(None, min=0, allownone=True)
|
||||
'''Number of rows in the grid.
|
||||
|
||||
.. versionchanged:: 1.0.8
|
||||
Changed from a NumericProperty to a BoundedNumericProperty. You can no
|
||||
longer set this to a negative value.
|
||||
|
||||
:attr:`rows` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
None.
|
||||
'''
|
||||
|
||||
col_default_width = NumericProperty(0)
|
||||
'''Default minimum size to use for a column.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`col_default_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
row_default_height = NumericProperty(0)
|
||||
'''Default minimum size to use for row.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`row_default_height` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
col_force_default = BooleanProperty(False)
|
||||
'''If True, ignore the width and size_hint_x of the child and use the
|
||||
default column width.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`col_force_default` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
row_force_default = BooleanProperty(False)
|
||||
'''If True, ignore the height and size_hint_y of the child and use the
|
||||
default row height.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`row_force_default` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
cols_minimum = DictProperty({})
|
||||
'''Dict of minimum width for each column. The dictionary keys are the
|
||||
column numbers, e.g. 0, 1, 2...
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`cols_minimum` is a :class:`~kivy.properties.DictProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
|
||||
rows_minimum = DictProperty({})
|
||||
'''Dict of minimum height for each row. The dictionary keys are the
|
||||
row numbers, e.g. 0, 1, 2...
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`rows_minimum` is a :class:`~kivy.properties.DictProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
|
||||
minimum_width = NumericProperty(0)
|
||||
'''Automatically computed minimum width needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0. It is read only.
|
||||
'''
|
||||
|
||||
minimum_height = NumericProperty(0)
|
||||
'''Automatically computed minimum height needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0. It is read only.
|
||||
'''
|
||||
|
||||
minimum_size = ReferenceListProperty(minimum_width, minimum_height)
|
||||
'''Automatically computed minimum size needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`minimum_size` is a
|
||||
:class:`~kivy.properties.ReferenceListProperty` of
|
||||
(:attr:`minimum_width`, :attr:`minimum_height`) properties. It is read
|
||||
only.
|
||||
'''
|
||||
|
||||
orientation = OptionProperty('lr-tb', options=(
|
||||
'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt', 'bt-lr', 'rl-bt',
|
||||
'bt-rl'))
|
||||
'''Orientation of the layout.
|
||||
|
||||
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'lr-tb'.
|
||||
|
||||
Valid orientations are 'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt',
|
||||
'bt-lr', 'rl-bt' and 'bt-rl'.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
.. note::
|
||||
|
||||
'lr' means Left to Right.
|
||||
'rl' means Right to Left.
|
||||
'tb' means Top to Bottom.
|
||||
'bt' means Bottom to Top.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._cols = self._rows = None
|
||||
super(GridLayout, self).__init__(**kwargs)
|
||||
fbind = self.fbind
|
||||
update = self._trigger_layout
|
||||
fbind('col_default_width', update)
|
||||
fbind('row_default_height', update)
|
||||
fbind('col_force_default', update)
|
||||
fbind('row_force_default', update)
|
||||
fbind('cols', update)
|
||||
fbind('rows', update)
|
||||
fbind('parent', update)
|
||||
fbind('spacing', update)
|
||||
fbind('padding', update)
|
||||
fbind('children', update)
|
||||
fbind('size', update)
|
||||
fbind('pos', update)
|
||||
fbind('orientation', update)
|
||||
|
||||
def get_max_widgets(self):
|
||||
if self.cols and self.rows:
|
||||
return self.rows * self.cols
|
||||
else:
|
||||
return None
|
||||
|
||||
def on_children(self, instance, value):
|
||||
# if that makes impossible to construct things with deferred method,
|
||||
# migrate this test in do_layout, and/or issue a warning.
|
||||
smax = self.get_max_widgets()
|
||||
if smax and len(value) > smax:
|
||||
raise GridLayoutException(
|
||||
'Too many children in GridLayout. Increase rows/cols!')
|
||||
|
||||
@property
|
||||
def _fills_row_first(self):
|
||||
return self.orientation[0] in 'lr'
|
||||
|
||||
@property
|
||||
def _fills_from_left_to_right(self):
|
||||
return 'lr' in self.orientation
|
||||
|
||||
@property
|
||||
def _fills_from_top_to_bottom(self):
|
||||
return 'tb' in self.orientation
|
||||
|
||||
def _init_rows_cols_sizes(self, count):
|
||||
# the goal here is to calculate the minimum size of every cols/rows
|
||||
# and determine if they have stretch or not
|
||||
current_cols = self.cols
|
||||
current_rows = self.rows
|
||||
|
||||
# if no cols or rows are set, we can't calculate minimum size.
|
||||
# the grid must be constrained at least on one side
|
||||
if not current_cols and not current_rows:
|
||||
Logger.warning('%r have no cols or rows set, '
|
||||
'layout is not triggered.' % self)
|
||||
return
|
||||
|
||||
if current_cols is None:
|
||||
current_cols = int(ceil(count / float(current_rows)))
|
||||
elif current_rows is None:
|
||||
current_rows = int(ceil(count / float(current_cols)))
|
||||
|
||||
current_cols = max(1, current_cols)
|
||||
current_rows = max(1, current_rows)
|
||||
|
||||
self._has_hint_bound_x = False
|
||||
self._has_hint_bound_y = False
|
||||
self._cols_min_size_none = 0. # min size from all the None hint
|
||||
self._rows_min_size_none = 0. # min size from all the None hint
|
||||
self._cols = cols = [self.col_default_width] * current_cols
|
||||
self._cols_sh = [None] * current_cols
|
||||
self._cols_sh_min = [None] * current_cols
|
||||
self._cols_sh_max = [None] * current_cols
|
||||
self._rows = rows = [self.row_default_height] * current_rows
|
||||
self._rows_sh = [None] * current_rows
|
||||
self._rows_sh_min = [None] * current_rows
|
||||
self._rows_sh_max = [None] * current_rows
|
||||
|
||||
# update minimum size from the dicts
|
||||
items = (i for i in self.cols_minimum.items() if i[0] < len(cols))
|
||||
for index, value in items:
|
||||
cols[index] = max(value, cols[index])
|
||||
|
||||
items = (i for i in self.rows_minimum.items() if i[0] < len(rows))
|
||||
for index, value in items:
|
||||
rows[index] = max(value, rows[index])
|
||||
return True
|
||||
|
||||
def _fill_rows_cols_sizes(self):
|
||||
cols, rows = self._cols, self._rows
|
||||
cols_sh, rows_sh = self._cols_sh, self._rows_sh
|
||||
cols_sh_min, rows_sh_min = self._cols_sh_min, self._rows_sh_min
|
||||
cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max
|
||||
|
||||
# calculate minimum size for each columns and rows
|
||||
has_bound_y = has_bound_x = False
|
||||
idx_iter = self._create_idx_iter(len(cols), len(rows))
|
||||
for child, (col, row) in zip(reversed(self.children), idx_iter):
|
||||
(shw, shh), (w, h) = child.size_hint, child.size
|
||||
shw_min, shh_min = child.size_hint_min
|
||||
shw_max, shh_max = child.size_hint_max
|
||||
|
||||
# compute minimum size / maximum stretch needed
|
||||
if shw is None:
|
||||
cols[col] = nmax(cols[col], w)
|
||||
else:
|
||||
cols_sh[col] = nmax(cols_sh[col], shw)
|
||||
if shw_min is not None:
|
||||
has_bound_x = True
|
||||
cols_sh_min[col] = nmax(cols_sh_min[col], shw_min)
|
||||
if shw_max is not None:
|
||||
has_bound_x = True
|
||||
cols_sh_max[col] = nmin(cols_sh_max[col], shw_max)
|
||||
|
||||
if shh is None:
|
||||
rows[row] = nmax(rows[row], h)
|
||||
else:
|
||||
rows_sh[row] = nmax(rows_sh[row], shh)
|
||||
if shh_min is not None:
|
||||
has_bound_y = True
|
||||
rows_sh_min[row] = nmax(rows_sh_min[row], shh_min)
|
||||
if shh_max is not None:
|
||||
has_bound_y = True
|
||||
rows_sh_max[row] = nmin(rows_sh_max[row], shh_max)
|
||||
self._has_hint_bound_x = has_bound_x
|
||||
self._has_hint_bound_y = has_bound_y
|
||||
|
||||
def _update_minimum_size(self):
|
||||
# calculate minimum width/height needed, starting from padding +
|
||||
# spacing
|
||||
l, t, r, b = self.padding
|
||||
spacing_x, spacing_y = self.spacing
|
||||
cols, rows = self._cols, self._rows
|
||||
|
||||
width = l + r + spacing_x * (len(cols) - 1)
|
||||
self._cols_min_size_none = sum(cols) + width
|
||||
# we need to subtract for the sh_max/min the already guaranteed size
|
||||
# due to having a None in the col. So sh_min gets smaller by that size
|
||||
# since it's already covered. Similarly for sh_max, because if we
|
||||
# already exceeded the max, the subtracted max will be zero, so
|
||||
# it won't get larger
|
||||
if self._has_hint_bound_x:
|
||||
cols_sh_min = self._cols_sh_min
|
||||
cols_sh_max = self._cols_sh_max
|
||||
|
||||
for i, (c, sh_min, sh_max) in enumerate(
|
||||
zip(cols, cols_sh_min, cols_sh_max)):
|
||||
if sh_min is not None:
|
||||
width += max(c, sh_min)
|
||||
cols_sh_min[i] = max(0., sh_min - c)
|
||||
else:
|
||||
width += c
|
||||
|
||||
if sh_max is not None:
|
||||
cols_sh_max[i] = max(0., sh_max - c)
|
||||
else:
|
||||
width = self._cols_min_size_none
|
||||
|
||||
height = t + b + spacing_y * (len(rows) - 1)
|
||||
self._rows_min_size_none = sum(rows) + height
|
||||
if self._has_hint_bound_y:
|
||||
rows_sh_min = self._rows_sh_min
|
||||
rows_sh_max = self._rows_sh_max
|
||||
|
||||
for i, (r, sh_min, sh_max) in enumerate(
|
||||
zip(rows, rows_sh_min, rows_sh_max)):
|
||||
if sh_min is not None:
|
||||
height += max(r, sh_min)
|
||||
rows_sh_min[i] = max(0., sh_min - r)
|
||||
else:
|
||||
height += r
|
||||
|
||||
if sh_max is not None:
|
||||
rows_sh_max[i] = max(0., sh_max - r)
|
||||
else:
|
||||
height = self._rows_min_size_none
|
||||
|
||||
# finally, set the minimum size
|
||||
self.minimum_size = (width, height)
|
||||
|
||||
def _finalize_rows_cols_sizes(self):
|
||||
selfw = self.width
|
||||
selfh = self.height
|
||||
|
||||
# resolve size for each column
|
||||
if self.col_force_default:
|
||||
cols = [self.col_default_width] * len(self._cols)
|
||||
for index, value in self.cols_minimum.items():
|
||||
cols[index] = value
|
||||
self._cols = cols
|
||||
else:
|
||||
cols = self._cols
|
||||
cols_sh = self._cols_sh
|
||||
cols_sh_min = self._cols_sh_min
|
||||
cols_weight = float(sum((x for x in cols_sh if x is not None)))
|
||||
stretch_w = max(0., selfw - self._cols_min_size_none)
|
||||
|
||||
if stretch_w > 1e-9:
|
||||
if self._has_hint_bound_x:
|
||||
# fix the hints to be within bounds
|
||||
self.layout_hint_with_bounds(
|
||||
cols_weight, stretch_w,
|
||||
sum((c for c in cols_sh_min if c is not None)),
|
||||
cols_sh_min, self._cols_sh_max, cols_sh)
|
||||
|
||||
for index, col_stretch in enumerate(cols_sh):
|
||||
# if the col don't have stretch information, nothing to do
|
||||
if not col_stretch:
|
||||
continue
|
||||
# add to the min width whatever remains from size_hint
|
||||
cols[index] += stretch_w * col_stretch / cols_weight
|
||||
|
||||
# same algo for rows
|
||||
if self.row_force_default:
|
||||
rows = [self.row_default_height] * len(self._rows)
|
||||
for index, value in self.rows_minimum.items():
|
||||
rows[index] = value
|
||||
self._rows = rows
|
||||
else:
|
||||
rows = self._rows
|
||||
rows_sh = self._rows_sh
|
||||
rows_sh_min = self._rows_sh_min
|
||||
rows_weight = float(sum((x for x in rows_sh if x is not None)))
|
||||
stretch_h = max(0., selfh - self._rows_min_size_none)
|
||||
|
||||
if stretch_h > 1e-9:
|
||||
if self._has_hint_bound_y:
|
||||
# fix the hints to be within bounds
|
||||
self.layout_hint_with_bounds(
|
||||
rows_weight, stretch_h,
|
||||
sum((r for r in rows_sh_min if r is not None)),
|
||||
rows_sh_min, self._rows_sh_max, rows_sh)
|
||||
|
||||
for index, row_stretch in enumerate(rows_sh):
|
||||
# if the row don't have stretch information, nothing to do
|
||||
if not row_stretch:
|
||||
continue
|
||||
# add to the min height whatever remains from size_hint
|
||||
rows[index] += stretch_h * row_stretch / rows_weight
|
||||
|
||||
def _iterate_layout(self, count):
|
||||
orientation = self.orientation
|
||||
padding = self.padding
|
||||
spacing_x, spacing_y = self.spacing
|
||||
|
||||
cols = self._cols
|
||||
if self._fills_from_left_to_right:
|
||||
x_iter = accumulate(chain(
|
||||
(self.x + padding[0], ),
|
||||
(
|
||||
col_width + spacing_x
|
||||
for col_width in islice(cols, len(cols) - 1)
|
||||
),
|
||||
))
|
||||
else:
|
||||
x_iter = accumulate(chain(
|
||||
(self.right - padding[2] - cols[-1], ),
|
||||
(
|
||||
col_width + spacing_x
|
||||
for col_width in islice(reversed(cols), 1, None)
|
||||
),
|
||||
), sub)
|
||||
cols = reversed(cols)
|
||||
|
||||
rows = self._rows
|
||||
if self._fills_from_top_to_bottom:
|
||||
y_iter = accumulate(chain(
|
||||
(self.top - padding[1] - rows[0], ),
|
||||
(
|
||||
row_height + spacing_y
|
||||
for row_height in islice(rows, 1, None)
|
||||
),
|
||||
), sub)
|
||||
else:
|
||||
y_iter = accumulate(chain(
|
||||
(self.y + padding[3], ),
|
||||
(
|
||||
row_height + spacing_y
|
||||
for row_height in islice(reversed(rows), len(rows) - 1)
|
||||
),
|
||||
))
|
||||
rows = reversed(rows)
|
||||
|
||||
if self._fills_row_first:
|
||||
for i, (y, x), (row_height, col_width) in zip(
|
||||
reversed(range(count)),
|
||||
product(y_iter, x_iter),
|
||||
product(rows, cols)):
|
||||
yield i, x, y, col_width, row_height
|
||||
else:
|
||||
for i, (x, y), (col_width, row_height) in zip(
|
||||
reversed(range(count)),
|
||||
product(x_iter, y_iter),
|
||||
product(cols, rows)):
|
||||
yield i, x, y, col_width, row_height
|
||||
|
||||
def do_layout(self, *largs):
|
||||
children = self.children
|
||||
if not children or not self._init_rows_cols_sizes(len(children)):
|
||||
l, t, r, b = self.padding
|
||||
self.minimum_size = l + r, t + b
|
||||
return
|
||||
self._fill_rows_cols_sizes()
|
||||
self._update_minimum_size()
|
||||
self._finalize_rows_cols_sizes()
|
||||
|
||||
for i, x, y, w, h in self._iterate_layout(len(children)):
|
||||
c = children[i]
|
||||
c.pos = x, y
|
||||
shw, shh = c.size_hint
|
||||
shw_min, shh_min = c.size_hint_min
|
||||
shw_max, shh_max = c.size_hint_max
|
||||
|
||||
if shw_min is not None:
|
||||
if shw_max is not None:
|
||||
w = max(min(w, shw_max), shw_min)
|
||||
else:
|
||||
w = max(w, shw_min)
|
||||
else:
|
||||
if shw_max is not None:
|
||||
w = min(w, shw_max)
|
||||
|
||||
if shh_min is not None:
|
||||
if shh_max is not None:
|
||||
h = max(min(h, shh_max), shh_min)
|
||||
else:
|
||||
h = max(h, shh_min)
|
||||
else:
|
||||
if shh_max is not None:
|
||||
h = min(h, shh_max)
|
||||
|
||||
if shw is None:
|
||||
if shh is not None:
|
||||
c.height = h
|
||||
else:
|
||||
if shh is None:
|
||||
c.width = w
|
||||
else:
|
||||
c.size = (w, h)
|
||||
|
||||
def _create_idx_iter(self, n_cols, n_rows):
|
||||
col_indices = range(n_cols) if self._fills_from_left_to_right \
|
||||
else range(n_cols - 1, -1, -1)
|
||||
row_indices = range(n_rows) if self._fills_from_top_to_bottom \
|
||||
else range(n_rows - 1, -1, -1)
|
||||
|
||||
if self._fills_row_first:
|
||||
return (
|
||||
(col_index, row_index)
|
||||
for row_index, col_index in product(row_indices, col_indices))
|
||||
else:
|
||||
return product(col_indices, row_indices)
|
||||
@@ -0,0 +1,528 @@
|
||||
'''
|
||||
Image
|
||||
=====
|
||||
|
||||
The :class:`Image` widget is used to display an image::
|
||||
|
||||
Example in python::
|
||||
|
||||
wimg = Image(source='mylogo.png')
|
||||
|
||||
Kv Example::
|
||||
|
||||
Image:
|
||||
source: 'mylogo.png'
|
||||
size: self.texture_size
|
||||
|
||||
|
||||
Asynchronous Loading
|
||||
--------------------
|
||||
|
||||
To load an image asynchronously (for example from an external webserver), use
|
||||
the :class:`AsyncImage` subclass::
|
||||
|
||||
aimg = AsyncImage(source='http://mywebsite.com/logo.png')
|
||||
|
||||
This can be useful as it prevents your application from waiting until the image
|
||||
is loaded. If you want to display large images or retrieve them from URL's,
|
||||
using :class:`AsyncImage` will allow these resources to be retrieved on a
|
||||
background thread without blocking your application.
|
||||
|
||||
Alignment
|
||||
---------
|
||||
|
||||
By default, the image is centered inside the widget bounding box.
|
||||
|
||||
Adjustment
|
||||
----------
|
||||
|
||||
To control how the image should be adjusted to fit inside the widget box, you
|
||||
should use the :attr:`~kivy.uix.image.Image.fit_mode` property. Available
|
||||
options include:
|
||||
|
||||
- ``"scale-down"``: maintains aspect ratio without stretching.
|
||||
- ``"fill"``: stretches to fill widget, may cause distortion.
|
||||
- ``"contain"``: maintains aspect ratio and resizes to fit inside widget.
|
||||
- ``"cover"``: maintains aspect ratio and stretches to fill widget, may clip
|
||||
image.
|
||||
|
||||
For more details, refer to the :attr:`~kivy.uix.image.Image.fit_mode`.
|
||||
|
||||
|
||||
You can also inherit from Image and create your own style. For example, if you
|
||||
want your image to be greater than the size of your widget, you could do::
|
||||
|
||||
class FullImage(Image):
|
||||
pass
|
||||
|
||||
And in your kivy language file::
|
||||
|
||||
<-FullImage>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: (1, 1, 1)
|
||||
Rectangle:
|
||||
texture: self.texture
|
||||
size: self.width + 20, self.height + 20
|
||||
pos: self.x - 10, self.y - 10
|
||||
|
||||
'''
|
||||
__all__ = ('Image', 'AsyncImage')
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.core.image import Image as CoreImage
|
||||
from kivy.resources import resource_find
|
||||
from kivy.properties import (
|
||||
StringProperty,
|
||||
ObjectProperty,
|
||||
ListProperty,
|
||||
AliasProperty,
|
||||
BooleanProperty,
|
||||
NumericProperty,
|
||||
ColorProperty,
|
||||
OptionProperty
|
||||
)
|
||||
from kivy.logger import Logger
|
||||
|
||||
# delayed imports
|
||||
Loader = None
|
||||
|
||||
|
||||
class Image(Widget):
|
||||
'''Image class, see module documentation for more information.'''
|
||||
|
||||
source = StringProperty(None)
|
||||
'''Filename / source of your image.
|
||||
|
||||
:attr:`source` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
texture = ObjectProperty(None, allownone=True)
|
||||
'''Texture object of the image. The texture represents the original, loaded
|
||||
image texture. It is stretched and positioned during rendering according to
|
||||
the :attr:`fit_mode` property.
|
||||
|
||||
Depending of the texture creation, the value will be a
|
||||
:class:`~kivy.graphics.texture.Texture` or a
|
||||
:class:`~kivy.graphics.texture.TextureRegion` object.
|
||||
|
||||
:attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
texture_size = ListProperty([0, 0])
|
||||
'''Texture size of the image. This represents the original, loaded image
|
||||
texture size.
|
||||
|
||||
.. warning::
|
||||
|
||||
The texture size is set after the texture property. So if you listen to
|
||||
the change on :attr:`texture`, the property texture_size will not be
|
||||
up-to-date. Use self.texture.size instead.
|
||||
'''
|
||||
|
||||
def get_image_ratio(self):
|
||||
if self.texture:
|
||||
return self.texture.width / float(self.texture.height)
|
||||
return 1.0
|
||||
|
||||
mipmap = BooleanProperty(False)
|
||||
'''Indicate if you want OpenGL mipmapping to be applied to the texture.
|
||||
Read :ref:`mipmap` for more information.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
image_ratio = AliasProperty(get_image_ratio, bind=('texture',), cache=True)
|
||||
'''Ratio of the image (width / float(height).
|
||||
|
||||
:attr:`image_ratio` is an :class:`~kivy.properties.AliasProperty` and is
|
||||
read-only.
|
||||
'''
|
||||
|
||||
color = ColorProperty([1, 1, 1, 1])
|
||||
'''Image color, in the format (r, g, b, a). This attribute can be used to
|
||||
'tint' an image. Be careful: if the source image is not gray/white, the
|
||||
color will not really work as expected.
|
||||
|
||||
.. versionadded:: 1.0.6
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to
|
||||
[1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
allow_stretch = BooleanProperty(False, deprecated=True)
|
||||
'''If True, the normalized image size will be maximized to fit in the image
|
||||
box. Otherwise, if the box is too tall, the image will not be
|
||||
stretched more than 1:1 pixels.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
.. deprecated:: 2.2.0
|
||||
:attr:`allow_stretch` have been deprecated. Please use `fit_mode`
|
||||
instead.
|
||||
|
||||
:attr:`allow_stretch` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
keep_ratio = BooleanProperty(True, deprecated=True)
|
||||
'''If False along with allow_stretch being True, the normalized image
|
||||
size will be maximized to fit in the image box and ignores the aspect
|
||||
ratio of the image.
|
||||
Otherwise, if the box is too tall, the image will not be stretched more
|
||||
than 1:1 pixels.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
.. deprecated:: 2.2.0
|
||||
:attr:`keep_ratio` have been deprecated. Please use `fit_mode`
|
||||
instead.
|
||||
|
||||
:attr:`keep_ratio` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
fit_mode = OptionProperty(
|
||||
"scale-down", options=["scale-down", "fill", "contain", "cover"]
|
||||
)
|
||||
'''If the size of the image is different than the size of the widget,
|
||||
determine how the image should be resized to fit inside the widget box.
|
||||
|
||||
Available options:
|
||||
|
||||
- ``"scale-down"``: the image will be scaled down to fit inside the widget
|
||||
box, **maintaining its aspect ratio and without stretching**. If the size
|
||||
of the image is smaller than the widget, it will be displayed at its
|
||||
original size. If the image has a different aspect ratio than the widget,
|
||||
there will be blank areas on the widget box.
|
||||
|
||||
- ``"fill"``: the image is stretched to fill the widget, **regardless of
|
||||
its aspect ratio or dimensions**. If the image has a different aspect ratio
|
||||
than the widget, this option can lead to distortion of the image.
|
||||
|
||||
- ``"contain"``: the image is resized to fit inside the widget box,
|
||||
**maintaining its aspect ratio**. If the image size is larger than the
|
||||
widget size, the behavior will be similar to ``"scale-down"``. However, if
|
||||
the size of the image is smaller than the widget size, unlike
|
||||
``"scale-down``, the image will be resized to fit inside the widget.
|
||||
If the image has a different aspect ratio than the widget, there will be
|
||||
blank areas on the widget box.
|
||||
|
||||
- ``"cover"``: the image will be stretched horizontally or vertically to
|
||||
fill the widget box, **maintaining its aspect ratio**. If the image has a
|
||||
different aspect ratio than the widget, then the image will be clipped to
|
||||
fit.
|
||||
|
||||
:attr:`fit_mode` is a :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to ``"scale-down"``.
|
||||
'''
|
||||
|
||||
keep_data = BooleanProperty(False)
|
||||
'''If True, the underlying _coreimage will store the raw image data.
|
||||
This is useful when performing pixel based collision detection.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
:attr:`keep_data` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
anim_delay = NumericProperty(0.25)
|
||||
'''Delay the animation if the image is sequenced (like an animated gif).
|
||||
If anim_delay is set to -1, the animation will be stopped.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`anim_delay` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.25 (4 FPS).
|
||||
'''
|
||||
|
||||
anim_loop = NumericProperty(0)
|
||||
'''Number of loops to play then stop animating. 0 means keep animating.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`anim_loop` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
nocache = BooleanProperty(False)
|
||||
'''If this property is set True, the image will not be added to the
|
||||
internal cache. The cache will simply ignore any calls trying to
|
||||
append the core image.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
:attr:`nocache` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
def get_norm_image_size(self):
|
||||
if not self.texture:
|
||||
return list(self.size)
|
||||
|
||||
ratio = self.image_ratio
|
||||
w, h = self.size
|
||||
tw, th = self.texture.size
|
||||
|
||||
if self.fit_mode == "cover":
|
||||
widget_ratio = w / max(1, h)
|
||||
if widget_ratio > ratio:
|
||||
return [w, (w * th) / tw]
|
||||
else:
|
||||
return [(h * tw) / th, h]
|
||||
elif self.fit_mode == "fill":
|
||||
return [w, h]
|
||||
elif self.fit_mode == "contain":
|
||||
iw = w
|
||||
else:
|
||||
iw = min(w, tw)
|
||||
|
||||
# calculate the appropriate height
|
||||
ih = iw / ratio
|
||||
# if the height is too higher, take the height of the container
|
||||
# and calculate appropriate width. no need to test further. :)
|
||||
if ih > h:
|
||||
if self.fit_mode == "contain":
|
||||
ih = h
|
||||
else:
|
||||
ih = min(h, th)
|
||||
iw = ih * ratio
|
||||
return [iw, ih]
|
||||
|
||||
norm_image_size = AliasProperty(
|
||||
get_norm_image_size,
|
||||
bind=(
|
||||
'texture',
|
||||
'size',
|
||||
'image_ratio',
|
||||
'fit_mode',
|
||||
),
|
||||
cache=True,
|
||||
)
|
||||
'''Normalized image size within the widget box.
|
||||
|
||||
This size will always fit the widget size and will preserve the image
|
||||
ratio.
|
||||
|
||||
:attr:`norm_image_size` is an :class:`~kivy.properties.AliasProperty` and
|
||||
is read-only.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._coreimage = None
|
||||
self._loops = 0
|
||||
update = self.texture_update
|
||||
fbind = self.fbind
|
||||
fbind('source', update)
|
||||
fbind('mipmap', update)
|
||||
|
||||
# NOTE: Compatibility code due to deprecated properties.
|
||||
fbind('keep_ratio', self._update_fit_mode)
|
||||
fbind('allow_stretch', self._update_fit_mode)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _update_fit_mode(self, *args):
|
||||
keep_ratio = self.keep_ratio
|
||||
allow_stretch = self.allow_stretch
|
||||
if (
|
||||
not keep_ratio and not allow_stretch
|
||||
or keep_ratio and not allow_stretch
|
||||
):
|
||||
self.fit_mode = "scale-down"
|
||||
elif not keep_ratio and allow_stretch:
|
||||
self.fit_mode = "fill"
|
||||
elif keep_ratio and allow_stretch:
|
||||
self.fit_mode = "contain"
|
||||
|
||||
def texture_update(self, *largs):
|
||||
self.set_texture_from_resource(self.source)
|
||||
|
||||
def set_texture_from_resource(self, resource):
|
||||
if not resource:
|
||||
self._clear_core_image()
|
||||
return
|
||||
source = resource_find(resource)
|
||||
if not source:
|
||||
Logger.error('Image: Not found <%s>' % resource)
|
||||
self._clear_core_image()
|
||||
return
|
||||
if self._coreimage:
|
||||
self._coreimage.unbind(on_texture=self._on_tex_change)
|
||||
try:
|
||||
self._coreimage = image = CoreImage(
|
||||
source,
|
||||
mipmap=self.mipmap,
|
||||
anim_delay=self.anim_delay,
|
||||
keep_data=self.keep_data,
|
||||
nocache=self.nocache
|
||||
)
|
||||
except Exception:
|
||||
Logger.error('Image: Error loading <%s>' % resource)
|
||||
self._clear_core_image()
|
||||
image = self._coreimage
|
||||
if image:
|
||||
image.bind(on_texture=self._on_tex_change)
|
||||
self.texture = image.texture
|
||||
|
||||
def on_anim_delay(self, instance, value):
|
||||
if self._coreimage is None:
|
||||
return
|
||||
self._coreimage.anim_delay = value
|
||||
if value < 0:
|
||||
self._coreimage.anim_reset(False)
|
||||
|
||||
def on_texture(self, instance, value):
|
||||
self.texture_size = value.size if value else [0, 0]
|
||||
|
||||
def _clear_core_image(self):
|
||||
if self._coreimage:
|
||||
self._coreimage.unbind(on_texture=self._on_tex_change)
|
||||
self.texture = None
|
||||
self._coreimage = None
|
||||
self._loops = 0
|
||||
|
||||
def _on_tex_change(self, *largs):
|
||||
# update texture from core image
|
||||
self.texture = self._coreimage.texture
|
||||
ci = self._coreimage
|
||||
if self.anim_loop and ci._anim_index == len(ci._image.textures) - 1:
|
||||
self._loops += 1
|
||||
if self.anim_loop == self._loops:
|
||||
ci.anim_reset(False)
|
||||
self._loops = 0
|
||||
|
||||
def reload(self):
|
||||
'''Reload image from disk. This facilitates re-loading of
|
||||
images from disk in case the image content changes.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
Usage::
|
||||
|
||||
im = Image(source = '1.jpg')
|
||||
# -- do something --
|
||||
im.reload()
|
||||
# image will be re-loaded from disk
|
||||
|
||||
'''
|
||||
self.remove_from_cache()
|
||||
old_source = self.source
|
||||
self.source = ''
|
||||
self.source = old_source
|
||||
|
||||
def remove_from_cache(self):
|
||||
'''Remove image from cache.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
'''
|
||||
if self._coreimage:
|
||||
self._coreimage.remove_from_cache()
|
||||
|
||||
def on_nocache(self, *args):
|
||||
if self.nocache:
|
||||
self.remove_from_cache()
|
||||
if self._coreimage:
|
||||
self._coreimage._nocache = True
|
||||
|
||||
|
||||
class AsyncImage(Image):
|
||||
'''Asynchronous Image class. See the module documentation for more
|
||||
information.
|
||||
|
||||
.. note::
|
||||
|
||||
The AsyncImage is a specialized form of the Image class. You may
|
||||
want to refer to the :mod:`~kivy.loader` documentation and in
|
||||
particular, the :class:`~kivy.loader.ProxyImage` for more detail
|
||||
on how to handle events around asynchronous image loading.
|
||||
|
||||
.. note::
|
||||
|
||||
AsyncImage currently does not support properties
|
||||
:attr:`anim_loop` and :attr:`mipmap` and setting those properties will
|
||||
have no effect.
|
||||
'''
|
||||
|
||||
__events__ = ('on_error', 'on_load')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._found_source = None
|
||||
self._coreimage = None
|
||||
global Loader
|
||||
if not Loader:
|
||||
from kivy.loader import Loader
|
||||
self.fbind('source', self._load_source)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _load_source(self, *args):
|
||||
source = self.source
|
||||
if not source:
|
||||
self._clear_core_image()
|
||||
return
|
||||
if not self.is_uri(source):
|
||||
source = resource_find(source)
|
||||
if not source:
|
||||
Logger.error('AsyncImage: Not found <%s>' % self.source)
|
||||
self._clear_core_image()
|
||||
return
|
||||
self._found_source = source
|
||||
self._coreimage = image = Loader.image(
|
||||
source,
|
||||
nocache=self.nocache,
|
||||
mipmap=self.mipmap,
|
||||
anim_delay=self.anim_delay
|
||||
)
|
||||
image.bind(
|
||||
on_load=self._on_source_load,
|
||||
on_error=self._on_source_error,
|
||||
on_texture=self._on_tex_change
|
||||
)
|
||||
self.texture = image.texture
|
||||
|
||||
def _on_source_load(self, value):
|
||||
image = self._coreimage.image
|
||||
if not image:
|
||||
return
|
||||
self.texture = image.texture
|
||||
self.dispatch('on_load')
|
||||
|
||||
def _on_source_error(self, instance, error=None):
|
||||
self.dispatch('on_error', error)
|
||||
|
||||
def on_error(self, error):
|
||||
pass
|
||||
|
||||
def on_load(self, *args):
|
||||
pass
|
||||
|
||||
def is_uri(self, filename):
|
||||
proto = filename.split('://', 1)[0]
|
||||
return proto in ('http', 'https', 'ftp', 'smb')
|
||||
|
||||
def _clear_core_image(self):
|
||||
if self._coreimage:
|
||||
self._coreimage.unbind(on_load=self._on_source_load)
|
||||
super()._clear_core_image()
|
||||
self._found_source = None
|
||||
|
||||
def _on_tex_change(self, *largs):
|
||||
if self._coreimage:
|
||||
self.texture = self._coreimage.texture
|
||||
|
||||
def texture_update(self, *largs):
|
||||
pass
|
||||
|
||||
def remove_from_cache(self):
|
||||
if self._found_source:
|
||||
Loader.remove_from_cache(self._found_source)
|
||||
super().remove_from_cache()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,322 @@
|
||||
'''
|
||||
Layout
|
||||
======
|
||||
|
||||
Layouts are used to calculate and assign widget positions.
|
||||
|
||||
The :class:`Layout` class itself cannot be used directly.
|
||||
You should use one of the following layout classes:
|
||||
|
||||
- Anchor layout: :class:`kivy.uix.anchorlayout.AnchorLayout`
|
||||
- Box layout: :class:`kivy.uix.boxlayout.BoxLayout`
|
||||
- Float layout: :class:`kivy.uix.floatlayout.FloatLayout`
|
||||
- Grid layout: :class:`kivy.uix.gridlayout.GridLayout`
|
||||
- Page Layout: :class:`kivy.uix.pagelayout.PageLayout`
|
||||
- Relative layout: :class:`kivy.uix.relativelayout.RelativeLayout`
|
||||
- Scatter layout: :class:`kivy.uix.scatterlayout.ScatterLayout`
|
||||
- Stack layout: :class:`kivy.uix.stacklayout.StackLayout`
|
||||
|
||||
|
||||
Understanding the `size_hint` Property in `Widget`
|
||||
--------------------------------------------------
|
||||
|
||||
The :attr:`~kivy.uix.Widget.size_hint` is a tuple of values used by
|
||||
layouts to manage the sizes of their children. It indicates the size
|
||||
relative to the layout's size instead of an absolute size (in
|
||||
pixels/points/cm/etc). The format is::
|
||||
|
||||
widget.size_hint = (width_proportion, height_proportion)
|
||||
|
||||
The proportions are specified as floating point numbers in the range 0-1. For
|
||||
example, 0.5 represents 50%, 1 represents 100%.
|
||||
|
||||
If you want a widget's width to be half of the parent's width and the
|
||||
height to be identical to the parent's height, you would do::
|
||||
|
||||
widget.size_hint = (0.5, 1.0)
|
||||
|
||||
If you don't want to use a size_hint for either the width or height, set the
|
||||
value to None. For example, to make a widget that is 250px wide and 30%
|
||||
of the parent's height, do::
|
||||
|
||||
widget.size_hint = (None, 0.3)
|
||||
widget.width = 250
|
||||
|
||||
Being :class:`Kivy properties <kivy.properties>`, these can also be set via
|
||||
constructor arguments::
|
||||
|
||||
widget = Widget(size_hint=(None, 0.3), width=250)
|
||||
|
||||
.. versionchanged:: 1.4.1
|
||||
The `reposition_child` internal method (made public by mistake) has
|
||||
been removed.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Layout', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.compat import isclose
|
||||
|
||||
|
||||
class Layout(Widget):
|
||||
'''Layout interface class, used to implement every layout. See module
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
_trigger_layout = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if self.__class__ == Layout:
|
||||
raise Exception('The Layout class is abstract and \
|
||||
cannot be used directly.')
|
||||
if self._trigger_layout is None:
|
||||
self._trigger_layout = Clock.create_trigger(self.do_layout, -1)
|
||||
super(Layout, self).__init__(**kwargs)
|
||||
|
||||
def do_layout(self, *largs):
|
||||
'''This function is called when a layout is called by a trigger.
|
||||
If you are writing a new Layout subclass, don't call this function
|
||||
directly but use :meth:`_trigger_layout` instead.
|
||||
|
||||
The function is by default called *before* the next frame, therefore
|
||||
the layout isn't updated immediately. Anything depending on the
|
||||
positions of e.g. children should be scheduled for the next frame.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
'''
|
||||
raise NotImplementedError('Must be implemented in subclasses.')
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
fbind = widget.fbind
|
||||
fbind('size', self._trigger_layout)
|
||||
fbind('size_hint', self._trigger_layout)
|
||||
fbind('size_hint_max', self._trigger_layout)
|
||||
fbind('size_hint_min', self._trigger_layout)
|
||||
super(Layout, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
funbind = widget.funbind
|
||||
funbind('size', self._trigger_layout)
|
||||
funbind('size_hint', self._trigger_layout)
|
||||
funbind('size_hint_max', self._trigger_layout)
|
||||
funbind('size_hint_min', self._trigger_layout)
|
||||
super(Layout, self).remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def layout_hint_with_bounds(
|
||||
self, sh_sum, available_space, min_bounded_size, sh_min_vals,
|
||||
sh_max_vals, hint):
|
||||
'''(internal) Computes the appropriate (size) hint for all the
|
||||
widgets given (potential) min or max bounds on the widgets' size.
|
||||
The ``hint`` list is updated with appropriate sizes.
|
||||
|
||||
It walks through the hints and for any widgets whose hint will result
|
||||
in violating min or max constraints, it fixes the hint. Any remaining
|
||||
or missing space after all the widgets are fixed get distributed
|
||||
to the widgets making them smaller or larger according to their
|
||||
size hint.
|
||||
|
||||
This algorithms knows nothing about the widgets other than what is
|
||||
passed through the input params, so it's fairly generic for laying
|
||||
things out according to constraints using size hints.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`sh_sum`: float
|
||||
The sum of the size hints (basically ``sum(size_hint)``).
|
||||
`available_space`: float
|
||||
The amount of pixels available for all the widgets
|
||||
whose size hint is not None. Cannot be zero.
|
||||
`min_bounded_size`: float
|
||||
The minimum amount of space required according to the
|
||||
`size_hint_min` of the widgets (basically
|
||||
``sum(size_hint_min)``).
|
||||
`sh_min_vals`: list or iterable
|
||||
Items in the iterable are the size_hint_min for each widget.
|
||||
Can be None. The length should be the same as ``hint``
|
||||
`sh_max_vals`: list or iterable
|
||||
Items in the iterable are the size_hint_max for each widget.
|
||||
Can be None. The length should be the same as ``hint``
|
||||
`hint`: list
|
||||
A list whose size is the same as the length of ``sh_min_vals``
|
||||
and ``sh_min_vals`` whose each element is the corresponding
|
||||
size hint value of that element. This list is updated in place
|
||||
with correct size hints that ensure the constraints are not
|
||||
violated.
|
||||
|
||||
:returns:
|
||||
Nothing. ``hint`` is updated in place.
|
||||
'''
|
||||
if not sh_sum:
|
||||
return
|
||||
# TODO: test when children have size_hint, max/min of zero
|
||||
|
||||
# all divs are float denominator ;)
|
||||
stretch_ratio = sh_sum / float(available_space)
|
||||
if available_space <= min_bounded_size or \
|
||||
isclose(available_space, min_bounded_size):
|
||||
# too small, just set to min
|
||||
for i, (sh, sh_min) in enumerate(zip(hint, sh_min_vals)):
|
||||
if sh is None:
|
||||
continue
|
||||
|
||||
if sh_min is not None:
|
||||
hint[i] = sh_min * stretch_ratio # set to min size
|
||||
else:
|
||||
hint[i] = 0. # everything else is zero
|
||||
return
|
||||
|
||||
# these dicts take i (widget child) as key
|
||||
not_mined_contrib = {} # all who's sh > min_sh or had no min_sh
|
||||
not_maxed_contrib = {} # all who's sh < max_sh or had no max_sh
|
||||
sh_mins_avail = {} # the sh amt removable until we hit sh_min
|
||||
sh_maxs_avail = {} # the sh amt addable until we hit sh_max
|
||||
oversize_amt = undersize_amt = 0
|
||||
hint_orig = hint[:]
|
||||
|
||||
# first, for all the items, set them to be within their max/min
|
||||
# size_hint bound, also find how much their size_hint can be reduced
|
||||
# or increased
|
||||
for i, (sh, sh_min, sh_max) in enumerate(
|
||||
zip(hint, sh_min_vals, sh_max_vals)):
|
||||
if sh is None:
|
||||
continue
|
||||
|
||||
diff = 0
|
||||
|
||||
if sh_min is not None:
|
||||
sh_min *= stretch_ratio
|
||||
diff = sh_min - sh # how much we are under the min
|
||||
|
||||
if diff > 0:
|
||||
hint[i] = sh_min
|
||||
undersize_amt += diff
|
||||
else:
|
||||
not_mined_contrib[i] = None
|
||||
|
||||
sh_mins_avail[i] = hint[i] - sh_min
|
||||
else:
|
||||
not_mined_contrib[i] = None
|
||||
sh_mins_avail[i] = hint[i]
|
||||
|
||||
if sh_max is not None:
|
||||
sh_max *= stretch_ratio
|
||||
diff = sh - sh_max
|
||||
|
||||
if diff > 0:
|
||||
hint[i] = sh_max # how much we are over the max
|
||||
oversize_amt += diff
|
||||
else:
|
||||
not_maxed_contrib[i] = None
|
||||
|
||||
sh_maxs_avail[i] = sh_max - hint[i]
|
||||
else:
|
||||
not_maxed_contrib[i] = None
|
||||
sh_maxs_avail[i] = sh_sum - hint[i]
|
||||
|
||||
if i in not_mined_contrib:
|
||||
not_mined_contrib[i] = max(0., diff) # how much got removed
|
||||
if i in not_maxed_contrib:
|
||||
not_maxed_contrib[i] = max(0., diff) # how much got added
|
||||
|
||||
# if margin is zero, the amount of the widgets that were made smaller
|
||||
# magically equals the amount of the widgets that were made larger
|
||||
# so we're all good
|
||||
margin = oversize_amt - undersize_amt
|
||||
if isclose(oversize_amt, undersize_amt, abs_tol=1e-15):
|
||||
return
|
||||
|
||||
# we need to redistribute the margin among all widgets
|
||||
# if margin is positive, then we have extra space because the widgets
|
||||
# that were larger and were reduced contributed more, so increase
|
||||
# the size hint for those that are allowed to be larger by the
|
||||
# most allowed, proportionately to their size (or inverse size hint).
|
||||
# similarly for the opposite case
|
||||
if margin > 1e-15:
|
||||
contrib_amt = not_maxed_contrib
|
||||
sh_available = sh_maxs_avail
|
||||
mult = 1.
|
||||
contrib_proportion = hint_orig
|
||||
elif margin < -1e-15:
|
||||
margin *= -1.
|
||||
contrib_amt = not_mined_contrib
|
||||
sh_available = sh_mins_avail
|
||||
mult = -1.
|
||||
|
||||
# when reducing the size of widgets proportionately, those with
|
||||
# larger sh get reduced less, and those with smaller, more.
|
||||
mn = min((h for h in hint_orig if h))
|
||||
mx = max((h for h in hint_orig if h is not None))
|
||||
hint_top = (2. * mn if mn else 1.) if mn == mx else mn + mx
|
||||
contrib_proportion = [None if h is None else hint_top - h for
|
||||
h in hint_orig]
|
||||
|
||||
# contrib_amt is all the widgets that are not their max/min and
|
||||
# can afford to be made bigger/smaller
|
||||
# We only use the contrib_amt indices from now on
|
||||
contrib_prop_sum = float(
|
||||
sum((contrib_proportion[i] for i in contrib_amt)))
|
||||
|
||||
if contrib_prop_sum < 1e-9:
|
||||
assert mult == 1. # should only happen when all sh are zero
|
||||
return
|
||||
|
||||
contrib_height = {
|
||||
i: val / (contrib_proportion[i] / contrib_prop_sum) for
|
||||
i, val in contrib_amt.items()}
|
||||
items = sorted(
|
||||
(i for i in contrib_amt),
|
||||
key=lambda x: contrib_height[x])
|
||||
|
||||
j = items[0]
|
||||
sum_i_contributed = contrib_amt[j]
|
||||
last_height = contrib_height[j]
|
||||
sh_available_i = {j: sh_available[j]}
|
||||
contrib_prop_sum_i = contrib_proportion[j]
|
||||
|
||||
n = len(items) # check when n <= 1
|
||||
i = 1
|
||||
if 1 < n:
|
||||
j = items[1]
|
||||
curr_height = contrib_height[j]
|
||||
|
||||
done = False
|
||||
while not done and i < n:
|
||||
while i < n and last_height == curr_height:
|
||||
j = items[i]
|
||||
sum_i_contributed += contrib_amt[j]
|
||||
contrib_prop_sum_i += contrib_proportion[j]
|
||||
sh_available_i[j] = sh_available[j]
|
||||
curr_height = contrib_height[j]
|
||||
i += 1
|
||||
last_height = curr_height
|
||||
|
||||
while not done:
|
||||
margin_height = ((margin + sum_i_contributed) /
|
||||
(contrib_prop_sum_i / contrib_prop_sum))
|
||||
if margin_height - curr_height > 1e-9 and i < n:
|
||||
break
|
||||
|
||||
done = True
|
||||
for k, available_sh in list(sh_available_i.items()):
|
||||
if margin_height - available_sh / (
|
||||
contrib_proportion[k] / contrib_prop_sum) > 1e-9:
|
||||
del sh_available_i[k]
|
||||
sum_i_contributed -= contrib_amt[k]
|
||||
contrib_prop_sum_i -= contrib_proportion[k]
|
||||
margin -= available_sh
|
||||
hint[k] += mult * available_sh
|
||||
done = False
|
||||
|
||||
if not sh_available_i: # all were under the margin
|
||||
break
|
||||
|
||||
if sh_available_i:
|
||||
assert contrib_prop_sum_i and margin
|
||||
margin_height = ((margin + sum_i_contributed) /
|
||||
(contrib_prop_sum_i / contrib_prop_sum))
|
||||
for i in sh_available_i:
|
||||
hint[i] += mult * (
|
||||
margin_height * contrib_proportion[i] / contrib_prop_sum -
|
||||
contrib_amt[i])
|
||||
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
ModalView
|
||||
=========
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
The :class:`ModalView` widget is used to create modal views. By default, the
|
||||
view will cover the whole "main" window.
|
||||
|
||||
Remember that the default size of a Widget is size_hint=(1, 1). If you don't
|
||||
want your view to be fullscreen, either use size hints with values lower than
|
||||
1 (for instance size_hint=(.8, .8)) or deactivate the size_hint and use fixed
|
||||
size attributes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Example of a simple 400x400 Hello world view::
|
||||
|
||||
view = ModalView(size_hint=(None, None), size=(400, 400))
|
||||
view.add_widget(Label(text='Hello world'))
|
||||
|
||||
By default, any click outside the view will dismiss it. If you don't
|
||||
want that, you can set :attr:`ModalView.auto_dismiss` to False::
|
||||
|
||||
view = ModalView(auto_dismiss=False)
|
||||
view.add_widget(Label(text='Hello world'))
|
||||
view.open()
|
||||
|
||||
To manually dismiss/close the view, use the :meth:`ModalView.dismiss` method of
|
||||
the ModalView instance::
|
||||
|
||||
view.dismiss()
|
||||
|
||||
Both :meth:`ModalView.open` and :meth:`ModalView.dismiss` are bind-able. That
|
||||
means you can directly bind the function to an action, e.g. to a button's
|
||||
on_press ::
|
||||
|
||||
# create content and add it to the view
|
||||
content = Button(text='Close me!')
|
||||
view = ModalView(auto_dismiss=False)
|
||||
view.add_widget(content)
|
||||
|
||||
# bind the on_press event of the button to the dismiss function
|
||||
content.bind(on_press=view.dismiss)
|
||||
|
||||
# open the view
|
||||
view.open()
|
||||
|
||||
|
||||
ModalView Events
|
||||
----------------
|
||||
|
||||
There are four events available: `on_pre_open` and `on_open` which are raised
|
||||
when the view is opening; `on_pre_dismiss` and `on_dismiss` which are raised
|
||||
when the view is closed.
|
||||
|
||||
For `on_dismiss`, you can prevent the view from closing by explicitly
|
||||
returning `True` from your callback::
|
||||
|
||||
def my_callback(instance):
|
||||
print('ModalView', instance, 'is being dismissed, but is prevented!')
|
||||
return True
|
||||
view = ModalView()
|
||||
view.add_widget(Label(text='Hello world'))
|
||||
view.bind(on_dismiss=my_callback)
|
||||
view.open()
|
||||
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The ModalView can be closed by hitting the escape key on the
|
||||
keyboard if the :attr:`ModalView.auto_dismiss` property is True (the
|
||||
default).
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ('ModalView', )
|
||||
|
||||
from kivy.animation import Animation
|
||||
from kivy.properties import (
|
||||
StringProperty, BooleanProperty, ObjectProperty, NumericProperty,
|
||||
ListProperty, ColorProperty)
|
||||
from kivy.uix.anchorlayout import AnchorLayout
|
||||
|
||||
|
||||
class ModalView(AnchorLayout):
|
||||
"""ModalView class. See module documentation for more information.
|
||||
|
||||
:Events:
|
||||
`on_pre_open`:
|
||||
Fired before the ModalView is opened. When this event is fired
|
||||
ModalView is not yet added to window.
|
||||
`on_open`:
|
||||
Fired when the ModalView is opened.
|
||||
`on_pre_dismiss`:
|
||||
Fired before the ModalView is closed.
|
||||
`on_dismiss`:
|
||||
Fired when the ModalView is closed. If the callback returns True,
|
||||
the dismiss will be canceled.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
Added events `on_pre_open` and `on_pre_dismiss`.
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Added property 'overlay_color'.
|
||||
|
||||
.. versionchanged:: 2.1.0
|
||||
Marked `attach_to` property as deprecated.
|
||||
|
||||
"""
|
||||
|
||||
# noinspection PyArgumentEqualDefault
|
||||
auto_dismiss = BooleanProperty(True)
|
||||
'''This property determines if the view is automatically
|
||||
dismissed when the user clicks outside it.
|
||||
|
||||
:attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
attach_to = ObjectProperty(None, deprecated=True)
|
||||
'''If a widget is set on attach_to, the view will attach to the nearest
|
||||
parent window of the widget. If none is found, it will attach to the
|
||||
main/global Window.
|
||||
|
||||
:attr:`attach_to` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a).
|
||||
|
||||
This acts as a *multiplier* to the texture color. The default
|
||||
texture is grey, so just setting the background color will give
|
||||
a darker result. To set a plain color, set the
|
||||
:attr:`background_normal` to ``''``.
|
||||
|
||||
The :attr:`background_color` is a
|
||||
:class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed behavior to affect the background of the widget itself, not
|
||||
the overlay dimming.
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background = StringProperty(
|
||||
'atlas://data/images/defaulttheme/modalview-background')
|
||||
'''Background image of the view used for the view background.
|
||||
|
||||
:attr:`background` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/modalview-background'.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used for the :attr:`background_normal` and the
|
||||
:attr:`background_down` properties. Can be used when using custom
|
||||
backgrounds.
|
||||
|
||||
It must be a list of four values: (bottom, right, top, left). Read the
|
||||
BorderImage instructions for more information about how to use it.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16).
|
||||
'''
|
||||
|
||||
overlay_color = ColorProperty([0, 0, 0, .7])
|
||||
'''Overlay color in the format (r, g, b, a).
|
||||
Used for dimming the window behind the modal view.
|
||||
|
||||
:attr:`overlay_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [0, 0, 0, .7].
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
'''
|
||||
|
||||
# Internals properties used for graphical representation.
|
||||
|
||||
_anim_alpha = NumericProperty(0)
|
||||
|
||||
_anim_duration = NumericProperty(.1)
|
||||
|
||||
_window = ObjectProperty(allownone=True, rebind=True)
|
||||
|
||||
_is_open = BooleanProperty(False)
|
||||
|
||||
_touch_started_inside = None
|
||||
|
||||
__events__ = ('on_pre_open', 'on_open', 'on_pre_dismiss', 'on_dismiss')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._parent = None
|
||||
super(ModalView, self).__init__(**kwargs)
|
||||
|
||||
def open(self, *_args, **kwargs):
|
||||
"""Display the modal in the Window.
|
||||
|
||||
When the view is opened, it will be faded in with an animation. If you
|
||||
don't want the animation, use::
|
||||
|
||||
view.open(animation=False)
|
||||
|
||||
"""
|
||||
from kivy.core.window import Window
|
||||
if self._is_open:
|
||||
return
|
||||
self._window = Window
|
||||
self._is_open = True
|
||||
self.dispatch('on_pre_open')
|
||||
Window.add_widget(self)
|
||||
Window.bind(
|
||||
on_resize=self._align_center,
|
||||
on_keyboard=self._handle_keyboard)
|
||||
self.center = Window.center
|
||||
self.fbind('center', self._align_center)
|
||||
self.fbind('size', self._align_center)
|
||||
if kwargs.get('animation', True):
|
||||
ani = Animation(_anim_alpha=1., d=self._anim_duration)
|
||||
ani.bind(on_complete=lambda *_args: self.dispatch('on_open'))
|
||||
ani.start(self)
|
||||
else:
|
||||
self._anim_alpha = 1.
|
||||
self.dispatch('on_open')
|
||||
|
||||
def dismiss(self, *_args, **kwargs):
|
||||
""" Close the view if it is open.
|
||||
|
||||
If you really want to close the view, whatever the on_dismiss
|
||||
event returns, you can use the *force* keyword argument::
|
||||
|
||||
view = ModalView()
|
||||
view.dismiss(force=True)
|
||||
|
||||
When the view is dismissed, it will be faded out before being
|
||||
removed from the parent. If you don't want this animation, use::
|
||||
|
||||
view.dismiss(animation=False)
|
||||
|
||||
"""
|
||||
if not self._is_open:
|
||||
return
|
||||
self.dispatch('on_pre_dismiss')
|
||||
if self.dispatch('on_dismiss') is True:
|
||||
if kwargs.get('force', False) is not True:
|
||||
return
|
||||
if kwargs.get('animation', True):
|
||||
Animation(_anim_alpha=0., d=self._anim_duration).start(self)
|
||||
else:
|
||||
self._anim_alpha = 0
|
||||
self._real_remove_widget()
|
||||
|
||||
def _align_center(self, *_args):
|
||||
if self._is_open:
|
||||
self.center = self._window.center
|
||||
|
||||
def on_motion(self, etype, me):
|
||||
super().on_motion(etype, me)
|
||||
return True
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
""" touch down event handler. """
|
||||
self._touch_started_inside = self.collide_point(*touch.pos)
|
||||
if not self.auto_dismiss or self._touch_started_inside:
|
||||
super().on_touch_down(touch)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
""" touch moved event handler. """
|
||||
if not self.auto_dismiss or self._touch_started_inside:
|
||||
super().on_touch_move(touch)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
""" touch up event handler. """
|
||||
# Explicitly test for False as None occurs when shown by on_touch_down
|
||||
if self.auto_dismiss and self._touch_started_inside is False:
|
||||
self.dismiss()
|
||||
else:
|
||||
super().on_touch_up(touch)
|
||||
self._touch_started_inside = None
|
||||
return True
|
||||
|
||||
def on__anim_alpha(self, _instance, value):
|
||||
""" animation progress callback. """
|
||||
if value == 0 and self._is_open:
|
||||
self._real_remove_widget()
|
||||
|
||||
def _real_remove_widget(self):
|
||||
if not self._is_open:
|
||||
return
|
||||
self._window.remove_widget(self)
|
||||
self._window.unbind(
|
||||
on_resize=self._align_center,
|
||||
on_keyboard=self._handle_keyboard)
|
||||
self._is_open = False
|
||||
self._window = None
|
||||
|
||||
def on_pre_open(self):
|
||||
""" default pre-open event handler. """
|
||||
|
||||
def on_open(self):
|
||||
""" default open event handler. """
|
||||
|
||||
def on_pre_dismiss(self):
|
||||
""" default pre-dismiss event handler. """
|
||||
|
||||
def on_dismiss(self):
|
||||
""" default dismiss event handler. """
|
||||
|
||||
def _handle_keyboard(self, _window, key, *_args):
|
||||
if key == 27 and self.auto_dismiss:
|
||||
self.dismiss()
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.button import Button
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
|
||||
# add view
|
||||
content = GridLayout(cols=1)
|
||||
content.add_widget(Label(text='This is a hello world'))
|
||||
view = ModalView(size_hint=(None, None), size=(256, 256))
|
||||
view.add_widget(content)
|
||||
|
||||
layout = GridLayout(cols=3)
|
||||
for x in range(9):
|
||||
btn = Button(text=f"click me {x}")
|
||||
btn.bind(on_release=view.open)
|
||||
layout.add_widget(btn)
|
||||
Window.add_widget(layout)
|
||||
|
||||
view.open()
|
||||
runTouchApp()
|
||||
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
PageLayout
|
||||
==========
|
||||
|
||||
.. image:: images/pagelayout.gif
|
||||
:align: right
|
||||
|
||||
The :class:`PageLayout` class is used to create a simple multi-page
|
||||
layout, in a way that allows easy flipping from one page to another using
|
||||
borders.
|
||||
|
||||
:class:`PageLayout` does not currently honor the
|
||||
:attr:`~kivy.uix.widget.Widget.size_hint`,
|
||||
:attr:`~kivy.uix.widget.Widget.size_hint_min`,
|
||||
:attr:`~kivy.uix.widget.Widget.size_hint_max`, or
|
||||
:attr:`~kivy.uix.widget.Widget.pos_hint` properties.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
PageLayout:
|
||||
Button:
|
||||
text: 'page1'
|
||||
Button:
|
||||
text: 'page2'
|
||||
Button:
|
||||
text: 'page3'
|
||||
|
||||
Transitions from one page to the next are made by swiping in from the border
|
||||
areas on the right or left hand side. If you wish to display multiple widgets
|
||||
in a page, we suggest you use a containing layout. Ideally, each page should
|
||||
consist of a single :mod:`~kivy.uix.layout` widget that contains the remaining
|
||||
widgets on that page.
|
||||
"""
|
||||
|
||||
__all__ = ('PageLayout', )
|
||||
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import NumericProperty, DictProperty
|
||||
from kivy.animation import Animation
|
||||
|
||||
|
||||
class PageLayout(Layout):
|
||||
'''PageLayout class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
page = NumericProperty(0)
|
||||
'''The currently displayed page.
|
||||
|
||||
:data:`page` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 0.
|
||||
'''
|
||||
|
||||
border = NumericProperty('50dp')
|
||||
'''The width of the border around the current page used to display
|
||||
the previous/next page swipe areas when needed.
|
||||
|
||||
:data:`border` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 50dp.
|
||||
'''
|
||||
|
||||
swipe_threshold = NumericProperty(.5)
|
||||
'''The threshold used to trigger swipes as ratio of the widget
|
||||
size.
|
||||
|
||||
:data:`swipe_threshold` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to .5.
|
||||
'''
|
||||
|
||||
anim_kwargs = DictProperty({'d': .5, 't': 'in_quad'})
|
||||
'''The animation kwargs used to construct the animation
|
||||
|
||||
:data:`anim_kwargs` is a :class:`~kivy.properties.DictProperty`
|
||||
and defaults to {'d': .5, 't': 'in_quad'}.
|
||||
|
||||
.. versionadded:: 1.11.0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(PageLayout, self).__init__(**kwargs)
|
||||
|
||||
trigger = self._trigger_layout
|
||||
fbind = self.fbind
|
||||
fbind('border', trigger)
|
||||
fbind('page', trigger)
|
||||
fbind('parent', trigger)
|
||||
fbind('children', trigger)
|
||||
fbind('size', trigger)
|
||||
fbind('pos', trigger)
|
||||
|
||||
def do_layout(self, *largs):
|
||||
l_children = len(self.children) - 1
|
||||
h = self.height
|
||||
x_parent, y_parent = self.pos
|
||||
p = self.page
|
||||
border = self.border
|
||||
half_border = border / 2.
|
||||
right = self.right
|
||||
width = self.width - border
|
||||
for i, c in enumerate(reversed(self.children)):
|
||||
|
||||
if i < p:
|
||||
x = x_parent
|
||||
elif i == p:
|
||||
if not p: # it's first page
|
||||
x = x_parent
|
||||
elif p != l_children: # not first, but there are post pages
|
||||
x = x_parent + half_border
|
||||
else: # not first and there are no post pages
|
||||
x = x_parent + border
|
||||
elif i == p + 1:
|
||||
if not p: # second page - no left margin
|
||||
x = right - border
|
||||
else: # there's already a left margin
|
||||
x = right - half_border
|
||||
else:
|
||||
x = right
|
||||
|
||||
c.height = h
|
||||
c.width = width
|
||||
|
||||
Animation(
|
||||
x=x,
|
||||
y=y_parent,
|
||||
**self.anim_kwargs).start(c)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if (
|
||||
self.disabled or
|
||||
not self.collide_point(*touch.pos) or
|
||||
not self.children
|
||||
):
|
||||
return
|
||||
|
||||
page = self.children[-self.page - 1]
|
||||
if self.x <= touch.x < page.x:
|
||||
touch.ud['page'] = 'previous'
|
||||
touch.grab(self)
|
||||
return True
|
||||
elif page.right <= touch.x < self.right:
|
||||
touch.ud['page'] = 'next'
|
||||
touch.grab(self)
|
||||
return True
|
||||
return page.on_touch_down(touch)
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current != self:
|
||||
return
|
||||
|
||||
p = self.page
|
||||
border = self.border
|
||||
half_border = border / 2.
|
||||
page = self.children[-p - 1]
|
||||
if touch.ud['page'] == 'previous':
|
||||
# move next page up to right edge
|
||||
if p < len(self.children) - 1:
|
||||
self.children[-p - 2].x = min(
|
||||
self.right - self.border * (1 - (touch.sx - touch.osx)),
|
||||
self.right)
|
||||
|
||||
# move current page until edge hits the right border
|
||||
if p >= 1:
|
||||
b_right = half_border if p > 1 else border
|
||||
b_left = half_border if p < len(self.children) - 1 else border
|
||||
self.children[-p - 1].x = max(min(
|
||||
self.x + b_left + (touch.x - touch.ox),
|
||||
self.right - b_right),
|
||||
self.x + b_left)
|
||||
|
||||
# move previous page left edge up to left border
|
||||
if p > 1:
|
||||
self.children[-p].x = min(
|
||||
self.x + half_border * (touch.sx - touch.osx),
|
||||
self.x + half_border)
|
||||
|
||||
elif touch.ud['page'] == 'next':
|
||||
# move current page up to left edge
|
||||
if p >= 1:
|
||||
self.children[-p - 1].x = max(
|
||||
self.x + half_border * (1 - (touch.osx - touch.sx)),
|
||||
self.x)
|
||||
|
||||
# move next page until its edge hit the left border
|
||||
if p < len(self.children) - 1:
|
||||
b_right = half_border if p >= 1 else border
|
||||
b_left = half_border if p < len(self.children) - 2 else border
|
||||
self.children[-p - 2].x = min(max(
|
||||
self.right - b_right + (touch.x - touch.ox),
|
||||
self.x + b_left),
|
||||
self.right - b_right)
|
||||
|
||||
# move second next page up to right border
|
||||
if p < len(self.children) - 2:
|
||||
self.children[-p - 3].x = max(
|
||||
self.right + half_border * (touch.sx - touch.osx),
|
||||
self.right - half_border)
|
||||
|
||||
return page.on_touch_move(touch)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current == self:
|
||||
if (
|
||||
touch.ud['page'] == 'previous' and
|
||||
abs(touch.x - touch.ox) / self.width > self.swipe_threshold
|
||||
):
|
||||
self.page -= 1
|
||||
elif (
|
||||
touch.ud['page'] == 'next' and
|
||||
abs(touch.x - touch.ox) / self.width > self.swipe_threshold
|
||||
):
|
||||
self.page += 1
|
||||
else:
|
||||
self._trigger_layout()
|
||||
|
||||
touch.ungrab(self)
|
||||
|
||||
if len(self.children) > 1:
|
||||
return self.children[-self.page + 1].on_touch_up(touch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.button import Button
|
||||
|
||||
pl = PageLayout()
|
||||
for i in range(1, 4):
|
||||
b = Button(text='page%s' % i)
|
||||
pl.add_widget(b)
|
||||
|
||||
runTouchApp(pl)
|
||||
@@ -0,0 +1,266 @@
|
||||
'''
|
||||
Popup
|
||||
=====
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
.. image:: images/popup.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`Popup` widget is used to create modal popups. By default, the popup
|
||||
will cover the whole "parent" window. When you are creating a popup, you
|
||||
must at least set a :attr:`Popup.title` and :attr:`Popup.content`.
|
||||
|
||||
Remember that the default size of a Widget is size_hint=(1, 1). If you don't
|
||||
want your popup to be fullscreen, either use size hints with values less than 1
|
||||
(for instance size_hint=(.8, .8)) or deactivate the size_hint and use
|
||||
fixed size attributes.
|
||||
|
||||
|
||||
.. versionchanged:: 1.4.0
|
||||
The :class:`Popup` class now inherits from
|
||||
:class:`~kivy.uix.modalview.ModalView`. The :class:`Popup` offers a default
|
||||
layout with a title and a separation bar.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Example of a simple 400x400 Hello world popup::
|
||||
|
||||
popup = Popup(title='Test popup',
|
||||
content=Label(text='Hello world'),
|
||||
size_hint=(None, None), size=(400, 400))
|
||||
|
||||
By default, any click outside the popup will dismiss/close it. If you don't
|
||||
want that, you can set
|
||||
:attr:`~kivy.uix.modalview.ModalView.auto_dismiss` to False::
|
||||
|
||||
popup = Popup(title='Test popup', content=Label(text='Hello world'),
|
||||
auto_dismiss=False)
|
||||
popup.open()
|
||||
|
||||
To manually dismiss/close the popup, use
|
||||
:attr:`~kivy.uix.modalview.ModalView.dismiss`::
|
||||
|
||||
popup.dismiss()
|
||||
|
||||
Both :meth:`~kivy.uix.modalview.ModalView.open` and
|
||||
:meth:`~kivy.uix.modalview.ModalView.dismiss` are bindable. That means you
|
||||
can directly bind the function to an action, e.g. to a button's on_press::
|
||||
|
||||
# create content and add to the popup
|
||||
content = Button(text='Close me!')
|
||||
popup = Popup(content=content, auto_dismiss=False)
|
||||
|
||||
# bind the on_press event of the button to the dismiss function
|
||||
content.bind(on_press=popup.dismiss)
|
||||
|
||||
# open the popup
|
||||
popup.open()
|
||||
|
||||
Same thing in KV language only with :class:`Factory`:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:import Factory kivy.factory.Factory
|
||||
<MyPopup@Popup>:
|
||||
auto_dismiss: False
|
||||
Button:
|
||||
text: 'Close me!'
|
||||
on_release: root.dismiss()
|
||||
|
||||
Button:
|
||||
text: 'Open popup'
|
||||
on_release: Factory.MyPopup().open()
|
||||
|
||||
.. note::
|
||||
|
||||
Popup is a special widget. Don't try to add it as a child to any other
|
||||
widget. If you do, Popup will be handled like an ordinary widget and
|
||||
won't be created hidden in the background.
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
BoxLayout:
|
||||
MyPopup: # bad!
|
||||
|
||||
Popup Events
|
||||
------------
|
||||
|
||||
There are two events available: `on_open` which is raised when the popup is
|
||||
opening, and `on_dismiss` which is raised when the popup is closed.
|
||||
For `on_dismiss`, you can prevent the
|
||||
popup from closing by explicitly returning True from your callback::
|
||||
|
||||
def my_callback(instance):
|
||||
print('Popup', instance, 'is being dismissed but is prevented!')
|
||||
return True
|
||||
popup = Popup(content=Label(text='Hello world'))
|
||||
popup.bind(on_dismiss=my_callback)
|
||||
popup.open()
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Popup', 'PopupException')
|
||||
|
||||
from kivy.core.text import DEFAULT_FONT
|
||||
from kivy.uix.modalview import ModalView
|
||||
from kivy.properties import (StringProperty, ObjectProperty, OptionProperty,
|
||||
NumericProperty, ColorProperty)
|
||||
|
||||
|
||||
class PopupException(Exception):
|
||||
'''Popup exception, fired when multiple content widgets are added to the
|
||||
popup.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
'''
|
||||
|
||||
|
||||
class Popup(ModalView):
|
||||
'''Popup class. See module documentation for more information.
|
||||
|
||||
:Events:
|
||||
`on_open`:
|
||||
Fired when the Popup is opened.
|
||||
`on_dismiss`:
|
||||
Fired when the Popup is closed. If the callback returns True, the
|
||||
dismiss will be canceled.
|
||||
'''
|
||||
|
||||
title = StringProperty('No title')
|
||||
'''String that represents the title of the popup.
|
||||
|
||||
:attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to
|
||||
'No title'.
|
||||
'''
|
||||
|
||||
title_size = NumericProperty('14sp')
|
||||
'''Represents the font size of the popup title.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
:attr:`title_size` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to '14sp'.
|
||||
'''
|
||||
|
||||
title_align = OptionProperty(
|
||||
'left', options=['left', 'center', 'right', 'justify'])
|
||||
'''Horizontal alignment of the title.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`title_align` is a :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'left'. Available options are left, center, right and justify.
|
||||
'''
|
||||
|
||||
title_font = StringProperty(DEFAULT_FONT)
|
||||
'''Font used to render the title text.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`title_font` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'Roboto'. This value is taken
|
||||
from :class:`~kivy.config.Config`.
|
||||
'''
|
||||
|
||||
content = ObjectProperty(None)
|
||||
'''Content of the popup that is displayed just under the title.
|
||||
|
||||
:attr:`content` is an :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
title_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Color used by the Title.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`title_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
separator_color = ColorProperty([47 / 255., 167 / 255., 212 / 255., 1.])
|
||||
'''Color used by the separator between title and content.
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
:attr:`separator_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [47 / 255., 167 / 255., 212 / 255., 1.].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
separator_height = NumericProperty('2dp')
|
||||
'''Height of the separator.
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
:attr:`separator_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 2dp.
|
||||
'''
|
||||
|
||||
# Internal properties used for graphical representation.
|
||||
|
||||
_container = ObjectProperty(None)
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
if self._container:
|
||||
if self.content:
|
||||
raise PopupException(
|
||||
'Popup can have only one widget as content')
|
||||
self.content = widget
|
||||
else:
|
||||
super(Popup, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
def on_content(self, instance, value):
|
||||
if self._container:
|
||||
self._container.clear_widgets()
|
||||
self._container.add_widget(value)
|
||||
|
||||
def on__container(self, instance, value):
|
||||
if value is None or self.content is None:
|
||||
return
|
||||
self._container.clear_widgets()
|
||||
self._container.add_widget(self.content)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.disabled and self.collide_point(*touch.pos):
|
||||
return True
|
||||
return super(Popup, self).on_touch_down(touch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.core.window import Window
|
||||
|
||||
# add popup
|
||||
content = GridLayout(cols=1)
|
||||
content_cancel = Button(text='Cancel', size_hint_y=None, height=40)
|
||||
content.add_widget(Label(text='This is a hello world'))
|
||||
content.add_widget(content_cancel)
|
||||
popup = Popup(title='Test popup',
|
||||
size_hint=(None, None), size=(256, 256),
|
||||
content=content, disabled=True)
|
||||
content_cancel.bind(on_release=popup.dismiss)
|
||||
|
||||
layout = GridLayout(cols=3)
|
||||
for x in range(9):
|
||||
btn = Button(text=str(x))
|
||||
btn.bind(on_release=popup.open)
|
||||
layout.add_widget(btn)
|
||||
|
||||
Window.add_widget(layout)
|
||||
|
||||
popup.open()
|
||||
|
||||
runTouchApp()
|
||||
@@ -0,0 +1,95 @@
|
||||
'''
|
||||
Progress Bar
|
||||
============
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
.. image:: images/progressbar.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`ProgressBar` widget is used to visualize the progress of some task.
|
||||
Only the horizontal mode is currently supported: the vertical mode is not
|
||||
yet available.
|
||||
|
||||
The progress bar has no interactive elements and is a display-only widget.
|
||||
|
||||
To use it, simply assign a value to indicate the current progress::
|
||||
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
pb = ProgressBar(max=1000)
|
||||
|
||||
# this will update the graphics automatically (75% done)
|
||||
pb.value = 750
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ProgressBar', )
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.properties import NumericProperty, AliasProperty
|
||||
|
||||
|
||||
class ProgressBar(Widget):
|
||||
'''Class for creating a progress bar widget.
|
||||
|
||||
See module documentation for more details.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._value = 0.
|
||||
super(ProgressBar, self).__init__(**kwargs)
|
||||
|
||||
def _get_value(self):
|
||||
return self._value
|
||||
|
||||
def _set_value(self, value):
|
||||
value = max(0, min(self.max, value))
|
||||
if value != self._value:
|
||||
self._value = value
|
||||
return True
|
||||
|
||||
value = AliasProperty(_get_value, _set_value)
|
||||
'''Current value used for the slider.
|
||||
|
||||
:attr:`value` is an :class:`~kivy.properties.AliasProperty` that
|
||||
returns the value of the progress bar. If the value is < 0 or >
|
||||
:attr:`max`, it will be normalized to those boundaries.
|
||||
|
||||
.. versionchanged:: 1.6.0
|
||||
The value is now limited to between 0 and :attr:`max`.
|
||||
'''
|
||||
|
||||
def get_norm_value(self):
|
||||
d = self.max
|
||||
if d == 0:
|
||||
return 0
|
||||
return self.value / float(d)
|
||||
|
||||
def set_norm_value(self, value):
|
||||
self.value = value * self.max
|
||||
|
||||
value_normalized = AliasProperty(get_norm_value, set_norm_value,
|
||||
bind=('value', 'max'), cache=True)
|
||||
'''Normalized value inside the range 0-1::
|
||||
|
||||
>>> pb = ProgressBar(value=50, max=100)
|
||||
>>> pb.value
|
||||
50
|
||||
>>> pb.value_normalized
|
||||
0.5
|
||||
|
||||
:attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
max = NumericProperty(100.)
|
||||
'''Maximum value allowed for :attr:`value`.
|
||||
|
||||
:attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
100.
|
||||
'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
from kivy.base import runTouchApp
|
||||
runTouchApp(ProgressBar(value=50))
|
||||
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
RecycleBoxLayout
|
||||
================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
|
||||
The RecycleBoxLayout is designed to provide a
|
||||
:class:`~kivy.uix.boxlayout.BoxLayout` type layout when used with the
|
||||
:class:`~kivy.uix.recycleview.RecycleView` widget. Please refer to the
|
||||
:mod:`~kivy.uix.recycleview` module documentation for more information.
|
||||
|
||||
"""
|
||||
|
||||
from kivy.uix.recyclelayout import RecycleLayout
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
|
||||
__all__ = ('RecycleBoxLayout', )
|
||||
|
||||
|
||||
class RecycleBoxLayout(RecycleLayout, BoxLayout):
|
||||
|
||||
_rv_positions = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RecycleBoxLayout, self).__init__(**kwargs)
|
||||
self.funbind('children', self._trigger_layout)
|
||||
|
||||
def _update_sizes(self, changed):
|
||||
horizontal = self.orientation == 'horizontal'
|
||||
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
||||
padding_x = padding_left + padding_right
|
||||
padding_y = padding_top + padding_bottom
|
||||
selfw = self.width
|
||||
selfh = self.height
|
||||
layout_w = max(0, selfw - padding_x)
|
||||
layout_h = max(0, selfh - padding_y)
|
||||
cx = self.x + padding_left
|
||||
cy = self.y + padding_bottom
|
||||
view_opts = self.view_opts
|
||||
remove_view = self.remove_view
|
||||
|
||||
for (index, widget, (w, h), (wn, hn), (shw, shh), (shnw, shnh),
|
||||
(shw_min, shh_min), (shwn_min, shhn_min), (shw_max, shh_max),
|
||||
(shwn_max, shhn_max), ph, phn) in changed:
|
||||
if (horizontal and
|
||||
(shw != shnw or w != wn or shw_min != shwn_min or
|
||||
shw_max != shwn_max) or
|
||||
not horizontal and
|
||||
(shh != shnh or h != hn or shh_min != shhn_min or
|
||||
shh_max != shhn_max)):
|
||||
return True
|
||||
|
||||
remove_view(widget, index)
|
||||
opt = view_opts[index]
|
||||
if horizontal:
|
||||
wo, ho = opt['size']
|
||||
if shnh is not None:
|
||||
_, h = opt['size'] = [wo, shnh * layout_h]
|
||||
else:
|
||||
h = ho
|
||||
|
||||
xo, yo = opt['pos']
|
||||
for key, value in phn.items():
|
||||
posy = value * layout_h
|
||||
if key == 'y':
|
||||
yo = posy + cy
|
||||
elif key == 'top':
|
||||
yo = posy - h
|
||||
elif key == 'center_y':
|
||||
yo = posy - (h / 2.)
|
||||
opt['pos'] = [xo, yo]
|
||||
else:
|
||||
wo, ho = opt['size']
|
||||
if shnw is not None:
|
||||
w, _ = opt['size'] = [shnw * layout_w, ho]
|
||||
else:
|
||||
w = wo
|
||||
|
||||
xo, yo = opt['pos']
|
||||
for key, value in phn.items():
|
||||
posx = value * layout_w
|
||||
if key == 'x':
|
||||
xo = posx + cx
|
||||
elif key == 'right':
|
||||
xo = posx - w
|
||||
elif key == 'center_x':
|
||||
xo = posx - (w / 2.)
|
||||
opt['pos'] = [xo, yo]
|
||||
|
||||
return False
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
super(RecycleBoxLayout, self).compute_layout(data, flags)
|
||||
|
||||
changed = self._changed_views
|
||||
if (changed is None or
|
||||
changed and not self._update_sizes(changed)):
|
||||
return
|
||||
|
||||
self.clear_layout()
|
||||
self._rv_positions = None
|
||||
if not data:
|
||||
l, t, r, b = self.padding
|
||||
self.minimum_size = l + r, t + b
|
||||
return
|
||||
|
||||
view_opts = self.view_opts
|
||||
n = len(view_opts)
|
||||
for i, x, y, w, h in self._iterate_layout(
|
||||
[(opt['size'], opt['size_hint'], opt['pos_hint'],
|
||||
opt['size_hint_min'], opt['size_hint_max']) for
|
||||
opt in reversed(view_opts)]):
|
||||
opt = view_opts[n - i - 1]
|
||||
shw, shh = opt['size_hint']
|
||||
opt['pos'] = x, y
|
||||
wo, ho = opt['size']
|
||||
# layout won't/shouldn't change previous size if size_hint is None
|
||||
# which is what w/h being None means.
|
||||
opt['size'] = [(wo if shw is None else w),
|
||||
(ho if shh is None else h)]
|
||||
|
||||
spacing = self.spacing
|
||||
pos = self._rv_positions = [None, ] * len(data)
|
||||
|
||||
if self.orientation == 'horizontal':
|
||||
pos[0] = self.x
|
||||
last = pos[0] + self.padding[0] + view_opts[0]['size'][0] + \
|
||||
spacing / 2.
|
||||
for i, val in enumerate(view_opts[1:], 1):
|
||||
pos[i] = last
|
||||
last += val['size'][0] + spacing
|
||||
else:
|
||||
last = pos[-1] = \
|
||||
self.y + self.height - self.padding[1] - \
|
||||
view_opts[0]['size'][1] - spacing / 2.
|
||||
n = len(view_opts)
|
||||
for i, val in enumerate(view_opts[1:], 1):
|
||||
last -= spacing + val['size'][1]
|
||||
pos[n - 1 - i] = last
|
||||
|
||||
def get_view_index_at(self, pos):
|
||||
calc_pos = self._rv_positions
|
||||
if not calc_pos:
|
||||
return 0
|
||||
|
||||
x, y = pos
|
||||
|
||||
if self.orientation == 'horizontal':
|
||||
if x >= calc_pos[-1] or len(calc_pos) == 1:
|
||||
return len(calc_pos) - 1
|
||||
|
||||
ix = 0
|
||||
for val in calc_pos[1:]:
|
||||
if x < val:
|
||||
return ix
|
||||
ix += 1
|
||||
else:
|
||||
if y >= calc_pos[-1] or len(calc_pos) == 1:
|
||||
return 0
|
||||
|
||||
iy = 0
|
||||
for val in calc_pos[1:]:
|
||||
if y < val:
|
||||
return len(calc_pos) - iy - 1
|
||||
iy += 1
|
||||
|
||||
assert False
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
if self._rv_positions is None or not data:
|
||||
return []
|
||||
|
||||
x, y, w, h = viewport
|
||||
at_idx = self.get_view_index_at
|
||||
if self.orientation == 'horizontal':
|
||||
a, b = at_idx((x, y)), at_idx((x + w, y))
|
||||
else:
|
||||
a, b = at_idx((x, y + h)), at_idx((x, y))
|
||||
return list(range(a, b + 1))
|
||||
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
RecycleGridLayout
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
|
||||
The RecycleGridLayout is designed to provide a
|
||||
:class:`~kivy.uix.gridlayout.GridLayout` type layout when used with the
|
||||
:class:`~kivy.uix.recycleview.RecycleView` widget. Please refer to the
|
||||
:mod:`~kivy.uix.recycleview` module documentation for more information.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
chain_from_iterable = itertools.chain.from_iterable
|
||||
from kivy.uix.recyclelayout import RecycleLayout
|
||||
from kivy.uix.gridlayout import GridLayout, GridLayoutException, nmax, nmin
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = ('RecycleGridLayout', )
|
||||
|
||||
|
||||
class RecycleGridLayout(RecycleLayout, GridLayout):
|
||||
|
||||
_cols_pos = None
|
||||
_rows_pos = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RecycleGridLayout, self).__init__(**kwargs)
|
||||
self.funbind('children', self._trigger_layout)
|
||||
|
||||
def on_children(self, instance, value):
|
||||
pass
|
||||
|
||||
def _fill_rows_cols_sizes(self):
|
||||
cols, rows = self._cols, self._rows
|
||||
cols_sh, rows_sh = self._cols_sh, self._rows_sh
|
||||
cols_sh_min, rows_sh_min = self._cols_sh_min, self._rows_sh_min
|
||||
cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max
|
||||
self._cols_count = cols_count = [defaultdict(int) for _ in cols]
|
||||
self._rows_count = rows_count = [defaultdict(int) for _ in rows]
|
||||
|
||||
# calculate minimum size for each columns and rows
|
||||
idx_iter = self._create_idx_iter(len(cols), len(rows))
|
||||
has_bound_y = has_bound_x = False
|
||||
for opt, (col, row) in zip(self.view_opts, idx_iter):
|
||||
(shw, shh), (w, h) = opt['size_hint'], opt['size']
|
||||
shw_min, shh_min = opt['size_hint_min']
|
||||
shw_max, shh_max = opt['size_hint_max']
|
||||
|
||||
if shw is None:
|
||||
cols_count[col][w] += 1
|
||||
if shh is None:
|
||||
rows_count[row][h] += 1
|
||||
|
||||
# compute minimum size / maximum stretch needed
|
||||
if shw is None:
|
||||
cols[col] = nmax(cols[col], w)
|
||||
else:
|
||||
cols_sh[col] = nmax(cols_sh[col], shw)
|
||||
if shw_min is not None:
|
||||
has_bound_x = True
|
||||
cols_sh_min[col] = nmax(cols_sh_min[col], shw_min)
|
||||
if shw_max is not None:
|
||||
has_bound_x = True
|
||||
cols_sh_max[col] = nmin(cols_sh_max[col], shw_max)
|
||||
|
||||
if shh is None:
|
||||
rows[row] = nmax(rows[row], h)
|
||||
else:
|
||||
rows_sh[row] = nmax(rows_sh[row], shh)
|
||||
if shh_min is not None:
|
||||
has_bound_y = True
|
||||
rows_sh_min[row] = nmax(rows_sh_min[row], shh_min)
|
||||
if shh_max is not None:
|
||||
has_bound_y = True
|
||||
rows_sh_max[row] = nmin(rows_sh_max[row], shh_max)
|
||||
self._has_hint_bound_x = has_bound_x
|
||||
self._has_hint_bound_y = has_bound_y
|
||||
|
||||
def _update_rows_cols_sizes(self, changed):
|
||||
cols_count, rows_count = self._cols_count, self._rows_count
|
||||
cols, rows = self._cols, self._rows
|
||||
remove_view = self.remove_view
|
||||
n_cols = len(cols)
|
||||
n_rows = len(rows)
|
||||
orientation = self.orientation
|
||||
|
||||
# this can be further improved to reduce re-comp, but whatever...
|
||||
for index, widget, (w, h), (wn, hn), sh, shn, sh_min, shn_min, \
|
||||
sh_max, shn_max, _, _ in changed:
|
||||
if sh != shn or sh_min != shn_min or sh_max != shn_max:
|
||||
return True
|
||||
elif (sh[0] is not None and w != wn and
|
||||
(h == hn or sh[1] is not None) or
|
||||
sh[1] is not None and h != hn and
|
||||
(w == wn or sh[0] is not None)):
|
||||
remove_view(widget, index)
|
||||
else: # size hint is None, so check if it can be resized inplace
|
||||
col, row = self._calculate_idx_from_a_view_idx(
|
||||
n_cols, n_rows, index)
|
||||
if w != wn:
|
||||
col_w = cols[col]
|
||||
cols_count[col][w] -= 1
|
||||
cols_count[col][wn] += 1
|
||||
was_last_w = cols_count[col][w] <= 0
|
||||
if was_last_w and col_w == w or wn > col_w:
|
||||
return True
|
||||
if was_last_w:
|
||||
del cols_count[col][w]
|
||||
|
||||
if h != hn:
|
||||
row_h = rows[row]
|
||||
rows_count[row][h] -= 1
|
||||
rows_count[row][hn] += 1
|
||||
was_last_h = rows_count[row][h] <= 0
|
||||
if was_last_h and row_h == h or hn > row_h:
|
||||
return True
|
||||
if was_last_h:
|
||||
del rows_count[row][h]
|
||||
|
||||
return False
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
super(RecycleGridLayout, self).compute_layout(data, flags)
|
||||
|
||||
n = len(data)
|
||||
smax = self.get_max_widgets()
|
||||
if smax and n > smax:
|
||||
raise GridLayoutException(
|
||||
'Too many children ({}) in GridLayout. Increase rows/cols!'.
|
||||
format(n))
|
||||
|
||||
changed = self._changed_views
|
||||
if (changed is None or
|
||||
changed and not self._update_rows_cols_sizes(changed)):
|
||||
return
|
||||
|
||||
self.clear_layout()
|
||||
if not self._init_rows_cols_sizes(n):
|
||||
self._cols_pos = None
|
||||
l, t, r, b = self.padding
|
||||
self.minimum_size = l + r, t + b
|
||||
return
|
||||
self._fill_rows_cols_sizes()
|
||||
self._update_minimum_size()
|
||||
self._finalize_rows_cols_sizes()
|
||||
|
||||
view_opts = self.view_opts
|
||||
for widget, x, y, w, h in self._iterate_layout(n):
|
||||
opt = view_opts[n - widget - 1]
|
||||
shw, shh = opt['size_hint']
|
||||
opt['pos'] = x, y
|
||||
wo, ho = opt['size']
|
||||
# layout won't/shouldn't change previous size if size_hint is None
|
||||
# which is what w/h being None means.
|
||||
opt['size'] = [(wo if shw is None else w),
|
||||
(ho if shh is None else h)]
|
||||
|
||||
spacing_x, spacing_y = self.spacing
|
||||
cols, rows = self._cols, self._rows
|
||||
|
||||
cols_pos = self._cols_pos = [None, ] * len(cols)
|
||||
rows_pos = self._rows_pos = [None, ] * len(rows)
|
||||
|
||||
cols_pos[0] = self.x
|
||||
last = cols_pos[0] + self.padding[0] + cols[0] + spacing_x / 2.
|
||||
for i, val in enumerate(cols[1:], 1):
|
||||
cols_pos[i] = last
|
||||
last += val + spacing_x
|
||||
|
||||
last = rows_pos[-1] = \
|
||||
self.y + self.height - self.padding[1] - rows[0] - spacing_y / 2.
|
||||
n = len(rows)
|
||||
for i, val in enumerate(rows[1:], 1):
|
||||
last -= spacing_y + val
|
||||
rows_pos[n - 1 - i] = last
|
||||
|
||||
def get_view_index_at(self, pos):
|
||||
if self._cols_pos is None:
|
||||
return 0
|
||||
|
||||
x, y = pos
|
||||
col_pos = self._cols_pos
|
||||
row_pos = self._rows_pos
|
||||
cols, rows = self._cols, self._rows
|
||||
if not col_pos or not row_pos:
|
||||
return 0
|
||||
|
||||
if x >= col_pos[-1]:
|
||||
ix = len(cols) - 1
|
||||
else:
|
||||
ix = 0
|
||||
for val in col_pos[1:]:
|
||||
if x < val:
|
||||
break
|
||||
ix += 1
|
||||
|
||||
if y >= row_pos[-1]:
|
||||
iy = len(rows) - 1
|
||||
else:
|
||||
iy = 0
|
||||
for val in row_pos[1:]:
|
||||
if y < val:
|
||||
break
|
||||
iy += 1
|
||||
|
||||
if not self._fills_from_left_to_right:
|
||||
ix = len(cols) - ix - 1
|
||||
if self._fills_from_top_to_bottom:
|
||||
iy = len(rows) - iy - 1
|
||||
return (iy * len(cols) + ix) if self._fills_row_first else \
|
||||
(ix * len(rows) + iy)
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
if self._cols_pos is None:
|
||||
return []
|
||||
x, y, w, h = viewport
|
||||
right = x + w
|
||||
top = y + h
|
||||
at_idx = self.get_view_index_at
|
||||
tl, tr, bl, br = sorted((
|
||||
at_idx((x, y)),
|
||||
at_idx((right, y)),
|
||||
at_idx((x, top)),
|
||||
at_idx((right, top)),
|
||||
))
|
||||
|
||||
n = len(data)
|
||||
if len({tl, tr, bl, br}) < 4:
|
||||
# visible area is one row/column
|
||||
return range(min(n, tl), min(n, br + 1))
|
||||
indices = []
|
||||
stride = len(self._cols) if self._fills_row_first else len(self._rows)
|
||||
if stride:
|
||||
x_slice = br - bl + 1
|
||||
indices = chain_from_iterable(
|
||||
range(min(s, n), min(n, s + x_slice))
|
||||
for s in range(tl, bl + 1, stride))
|
||||
return indices
|
||||
|
||||
def _calculate_idx_from_a_view_idx(self, n_cols, n_rows, view_idx):
|
||||
'''returns a tuple of (column-index, row-index) from a view-index'''
|
||||
if self._fills_row_first:
|
||||
row_idx, col_idx = divmod(view_idx, n_cols)
|
||||
else:
|
||||
col_idx, row_idx = divmod(view_idx, n_rows)
|
||||
if not self._fills_from_left_to_right:
|
||||
col_idx = n_cols - col_idx - 1
|
||||
if not self._fills_from_top_to_bottom:
|
||||
row_idx = n_rows - row_idx - 1
|
||||
return (col_idx, row_idx, )
|
||||
@@ -0,0 +1,446 @@
|
||||
"""
|
||||
RecycleLayout
|
||||
=============
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
"""
|
||||
|
||||
from kivy.uix.recycleview.layout import RecycleLayoutManagerBehavior
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import (
|
||||
ObjectProperty, StringProperty, ReferenceListProperty, NumericProperty
|
||||
)
|
||||
from kivy.factory import Factory
|
||||
|
||||
__all__ = ('RecycleLayout', )
|
||||
|
||||
|
||||
class RecycleLayout(RecycleLayoutManagerBehavior, Layout):
|
||||
"""
|
||||
RecycleLayout provides the default layout for RecycleViews.
|
||||
"""
|
||||
|
||||
default_width = NumericProperty(100, allownone=True)
|
||||
'''Default width for items
|
||||
|
||||
:attr:`default_width` is a NumericProperty and default to 100
|
||||
'''
|
||||
default_height = NumericProperty(100, allownone=True)
|
||||
'''Default height for items
|
||||
|
||||
:attr:`default_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
default to 100.
|
||||
'''
|
||||
default_size = ReferenceListProperty(default_width, default_height)
|
||||
'''size (width, height). Each value can be None.
|
||||
|
||||
:attr:`default_size` is an :class:`~kivy.properties.ReferenceListProperty`
|
||||
to [:attr:`default_width`, :attr:`default_height`].
|
||||
'''
|
||||
default_size_hint_x = NumericProperty(None, allownone=True)
|
||||
'''Default size_hint_x for items
|
||||
|
||||
:attr:`default_size_hint_x` is a :class:`~kivy.properties.NumericProperty`
|
||||
and default to None.
|
||||
'''
|
||||
default_size_hint_y = NumericProperty(None, allownone=True)
|
||||
'''Default size_hint_y for items
|
||||
|
||||
:attr:`default_size_hint_y` is a :class:`~kivy.properties.NumericProperty`
|
||||
and default to None.
|
||||
'''
|
||||
default_size_hint = ReferenceListProperty(
|
||||
default_size_hint_x, default_size_hint_y
|
||||
)
|
||||
'''size (width, height). Each value can be None.
|
||||
|
||||
:attr:`default_size_hint` is an
|
||||
:class:`~kivy.properties.ReferenceListProperty` to
|
||||
[:attr:`default_size_hint_x`, :attr:`default_size_hint_y`].
|
||||
'''
|
||||
|
||||
key_size = StringProperty(None, allownone=True)
|
||||
'''If set, which key in the dict should be used to set the size property of
|
||||
the item.
|
||||
|
||||
:attr:`key_size` is a :class:`~kivy.properties.StringProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
key_size_hint = StringProperty(None, allownone=True)
|
||||
'''If set, which key in the dict should be used to set the size_hint
|
||||
property of the item.
|
||||
|
||||
:attr:`key_size_hint` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
key_size_hint_min = StringProperty(None, allownone=True)
|
||||
'''If set, which key in the dict should be used to set the size_hint_min
|
||||
property of the item.
|
||||
|
||||
:attr:`key_size_hint_min` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
default_size_hint_x_min = NumericProperty(None, allownone=True)
|
||||
'''Default value for size_hint_x_min of items
|
||||
|
||||
:attr:`default_pos_hint_x_min` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to None.
|
||||
'''
|
||||
default_size_hint_y_min = NumericProperty(None, allownone=True)
|
||||
'''Default value for size_hint_y_min of items
|
||||
|
||||
:attr:`default_pos_hint_y_min` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to None.
|
||||
'''
|
||||
default_size_hint_min = ReferenceListProperty(
|
||||
default_size_hint_x_min,
|
||||
default_size_hint_y_min
|
||||
)
|
||||
'''Default value for size_hint_min of items
|
||||
|
||||
:attr:`default_size_min` is a
|
||||
:class:`~kivy.properties.ReferenceListProperty` to
|
||||
[:attr:`default_size_hint_x_min`, :attr:`default_size_hint_y_min`].
|
||||
'''
|
||||
|
||||
key_size_hint_max = StringProperty(None, allownone=True)
|
||||
'''If set, which key in the dict should be used to set the size_hint_max
|
||||
property of the item.
|
||||
|
||||
:attr:`key_size_hint_max` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
default_size_hint_x_max = NumericProperty(None, allownone=True)
|
||||
'''Default value for size_hint_x_max of items
|
||||
|
||||
:attr:`default_pos_hint_x_max` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to None.
|
||||
'''
|
||||
default_size_hint_y_max = NumericProperty(None, allownone=True)
|
||||
'''Default value for size_hint_y_max of items
|
||||
|
||||
:attr:`default_pos_hint_y_max` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to None.
|
||||
'''
|
||||
default_size_hint_max = ReferenceListProperty(
|
||||
default_size_hint_x_max,
|
||||
default_size_hint_y_max
|
||||
)
|
||||
'''Default value for size_hint_max of items
|
||||
|
||||
:attr:`default_size_max` is a
|
||||
:class:`~kivy.properties.ReferenceListProperty` to
|
||||
[:attr:`default_size_hint_x_max`, :attr:`default_size_hint_y_max`].
|
||||
'''
|
||||
|
||||
default_pos_hint = ObjectProperty({})
|
||||
'''Default pos_hint value for items
|
||||
|
||||
:attr:`default_pos_hint` is a :class:`~kivy.properties.DictProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
key_pos_hint = StringProperty(None, allownone=True)
|
||||
'''If set, which key in the dict should be used to set the pos_hint of
|
||||
items.
|
||||
|
||||
:attr:`key_pos_hint` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
initial_width = NumericProperty(100)
|
||||
'''Initial width for the items.
|
||||
|
||||
:attr:`initial_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
initial_height = NumericProperty(100)
|
||||
'''Initial height for the items.
|
||||
|
||||
:attr:`initial_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
initial_size = ReferenceListProperty(initial_width, initial_height)
|
||||
'''Initial size of items
|
||||
|
||||
:attr:`initial_size` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
to [:attr:`initial_width`, :attr:`initial_height`].
|
||||
'''
|
||||
|
||||
view_opts = []
|
||||
|
||||
_size_needs_update = False
|
||||
_changed_views = []
|
||||
|
||||
view_indices = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.view_indices = {}
|
||||
self._updated_views = []
|
||||
self._trigger_layout = self._catch_layout_trigger
|
||||
super(RecycleLayout, self).__init__(**kwargs)
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
super(RecycleLayout, self).attach_recycleview(rv)
|
||||
if rv:
|
||||
fbind = self.fbind
|
||||
fbind('default_size', rv.refresh_from_data)
|
||||
fbind('key_size', rv.refresh_from_data)
|
||||
fbind('default_size_hint', rv.refresh_from_data)
|
||||
fbind('key_size_hint', rv.refresh_from_data)
|
||||
fbind('default_size_hint_min', rv.refresh_from_data)
|
||||
fbind('key_size_hint_min', rv.refresh_from_data)
|
||||
fbind('default_size_hint_max', rv.refresh_from_data)
|
||||
fbind('key_size_hint_max', rv.refresh_from_data)
|
||||
fbind('default_pos_hint', rv.refresh_from_data)
|
||||
fbind('key_pos_hint', rv.refresh_from_data)
|
||||
|
||||
def detach_recycleview(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
funbind = self.funbind
|
||||
funbind('default_size', rv.refresh_from_data)
|
||||
funbind('key_size', rv.refresh_from_data)
|
||||
funbind('default_size_hint', rv.refresh_from_data)
|
||||
funbind('key_size_hint', rv.refresh_from_data)
|
||||
funbind('default_size_hint_min', rv.refresh_from_data)
|
||||
funbind('key_size_hint_min', rv.refresh_from_data)
|
||||
funbind('default_size_hint_max', rv.refresh_from_data)
|
||||
funbind('key_size_hint_max', rv.refresh_from_data)
|
||||
funbind('default_pos_hint', rv.refresh_from_data)
|
||||
funbind('key_pos_hint', rv.refresh_from_data)
|
||||
super(RecycleLayout, self).detach_recycleview()
|
||||
|
||||
def _catch_layout_trigger(self, instance=None, value=None):
|
||||
rv = self.recycleview
|
||||
if rv is None:
|
||||
return
|
||||
|
||||
idx = self.view_indices.get(instance)
|
||||
if idx is not None:
|
||||
if self._size_needs_update:
|
||||
return
|
||||
opt = self.view_opts[idx]
|
||||
if (instance.size == opt['size'] and
|
||||
instance.size_hint == opt['size_hint'] and
|
||||
instance.size_hint_min == opt['size_hint_min'] and
|
||||
instance.size_hint_max == opt['size_hint_max'] and
|
||||
instance.pos_hint == opt['pos_hint']):
|
||||
return
|
||||
self._size_needs_update = True
|
||||
rv.refresh_from_layout(view_size=True)
|
||||
else:
|
||||
rv.refresh_from_layout()
|
||||
|
||||
def compute_sizes_from_data(self, data, flags):
|
||||
if [f for f in flags if not f]:
|
||||
# at least one changed data unpredictably
|
||||
self.clear_layout()
|
||||
opts = self.view_opts = [None for _ in data]
|
||||
else:
|
||||
opts = self.view_opts
|
||||
changed = False
|
||||
for flag in flags:
|
||||
for k, v in flag.items():
|
||||
changed = True
|
||||
if k == 'removed':
|
||||
del opts[v]
|
||||
elif k == 'appended':
|
||||
opts.extend([None, ] * (v.stop - v.start))
|
||||
elif k == 'inserted':
|
||||
opts.insert(v, None)
|
||||
elif k == 'modified':
|
||||
start, stop, step = v.start, v.stop, v.step
|
||||
r = range(start, stop) if step is None else \
|
||||
range(start, stop, step)
|
||||
for i in r:
|
||||
opts[i] = None
|
||||
else:
|
||||
raise Exception('Unrecognized data flag {}'.format(k))
|
||||
|
||||
if changed:
|
||||
self.clear_layout()
|
||||
|
||||
assert len(data) == len(opts)
|
||||
ph_key = self.key_pos_hint
|
||||
ph_def = self.default_pos_hint
|
||||
sh_key = self.key_size_hint
|
||||
sh_def = self.default_size_hint
|
||||
sh_min_key = self.key_size_hint_min
|
||||
sh_min_def = self.default_size_hint_min
|
||||
sh_max_key = self.key_size_hint_max
|
||||
sh_max_def = self.default_size_hint_max
|
||||
s_key = self.key_size
|
||||
s_def = self.default_size
|
||||
viewcls_def = self.viewclass
|
||||
viewcls_key = self.key_viewclass
|
||||
iw, ih = self.initial_size
|
||||
|
||||
sh = []
|
||||
for i, item in enumerate(data):
|
||||
if opts[i] is not None:
|
||||
continue
|
||||
|
||||
ph = ph_def if ph_key is None else item.get(ph_key, ph_def)
|
||||
ph = item.get('pos_hint', ph)
|
||||
|
||||
sh = sh_def if sh_key is None else item.get(sh_key, sh_def)
|
||||
sh = item.get('size_hint', sh)
|
||||
sh = [item.get('size_hint_x', sh[0]),
|
||||
item.get('size_hint_y', sh[1])]
|
||||
|
||||
sh_min = sh_min_def if sh_min_key is None else item.get(sh_min_key,
|
||||
sh_min_def)
|
||||
sh_min = item.get('size_hint_min', sh_min)
|
||||
sh_min = [item.get('size_hint_min_x', sh_min[0]),
|
||||
item.get('size_hint_min_y', sh_min[1])]
|
||||
|
||||
sh_max = sh_max_def if sh_max_key is None else item.get(sh_max_key,
|
||||
sh_max_def)
|
||||
sh_max = item.get('size_hint_max', sh_max)
|
||||
sh_max = [item.get('size_hint_max_x', sh_max[0]),
|
||||
item.get('size_hint_max_y', sh_max[1])]
|
||||
|
||||
s = s_def if s_key is None else item.get(s_key, s_def)
|
||||
s = item.get('size', s)
|
||||
w, h = s = item.get('width', s[0]), item.get('height', s[1])
|
||||
|
||||
viewcls = None
|
||||
if viewcls_key is not None:
|
||||
viewcls = item.get(viewcls_key)
|
||||
if viewcls is not None:
|
||||
viewcls = getattr(Factory, viewcls)
|
||||
if viewcls is None:
|
||||
viewcls = viewcls_def
|
||||
|
||||
opts[i] = {
|
||||
'size': [(iw if w is None else w), (ih if h is None else h)],
|
||||
'size_hint': sh, 'size_hint_min': sh_min,
|
||||
'size_hint_max': sh_max, 'pos': None, 'pos_hint': ph,
|
||||
'viewclass': viewcls, 'width_none': w is None,
|
||||
'height_none': h is None}
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
self._size_needs_update = False
|
||||
|
||||
opts = self.view_opts
|
||||
changed = []
|
||||
for widget, index in self.view_indices.items():
|
||||
opt = opts[index]
|
||||
s = opt['size']
|
||||
w, h = sn = list(widget.size)
|
||||
sh = opt['size_hint']
|
||||
shnw, shnh = shn = list(widget.size_hint)
|
||||
sh_min = opt['size_hint_min']
|
||||
shn_min = list(widget.size_hint_min)
|
||||
sh_max = opt['size_hint_max']
|
||||
shn_max = list(widget.size_hint_max)
|
||||
ph = opt['pos_hint']
|
||||
phn = dict(widget.pos_hint)
|
||||
if s != sn or sh != shn or ph != phn or sh_min != shn_min or \
|
||||
sh_max != shn_max:
|
||||
changed.append((index, widget, s, sn, sh, shn, sh_min, shn_min,
|
||||
sh_max, shn_max, ph, phn))
|
||||
if shnw is None:
|
||||
if shnh is None:
|
||||
opt['size'] = sn
|
||||
else:
|
||||
opt['size'] = [w, s[1]]
|
||||
elif shnh is None:
|
||||
opt['size'] = [s[0], h]
|
||||
opt['size_hint'] = shn
|
||||
opt['size_hint_min'] = shn_min
|
||||
opt['size_hint_max'] = shn_max
|
||||
opt['pos_hint'] = phn
|
||||
|
||||
if [f for f in flags if not f]: # need to redo everything
|
||||
self._changed_views = []
|
||||
else:
|
||||
self._changed_views = changed if changed else None
|
||||
|
||||
def do_layout(self, *largs):
|
||||
assert False
|
||||
|
||||
def set_visible_views(self, indices, data, viewport):
|
||||
view_opts = self.view_opts
|
||||
new, remaining, old = self.recycleview.view_adapter.set_visible_views(
|
||||
indices, data, view_opts)
|
||||
|
||||
remove = self.remove_widget
|
||||
view_indices = self.view_indices
|
||||
for _, widget in old:
|
||||
remove(widget)
|
||||
del view_indices[widget]
|
||||
|
||||
# first update the sizing info so that when we update the size
|
||||
# the widgets are not bound and won't trigger a re-layout
|
||||
refresh_view_layout = self.refresh_view_layout
|
||||
for index, widget in new:
|
||||
# make sure widget is added first so that any sizing updates
|
||||
# will be recorded
|
||||
opt = view_opts[index].copy()
|
||||
del opt['width_none']
|
||||
del opt['height_none']
|
||||
refresh_view_layout(index, opt, widget, viewport)
|
||||
|
||||
# then add all the visible widgets, which binds size/size_hint
|
||||
add = self.add_widget
|
||||
for index, widget in new:
|
||||
# add to the container if it's not already done
|
||||
view_indices[widget] = index
|
||||
if widget.parent is None:
|
||||
add(widget)
|
||||
|
||||
# finally, make sure if the size has changed to cause a re-layout
|
||||
changed = False
|
||||
for index, widget in new:
|
||||
opt = view_opts[index]
|
||||
if (changed or widget.size == opt['size'] and
|
||||
widget.size_hint == opt['size_hint'] and
|
||||
widget.size_hint_min == opt['size_hint_min'] and
|
||||
widget.size_hint_max == opt['size_hint_max'] and
|
||||
widget.pos_hint == opt['pos_hint']):
|
||||
continue
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
# we could use LayoutChangeException here, but refresh_views in rv
|
||||
# needs to be updated to watch for it in the layout phase
|
||||
self._size_needs_update = True
|
||||
self.recycleview.refresh_from_layout(view_size=True)
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
opt = self.view_opts[index].copy()
|
||||
width_none = opt.pop('width_none')
|
||||
height_none = opt.pop('height_none')
|
||||
opt.update(layout)
|
||||
|
||||
w, h = opt['size']
|
||||
shw, shh = opt['size_hint']
|
||||
if shw is None and width_none:
|
||||
w = None
|
||||
if shh is None and height_none:
|
||||
h = None
|
||||
opt['size'] = w, h
|
||||
super(RecycleLayout, self).refresh_view_layout(
|
||||
index, opt, view, viewport)
|
||||
|
||||
def remove_views(self):
|
||||
super(RecycleLayout, self).remove_views()
|
||||
self.clear_widgets()
|
||||
self.view_indices = {}
|
||||
|
||||
def remove_view(self, view, index):
|
||||
super(RecycleLayout, self).remove_view(view, index)
|
||||
self.remove_widget(view)
|
||||
del self.view_indices[view]
|
||||
|
||||
def clear_layout(self):
|
||||
super(RecycleLayout, self).clear_layout()
|
||||
self.clear_widgets()
|
||||
self.view_indices = {}
|
||||
self._size_needs_update = False
|
||||
@@ -0,0 +1,624 @@
|
||||
"""
|
||||
RecycleView
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The RecycleView provides a flexible model for viewing selected sections of
|
||||
large data sets. It aims to prevent the performance degradation that can occur
|
||||
when generating large numbers of widgets in order to display many data items.
|
||||
|
||||
.. warning::
|
||||
|
||||
Because :class:`RecycleView` reuses widgets, any state change to a single
|
||||
widget will stay with that widget as it's reused, even if the
|
||||
:attr:`~RecycleView.data` assigned to it by the :class:`RecycleView`
|
||||
changes, unless the complete state is tracked in :attr:`~RecycleView.data`
|
||||
(see below).
|
||||
|
||||
The view is generated by processing the :attr:`~RecycleView.data`, essentially
|
||||
a list of dicts, and uses these dicts to generate instances of the
|
||||
:attr:`~RecycleView.viewclass` as required. Its design is based on the
|
||||
MVC (`Model-View-Controller
|
||||
<https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller>`_)
|
||||
pattern.
|
||||
|
||||
* Model: The model is formed by :attr:`~RecycleView.data` you pass in via a
|
||||
list of dicts.
|
||||
* View: The View is split across layout and views and implemented using
|
||||
adapters.
|
||||
* Controller: The controller determines the logical interaction and is
|
||||
implemented by :class:`RecycleViewBehavior`.
|
||||
|
||||
These are abstract classes and cannot be used directly. The default concrete
|
||||
implementations are the
|
||||
:class:`~kivy.uix.recycleview.datamodel.RecycleDataModel` for the model, the
|
||||
:class:`~kivy.uix.recyclelayout.RecycleLayout` for the view, and the
|
||||
:class:`RecycleView` for the controller.
|
||||
|
||||
When a RecycleView is instantiated, it automatically creates the views and data
|
||||
classes. However, one must manually create the layout classes and add them to
|
||||
the RecycleView.
|
||||
|
||||
A layout manager is automatically created as a
|
||||
:attr:`~RecycleViewBehavior.layout_manager` when added as the child of the
|
||||
RecycleView. Similarly when removed. A requirement is that the layout manager
|
||||
must be contained as a child somewhere within the RecycleView's widget tree so
|
||||
the view port can be found.
|
||||
|
||||
A minimal example might look something like this::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
|
||||
|
||||
Builder.load_string('''
|
||||
<RV>:
|
||||
viewclass: 'Label'
|
||||
RecycleBoxLayout:
|
||||
default_size: None, dp(56)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
''')
|
||||
|
||||
class RV(RecycleView):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x)} for x in range(100)]
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return RV()
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestApp().run()
|
||||
|
||||
In order to support selection in the view, you can add the required behaviors
|
||||
as follows::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||
|
||||
Builder.load_string('''
|
||||
<SelectableLabel>:
|
||||
# Draw a background to indicate selection
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
<RV>:
|
||||
viewclass: 'SelectableLabel'
|
||||
SelectableRecycleBoxLayout:
|
||||
default_size: None, dp(56)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
multiselect: True
|
||||
touch_multiselect: True
|
||||
''')
|
||||
|
||||
|
||||
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
|
||||
RecycleBoxLayout):
|
||||
''' Adds selection and focus behavior to the view. '''
|
||||
|
||||
|
||||
class SelectableLabel(RecycleDataViewBehavior, Label):
|
||||
''' Add selection support to the Label '''
|
||||
index = None
|
||||
selected = BooleanProperty(False)
|
||||
selectable = BooleanProperty(True)
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
''' Catch and handle the view changes '''
|
||||
self.index = index
|
||||
return super(SelectableLabel, self).refresh_view_attrs(
|
||||
rv, index, data)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
''' Add selection on touch down '''
|
||||
if super(SelectableLabel, self).on_touch_down(touch):
|
||||
return True
|
||||
if self.collide_point(*touch.pos) and self.selectable:
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
''' Respond to the selection of items in the view. '''
|
||||
self.selected = is_selected
|
||||
if is_selected:
|
||||
print("selection changed to {0}".format(rv.data[index]))
|
||||
else:
|
||||
print("selection removed for {0}".format(rv.data[index]))
|
||||
|
||||
|
||||
class RV(RecycleView):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x)} for x in range(100)]
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return RV()
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestApp().run()
|
||||
|
||||
|
||||
|
||||
Please see the `examples/widgets/recycleview/basic_data.py` file for a more
|
||||
complete example.
|
||||
|
||||
Viewclass State
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Because the viewclass widgets are reused or instantiated as needed by the
|
||||
:class:`RecycleView`, the order and content of the widgets are mutable. So any
|
||||
state change to a single widget will stay with that widget, even when the data
|
||||
assigned to it from the :attr:`~RecycleView.data` dict changes, unless
|
||||
:attr:`~RecycleView.data` tracks those changes or they are manually refreshed
|
||||
when re-used.
|
||||
|
||||
There are two methods for managing state changes in viewclass widgets:
|
||||
|
||||
1. Store state in the RecycleView.data Model
|
||||
2. Generate state changes on-the-fly by catching :attr:`~RecycleView.data`
|
||||
updates and manually refreshing.
|
||||
|
||||
An example::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.properties import BooleanProperty, StringProperty
|
||||
|
||||
Builder.load_string('''
|
||||
<StatefulLabel>:
|
||||
active: stored_state.active
|
||||
CheckBox:
|
||||
id: stored_state
|
||||
active: root.active
|
||||
on_release: root.store_checkbox_state()
|
||||
Label:
|
||||
text: root.text
|
||||
Label:
|
||||
id: generate_state
|
||||
text: root.generated_state_text
|
||||
|
||||
<RV>:
|
||||
viewclass: 'StatefulLabel'
|
||||
RecycleBoxLayout:
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
''')
|
||||
|
||||
class StatefulLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
text = StringProperty()
|
||||
generated_state_text = StringProperty()
|
||||
active = BooleanProperty()
|
||||
index = 0
|
||||
|
||||
'''
|
||||
To change a viewclass' state as the data assigned to it changes,
|
||||
overload the refresh_view_attrs function (inherited from
|
||||
RecycleDataViewBehavior)
|
||||
'''
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
self.index = index
|
||||
if data['text'] == '0':
|
||||
self.generated_state_text = "is zero"
|
||||
elif int(data['text']) % 2 == 1:
|
||||
self.generated_state_text = "is odd"
|
||||
else:
|
||||
self.generated_state_text = "is even"
|
||||
super(StatefulLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
'''
|
||||
To keep state changes in the viewclass with associated data,
|
||||
they can be explicitly stored in the RecycleView's data object
|
||||
'''
|
||||
def store_checkbox_state(self):
|
||||
rv = App.get_running_app().rv
|
||||
rv.data[self.index]['active'] = self.active
|
||||
|
||||
class RV(RecycleView, App):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x), 'active': False} for x in range(10)]
|
||||
App.get_running_app().rv = self
|
||||
|
||||
def build(self):
|
||||
return self
|
||||
|
||||
if __name__ == '__main__':
|
||||
RV().run()
|
||||
|
||||
TODO:
|
||||
- Method to clear cached class instances.
|
||||
- Test when views cannot be found (e.g. viewclass is None).
|
||||
- Fix selection goto.
|
||||
|
||||
.. warning::
|
||||
When views are re-used they may not trigger if the data remains the same.
|
||||
"""
|
||||
|
||||
__all__ = ('RecycleViewBehavior', 'RecycleView')
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.properties import AliasProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
from kivy.uix.recycleview.layout import RecycleLayoutManagerBehavior, \
|
||||
LayoutChangeException
|
||||
from kivy.uix.recycleview.views import RecycleDataAdapter
|
||||
from kivy.uix.recycleview.datamodel import RecycleDataModelBehavior, \
|
||||
RecycleDataModel
|
||||
|
||||
|
||||
class RecycleViewBehavior(object):
|
||||
"""RecycleViewBehavior provides a behavioral model upon which the
|
||||
:class:`RecycleView` is built. Together, they offer an extensible and
|
||||
flexible way to produce views with limited windows over large data sets.
|
||||
|
||||
See the module documentation for more information.
|
||||
"""
|
||||
|
||||
# internals
|
||||
_view_adapter = None
|
||||
_data_model = None
|
||||
_layout_manager = None
|
||||
|
||||
_refresh_flags = {'data': [], 'layout': [], 'viewport': False}
|
||||
_refresh_trigger = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._refresh_trigger = Clock.create_trigger(self.refresh_views, -1)
|
||||
self._refresh_flags = deepcopy(self._refresh_flags)
|
||||
super(RecycleViewBehavior, self).__init__(**kwargs)
|
||||
|
||||
def get_viewport(self):
|
||||
pass
|
||||
|
||||
def save_viewport(self):
|
||||
pass
|
||||
|
||||
def restore_viewport(self):
|
||||
pass
|
||||
|
||||
def refresh_views(self, *largs):
|
||||
lm = self.layout_manager
|
||||
flags = self._refresh_flags
|
||||
if lm is None or self.view_adapter is None or self.data_model is None:
|
||||
return
|
||||
|
||||
data = self.data
|
||||
f = flags['data']
|
||||
if f:
|
||||
self.save_viewport()
|
||||
# lm.clear_layout()
|
||||
flags['data'] = []
|
||||
flags['layout'] = [{}]
|
||||
lm.compute_sizes_from_data(data, f)
|
||||
|
||||
while flags['layout']:
|
||||
# if `data` we were re-triggered so finish in the next call.
|
||||
# Otherwise go until fully laid out.
|
||||
self.save_viewport()
|
||||
if flags['data']:
|
||||
return
|
||||
flags['viewport'] = True
|
||||
f = flags['layout']
|
||||
flags['layout'] = []
|
||||
|
||||
try:
|
||||
lm.compute_layout(data, f)
|
||||
except LayoutChangeException:
|
||||
flags['layout'].append({})
|
||||
continue
|
||||
|
||||
if flags['data']: # in case that happened meanwhile
|
||||
return
|
||||
|
||||
# make sure if we were re-triggered in the loop that we won't be
|
||||
# called needlessly later.
|
||||
self._refresh_trigger.cancel()
|
||||
|
||||
self.restore_viewport()
|
||||
|
||||
if flags['viewport']:
|
||||
# TODO: make this also listen to LayoutChangeException
|
||||
flags['viewport'] = False
|
||||
viewport = self.get_viewport()
|
||||
indices = lm.compute_visible_views(data, viewport)
|
||||
lm.set_visible_views(indices, data, viewport)
|
||||
|
||||
def refresh_from_data(self, *largs, **kwargs):
|
||||
"""
|
||||
This should be called when data changes. Data changes typically
|
||||
indicate that everything should be recomputed since the source data
|
||||
changed.
|
||||
|
||||
This method is automatically bound to the
|
||||
:attr:`~RecycleDataModelBehavior.on_data_changed` method of the
|
||||
:class:`~RecycleDataModelBehavior` class and
|
||||
therefore responds to and accepts the keyword arguments of that event.
|
||||
|
||||
It can be called manually to trigger an update.
|
||||
"""
|
||||
self._refresh_flags['data'].append(kwargs)
|
||||
self._refresh_trigger()
|
||||
|
||||
def refresh_from_layout(self, *largs, **kwargs):
|
||||
"""
|
||||
This should be called when the layout changes or needs to change. It is
|
||||
typically called when a layout parameter has changed and therefore the
|
||||
layout needs to be recomputed.
|
||||
"""
|
||||
self._refresh_flags['layout'].append(kwargs)
|
||||
self._refresh_trigger()
|
||||
|
||||
def refresh_from_viewport(self, *largs):
|
||||
"""
|
||||
This should be called when the viewport changes and the displayed data
|
||||
must be updated. Neither the data nor the layout will be recomputed.
|
||||
"""
|
||||
self._refresh_flags['viewport'] = True
|
||||
self._refresh_trigger()
|
||||
|
||||
def _dispatch_prop_on_source(self, prop_name, *largs):
|
||||
# Dispatches the prop of this class when the
|
||||
# view_adapter/layout_manager property changes.
|
||||
getattr(self.__class__, prop_name).dispatch(self)
|
||||
|
||||
def _get_data_model(self):
|
||||
return self._data_model
|
||||
|
||||
def _set_data_model(self, value):
|
||||
data_model = self._data_model
|
||||
if value is data_model:
|
||||
return
|
||||
if data_model is not None:
|
||||
self._data_model = None
|
||||
data_model.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleDataModelBehavior):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleDataModelBehavior, got {}'.
|
||||
format(value.__class__))
|
||||
|
||||
self._data_model = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_data()
|
||||
return True
|
||||
|
||||
data_model = AliasProperty(_get_data_model, _set_data_model)
|
||||
"""
|
||||
The Data model responsible for maintaining the data set.
|
||||
|
||||
data_model is an :class:`~kivy.properties.AliasProperty` that gets and sets
|
||||
the current data model.
|
||||
"""
|
||||
|
||||
def _get_view_adapter(self):
|
||||
return self._view_adapter
|
||||
|
||||
def _set_view_adapter(self, value):
|
||||
view_adapter = self._view_adapter
|
||||
if value is view_adapter:
|
||||
return
|
||||
if view_adapter is not None:
|
||||
self._view_adapter = None
|
||||
view_adapter.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleDataAdapter):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleAdapter, got {}'.
|
||||
format(value.__class__))
|
||||
|
||||
self._view_adapter = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_layout()
|
||||
return True
|
||||
|
||||
view_adapter = AliasProperty(_get_view_adapter, _set_view_adapter)
|
||||
"""
|
||||
The adapter responsible for providing views that represent items in a data
|
||||
set.
|
||||
|
||||
view_adapter is an :class:`~kivy.properties.AliasProperty` that gets and
|
||||
sets the current view adapter.
|
||||
"""
|
||||
|
||||
def _get_layout_manager(self):
|
||||
return self._layout_manager
|
||||
|
||||
def _set_layout_manager(self, value):
|
||||
lm = self._layout_manager
|
||||
if value is lm:
|
||||
return
|
||||
|
||||
if lm is not None:
|
||||
self._layout_manager = None
|
||||
lm.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleLayoutManagerBehavior):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleLayoutManagerBehavior, '
|
||||
'got {}'.format(value.__class__))
|
||||
|
||||
self._layout_manager = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_layout()
|
||||
return True
|
||||
|
||||
layout_manager = AliasProperty(_get_layout_manager, _set_layout_manager)
|
||||
"""
|
||||
The Layout manager responsible for positioning views within the
|
||||
:class:`RecycleView`.
|
||||
|
||||
layout_manager is an :class:`~kivy.properties.AliasProperty` that gets
|
||||
and sets the layout_manger.
|
||||
"""
|
||||
|
||||
|
||||
class RecycleView(RecycleViewBehavior, ScrollView):
|
||||
"""
|
||||
RecycleView is a flexible view for providing a limited window
|
||||
into a large data set.
|
||||
|
||||
See the module documentation for more information.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
if self.data_model is None:
|
||||
kwargs.setdefault('data_model', RecycleDataModel())
|
||||
if self.view_adapter is None:
|
||||
kwargs.setdefault('view_adapter', RecycleDataAdapter())
|
||||
super(RecycleView, self).__init__(**kwargs)
|
||||
|
||||
fbind = self.fbind
|
||||
fbind('scroll_x', self.refresh_from_viewport)
|
||||
fbind('scroll_y', self.refresh_from_viewport)
|
||||
fbind('size', self.refresh_from_viewport)
|
||||
self.refresh_from_data()
|
||||
|
||||
def _convert_sv_to_lm(self, x, y):
|
||||
lm = self.layout_manager
|
||||
tree = [lm]
|
||||
parent = lm.parent
|
||||
while parent is not None and parent is not self:
|
||||
tree.append(parent)
|
||||
parent = parent.parent
|
||||
|
||||
if parent is not self:
|
||||
raise Exception(
|
||||
'The layout manager must be a sub child of the recycleview. '
|
||||
'Could not find {} in the parent tree of {}'.format(self, lm))
|
||||
|
||||
for widget in reversed(tree):
|
||||
x, y = widget.to_local(x, y)
|
||||
|
||||
return x, y
|
||||
|
||||
def get_viewport(self):
|
||||
lm = self.layout_manager
|
||||
lm_w, lm_h = lm.size
|
||||
w, h = self.size
|
||||
scroll_y = min(1, max(self.scroll_y, 0))
|
||||
scroll_x = min(1, max(self.scroll_x, 0))
|
||||
|
||||
if lm_h <= h:
|
||||
bottom = 0
|
||||
else:
|
||||
above = (lm_h - h) * scroll_y
|
||||
bottom = max(0, lm_h - above - h)
|
||||
|
||||
bottom = max(0, (lm_h - h) * scroll_y)
|
||||
left = max(0, (lm_w - w) * scroll_x)
|
||||
width = min(w, lm_w)
|
||||
height = min(h, lm_h)
|
||||
|
||||
# now convert the sv coordinates into the coordinates of the lm. In
|
||||
# case there's a relative layout type widget in the parent tree
|
||||
# between the sv and the lm.
|
||||
left, bottom = self._convert_sv_to_lm(left, bottom)
|
||||
return left, bottom, width, height
|
||||
|
||||
def save_viewport(self):
|
||||
pass
|
||||
|
||||
def restore_viewport(self):
|
||||
pass
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
super(RecycleView, self).add_widget(widget, *args, **kwargs)
|
||||
if (isinstance(widget, RecycleLayoutManagerBehavior) and
|
||||
not self.layout_manager):
|
||||
self.layout_manager = widget
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
super(RecycleView, self).remove_widget(widget, *args, **kwargs)
|
||||
if self.layout_manager == widget:
|
||||
self.layout_manager = None
|
||||
|
||||
# or easier way to use
|
||||
def _get_data(self):
|
||||
d = self.data_model
|
||||
return d and d.data
|
||||
|
||||
def _set_data(self, value):
|
||||
d = self.data_model
|
||||
if d is not None:
|
||||
d.data = value
|
||||
|
||||
data = AliasProperty(_get_data, _set_data, bind=["data_model"])
|
||||
"""
|
||||
The data used by the current view adapter. This is a list of dicts whose
|
||||
keys map to the corresponding property names of the
|
||||
:attr:`~RecycleView.viewclass`.
|
||||
|
||||
data is an :class:`~kivy.properties.AliasProperty` that gets and sets the
|
||||
data used to generate the views.
|
||||
"""
|
||||
|
||||
def _get_viewclass(self):
|
||||
a = self.layout_manager
|
||||
return a and a.viewclass
|
||||
|
||||
def _set_viewclass(self, value):
|
||||
a = self.layout_manager
|
||||
if a:
|
||||
a.viewclass = value
|
||||
|
||||
viewclass = AliasProperty(_get_viewclass, _set_viewclass,
|
||||
bind=["layout_manager"])
|
||||
"""
|
||||
The viewclass used by the current layout_manager.
|
||||
|
||||
viewclass is an :class:`~kivy.properties.AliasProperty` that gets and sets
|
||||
the class used to generate the individual items presented in the view.
|
||||
"""
|
||||
|
||||
def _get_key_viewclass(self):
|
||||
a = self.layout_manager
|
||||
return a and a.key_viewclass
|
||||
|
||||
def _set_key_viewclass(self, value):
|
||||
a = self.layout_manager
|
||||
if a:
|
||||
a.key_viewclass = value
|
||||
|
||||
key_viewclass = AliasProperty(_get_key_viewclass, _set_key_viewclass,
|
||||
bind=["layout_manager"])
|
||||
"""
|
||||
key_viewclass is an :class:`~kivy.properties.AliasProperty` that gets and
|
||||
sets the key viewclass for the current
|
||||
:attr:`~kivy.uix.recycleview.layout_manager`.
|
||||
"""
|
||||
@@ -0,0 +1,194 @@
|
||||
'''
|
||||
RecycleView Data Model
|
||||
======================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The data model part of the RecycleView model-view-controller pattern.
|
||||
|
||||
It defines the models (classes) that store the data associated with a
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior`. Each model (class)
|
||||
determines how the data is stored and emits requests to the controller
|
||||
(:class:`~kivy.uix.recycleview.RecycleViewBehavior`) when the data is
|
||||
modified.
|
||||
'''
|
||||
|
||||
from kivy.properties import ListProperty, ObservableDict, ObjectProperty
|
||||
from kivy.event import EventDispatcher
|
||||
from functools import partial
|
||||
|
||||
__all__ = ('RecycleDataModelBehavior', 'RecycleDataModel')
|
||||
|
||||
|
||||
def recondition_slice_assign(val, last_len, new_len):
|
||||
if not isinstance(val, slice):
|
||||
return slice(val, val + 1)
|
||||
|
||||
diff = new_len - last_len
|
||||
|
||||
start, stop, step = val.start, val.stop, val.step
|
||||
if stop <= start:
|
||||
return slice(0, 0)
|
||||
|
||||
if step is not None and step != 1:
|
||||
assert last_len == new_len
|
||||
if stop < 0:
|
||||
stop = max(0, last_len + stop)
|
||||
stop = min(last_len, stop)
|
||||
|
||||
if start < 0:
|
||||
start = max(0, last_len + start)
|
||||
start = min(last_len, start)
|
||||
|
||||
return slice(start, stop, step)
|
||||
|
||||
if start < 0:
|
||||
start = last_len + start
|
||||
if stop < 0:
|
||||
stop = last_len + stop
|
||||
|
||||
# whatever, too complicated don't try to compute it
|
||||
if (start < 0 or stop < 0 or start > last_len or stop > last_len or
|
||||
new_len != last_len):
|
||||
return None
|
||||
|
||||
return slice(start, stop)
|
||||
|
||||
|
||||
class RecycleDataModelBehavior(object):
|
||||
""":class:`RecycleDataModelBehavior` is the base class for the models
|
||||
that describes and provides the data for the
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior`.
|
||||
|
||||
:Events:
|
||||
`on_data_changed`:
|
||||
Fired when the data changes. The event may dispatch
|
||||
keyword arguments specific to each implementation of the data
|
||||
model.
|
||||
When dispatched, the event and keyword arguments are forwarded to
|
||||
:meth:`~kivy.uix.recycleview.RecycleViewBehavior.\
|
||||
refresh_from_data`.
|
||||
"""
|
||||
|
||||
__events__ = ("on_data_changed", )
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
'''The
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior` instance
|
||||
associated with this data model.
|
||||
'''
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
'''Associates a
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior` with
|
||||
this data model.
|
||||
'''
|
||||
self.recycleview = rv
|
||||
if rv:
|
||||
self.fbind('on_data_changed', rv.refresh_from_data)
|
||||
|
||||
def detach_recycleview(self):
|
||||
'''Removes the
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
associated with this data model.
|
||||
'''
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
self.funbind('on_data_changed', rv.refresh_from_data)
|
||||
self.recycleview = None
|
||||
|
||||
def on_data_changed(self, *largs, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class RecycleDataModel(RecycleDataModelBehavior, EventDispatcher):
|
||||
'''An implementation of :class:`RecycleDataModelBehavior` that keeps the
|
||||
data in a indexable list. See :attr:`data`.
|
||||
|
||||
When data changes this class currently dispatches `on_data_changed` with
|
||||
one of the following additional keyword arguments.
|
||||
|
||||
`none`: no keyword argument
|
||||
With no additional argument it means a generic data change.
|
||||
`removed`: a slice or integer
|
||||
The value is a slice or integer indicating the indices removed.
|
||||
`appended`: a slice
|
||||
The slice in :attr:`data` indicating the first and last new items
|
||||
(i.e. the slice pointing to the new items added at the end).
|
||||
`inserted`: a integer
|
||||
The index in :attr:`data` where a new data item was inserted.
|
||||
`modified`: a slice
|
||||
The slice with the indices where the data has been modified.
|
||||
This currently does not allow changing of size etc.
|
||||
'''
|
||||
|
||||
data = ListProperty([])
|
||||
'''Stores the model's data using a list.
|
||||
|
||||
The data for a item at index `i` can also be accessed with
|
||||
:class:`RecycleDataModel` `[i]`.
|
||||
'''
|
||||
|
||||
_last_len = 0
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fbind('data', self._on_data_callback)
|
||||
super(RecycleDataModel, self).__init__(**kwargs)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.data[index]
|
||||
|
||||
@property
|
||||
def observable_dict(self):
|
||||
'''A dictionary instance, which when modified will trigger a `data` and
|
||||
consequently an `on_data_changed` dispatch.
|
||||
'''
|
||||
return partial(ObservableDict, self.__class__.data, self)
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
super(RecycleDataModel, self).attach_recycleview(rv)
|
||||
if rv:
|
||||
self.fbind('data', rv._dispatch_prop_on_source, 'data')
|
||||
|
||||
def detach_recycleview(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
self.funbind('data', rv._dispatch_prop_on_source, 'data')
|
||||
super(RecycleDataModel, self).detach_recycleview()
|
||||
|
||||
def _on_data_callback(self, instance, value):
|
||||
last_len = self._last_len
|
||||
new_len = self._last_len = len(self.data)
|
||||
op, val = value.last_op
|
||||
|
||||
if op == '__setitem__':
|
||||
val = recondition_slice_assign(val, last_len, new_len)
|
||||
if val is not None:
|
||||
self.dispatch('on_data_changed', modified=val)
|
||||
else:
|
||||
self.dispatch('on_data_changed')
|
||||
elif op == '__delitem__':
|
||||
self.dispatch('on_data_changed', removed=val)
|
||||
elif op == '__setslice__':
|
||||
val = recondition_slice_assign(slice(*val), last_len, new_len)
|
||||
if val is not None:
|
||||
self.dispatch('on_data_changed', modified=val)
|
||||
else:
|
||||
self.dispatch('on_data_changed')
|
||||
elif op == '__delslice__':
|
||||
self.dispatch('on_data_changed', removed=slice(*val))
|
||||
elif op == '__iadd__' or op == '__imul__':
|
||||
self.dispatch('on_data_changed', appended=slice(last_len, new_len))
|
||||
elif op == 'append':
|
||||
self.dispatch('on_data_changed', appended=slice(last_len, new_len))
|
||||
elif op == 'insert':
|
||||
self.dispatch('on_data_changed', inserted=val)
|
||||
elif op == 'pop':
|
||||
if val:
|
||||
self.dispatch('on_data_changed', removed=val[0])
|
||||
else:
|
||||
self.dispatch('on_data_changed', removed=last_len - 1)
|
||||
elif op == 'extend':
|
||||
self.dispatch('on_data_changed', appended=slice(last_len, new_len))
|
||||
else:
|
||||
self.dispatch('on_data_changed')
|
||||
@@ -0,0 +1,253 @@
|
||||
'''
|
||||
RecycleView Layouts
|
||||
===================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The Layouts handle the presentation of views for the
|
||||
:class:`~kivy.uix.recycleview.RecycleView`.
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
'''
|
||||
from kivy.compat import string_types
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import StringProperty, ObjectProperty
|
||||
from kivy.uix.behaviors import CompoundSelectionBehavior
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior, \
|
||||
_view_base_cache
|
||||
|
||||
|
||||
class LayoutChangeException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LayoutSelectionBehavior(CompoundSelectionBehavior):
|
||||
'''The :class:`LayoutSelectionBehavior` can be combined with
|
||||
:class:`RecycleLayoutManagerBehavior` to allow its derived classes
|
||||
selection behaviors similarly to how
|
||||
:class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
can be used to add selection behaviors to normal layout.
|
||||
|
||||
:class:`RecycleLayoutManagerBehavior` manages its children
|
||||
differently than normal layouts or widgets so this class adapts
|
||||
:class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
based selection to work with :class:`RecycleLayoutManagerBehavior` as well.
|
||||
|
||||
Similarly to
|
||||
:class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`,
|
||||
one can select using the keyboard or touch, which calls :meth:`select_node`
|
||||
or :meth:`deselect_node`, or one can call these methods directly. When a
|
||||
item is selected or deselected :meth:`apply_selection` is called. See
|
||||
:meth:`apply_selection`.
|
||||
|
||||
|
||||
'''
|
||||
|
||||
key_selection = StringProperty(None, allownone=True)
|
||||
'''The key used to check whether a view of a data item can be selected
|
||||
with touch or the keyboard.
|
||||
|
||||
:attr:`key_selection` is the key in data, which if present and ``True``
|
||||
will enable selection for this item from the keyboard or with a touch.
|
||||
When None, the default, not item will be selectable.
|
||||
|
||||
:attr:`key_selection` is a :class:`StringProperty` and defaults to None.
|
||||
|
||||
.. note::
|
||||
All data items can be selected directly using :meth:`select_node` or
|
||||
:meth:`deselect_node`, even if :attr:`key_selection` is False.
|
||||
'''
|
||||
|
||||
_selectable_nodes = []
|
||||
_nodes_map = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.nodes_order_reversed = False
|
||||
super(LayoutSelectionBehavior, self).__init__(**kwargs)
|
||||
|
||||
def compute_sizes_from_data(self, data, flags):
|
||||
# overwrite this method so that when data changes we update
|
||||
# selectable nodes.
|
||||
key = self.key_selection
|
||||
if key is None:
|
||||
nodes = self._selectable_nodes = []
|
||||
else:
|
||||
nodes = self._selectable_nodes = [
|
||||
i for i, d in enumerate(data) if d.get(key)]
|
||||
|
||||
self._nodes_map = {v: k for k, v in enumerate(nodes)}
|
||||
return super(LayoutSelectionBehavior, self).compute_sizes_from_data(
|
||||
data, flags)
|
||||
|
||||
def get_selectable_nodes(self):
|
||||
# the indices of the data is used as the nodes
|
||||
return self._selectable_nodes
|
||||
|
||||
def get_index_of_node(self, node, selectable_nodes):
|
||||
# the indices of the data is used as the nodes, so node
|
||||
return self._nodes_map[node]
|
||||
|
||||
def goto_node(self, key, last_node, last_node_idx):
|
||||
node, idx = super(LayoutSelectionBehavior, self).goto_node(
|
||||
key, last_node, last_node_idx)
|
||||
if node is not last_node:
|
||||
self.goto_view(node)
|
||||
return node, idx
|
||||
|
||||
def select_node(self, node):
|
||||
if super(LayoutSelectionBehavior, self).select_node(node):
|
||||
view = self.recycleview.view_adapter.get_visible_view(node)
|
||||
if view is not None:
|
||||
self.apply_selection(node, view, True)
|
||||
|
||||
def deselect_node(self, node):
|
||||
if super(LayoutSelectionBehavior, self).deselect_node(node):
|
||||
view = self.recycleview.view_adapter.get_visible_view(node)
|
||||
if view is not None:
|
||||
self.apply_selection(node, view, False)
|
||||
|
||||
def apply_selection(self, index, view, is_selected):
|
||||
'''Applies the selection to the view. This is called internally when
|
||||
a view is displayed and it needs to be shown as selected or as not
|
||||
selected.
|
||||
|
||||
It is called when :meth:`select_node` or :meth:`deselect_node` is
|
||||
called or when a view needs to be refreshed. Its function is purely to
|
||||
update the view to reflect the selection state. So the function may be
|
||||
called multiple times even if the selection state may not have changed.
|
||||
|
||||
If the view is a instance of
|
||||
:class:`~kivy.uix.recycleview.views.RecycleDataViewBehavior`, its
|
||||
:meth:`~kivy.uix.recycleview.views.RecycleDataViewBehavior.\
|
||||
apply_selection` method will be called every time the view needs to refresh
|
||||
the selection state. Otherwise, the this method is responsible
|
||||
for applying the selection.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`index`: int
|
||||
The index of the data item that is associated with the view.
|
||||
`view`: widget
|
||||
The widget that is the view of this data item.
|
||||
`is_selected`: bool
|
||||
Whether the item is selected.
|
||||
'''
|
||||
viewclass = view.__class__
|
||||
if viewclass not in _view_base_cache:
|
||||
_view_base_cache[viewclass] = isinstance(view,
|
||||
RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[viewclass]:
|
||||
view.apply_selection(self.recycleview, index, is_selected)
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
super(LayoutSelectionBehavior, self).refresh_view_layout(
|
||||
index, layout, view, viewport)
|
||||
self.apply_selection(index, view, index in self.selected_nodes)
|
||||
|
||||
|
||||
class RecycleLayoutManagerBehavior(object):
|
||||
"""A RecycleLayoutManagerBehavior is responsible for positioning views into
|
||||
the :attr:`RecycleView.data` within a :class:`RecycleView`. It adds new
|
||||
views into the data when it becomes visible to the user, and removes them
|
||||
when they leave the visible area.
|
||||
"""
|
||||
|
||||
viewclass = ObjectProperty(None)
|
||||
'''See :attr:`RecyclerView.viewclass`.
|
||||
'''
|
||||
key_viewclass = StringProperty(None)
|
||||
'''See :attr:`RecyclerView.key_viewclass`.
|
||||
'''
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
|
||||
asked_sizes = None
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
self.recycleview = rv
|
||||
if rv:
|
||||
fbind = self.fbind
|
||||
# can be made more selective update than refresh_from_data which
|
||||
# causes a full update. But this likely affects most of the data.
|
||||
fbind('viewclass', rv.refresh_from_data)
|
||||
fbind('key_viewclass', rv.refresh_from_data)
|
||||
fbind('viewclass', rv._dispatch_prop_on_source, 'viewclass')
|
||||
fbind('key_viewclass', rv._dispatch_prop_on_source,
|
||||
'key_viewclass')
|
||||
|
||||
def detach_recycleview(self):
|
||||
self.clear_layout()
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
funbind = self.funbind
|
||||
funbind('viewclass', rv.refresh_from_data)
|
||||
funbind('key_viewclass', rv.refresh_from_data)
|
||||
funbind('viewclass', rv._dispatch_prop_on_source, 'viewclass')
|
||||
funbind('key_viewclass', rv._dispatch_prop_on_source,
|
||||
'key_viewclass')
|
||||
self.recycleview = None
|
||||
|
||||
def compute_sizes_from_data(self, data, flags):
|
||||
pass
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
pass
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
'''`viewport` is in coordinates of the layout manager.
|
||||
'''
|
||||
pass
|
||||
|
||||
def set_visible_views(self, indices, data, viewport):
|
||||
'''`viewport` is in coordinates of the layout manager.
|
||||
'''
|
||||
pass
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
'''`See :meth:`~kivy.uix.recycleview.views.RecycleDataAdapter.\
|
||||
refresh_view_layout`.
|
||||
'''
|
||||
self.recycleview.view_adapter.refresh_view_layout(
|
||||
index, layout, view, viewport)
|
||||
|
||||
def get_view_index_at(self, pos):
|
||||
"""Return the view `index` on which position, `pos`, falls.
|
||||
|
||||
`pos` is in coordinates of the layout manager.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_views(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
adapter = rv.view_adapter
|
||||
if adapter:
|
||||
adapter.make_views_dirty()
|
||||
|
||||
def remove_view(self, view, index):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
adapter = rv.view_adapter
|
||||
if adapter:
|
||||
adapter.make_view_dirty(view, index)
|
||||
|
||||
def clear_layout(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
adapter = rv.view_adapter
|
||||
if adapter:
|
||||
adapter.invalidate()
|
||||
|
||||
def goto_view(self, index):
|
||||
'''Moves the views so that the view corresponding to `index` is
|
||||
visible.
|
||||
'''
|
||||
pass
|
||||
|
||||
def on_viewclass(self, instance, value):
|
||||
# resolve the real class if it was a string.
|
||||
if isinstance(value, string_types):
|
||||
self.viewclass = getattr(Factory, value)
|
||||
@@ -0,0 +1,423 @@
|
||||
'''
|
||||
RecycleView Views
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The adapter part of the RecycleView which together with the layout is the
|
||||
view part of the model-view-controller pattern.
|
||||
|
||||
The view module handles converting the data to a view using the adapter class
|
||||
which is then displayed by the layout. A view can be any Widget based class.
|
||||
However, inheriting from RecycleDataViewBehavior adds methods for converting
|
||||
the data to a view.
|
||||
|
||||
TODO:
|
||||
* Make view caches specific to each view class type.
|
||||
|
||||
'''
|
||||
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.event import EventDispatcher
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = (
|
||||
'RecycleDataViewBehavior', 'RecycleKVIDsDataViewBehavior',
|
||||
'RecycleDataAdapter')
|
||||
|
||||
_view_base_cache = {}
|
||||
'''Cache whose keys are classes and values is a boolean indicating whether the
|
||||
class inherits from :class:`RecycleDataViewBehavior`.
|
||||
'''
|
||||
|
||||
_cached_views = defaultdict(list)
|
||||
'''A size limited cache that contains old views (instances) that are not used.
|
||||
Each key is a class whose value is the list of the instances of that class.
|
||||
'''
|
||||
# current number of unused classes in the class cache
|
||||
_cache_count = 0
|
||||
# maximum number of items in the class cache
|
||||
_max_cache_size = 1000
|
||||
|
||||
|
||||
def _clean_cache():
|
||||
'''Trims _cached_views cache to half the size of `_max_cache_size`.
|
||||
'''
|
||||
# all keys will be reduced to max_size.
|
||||
max_size = (_max_cache_size // 2) // len(_cached_views)
|
||||
global _cache_count
|
||||
for cls, instances in _cached_views.items():
|
||||
_cache_count -= max(0, len(instances) - max_size)
|
||||
del instances[max_size:]
|
||||
|
||||
|
||||
class RecycleDataViewBehavior(object):
|
||||
'''A optional base class for data views (:attr:`RecycleView`.viewclass).
|
||||
If a view inherits from this class, the class's functions will be called
|
||||
when the view needs to be updated due to a data change or layout update.
|
||||
'''
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
'''Called by the :class:`RecycleAdapter` when the view is initially
|
||||
populated with the values from the `data` dictionary for this item.
|
||||
|
||||
Any pos or size info should be removed because they are set
|
||||
subsequently with :attr:`refresh_view_layout`.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`rv`: :class:`RecycleView` instance
|
||||
The :class:`RecycleView` that caused the update.
|
||||
`data`: dict
|
||||
The data dict used to populate this view.
|
||||
'''
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data.items():
|
||||
if key not in sizing_attrs:
|
||||
setattr(self, key, value)
|
||||
|
||||
def refresh_view_layout(self, rv, index, layout, viewport):
|
||||
'''Called when the view's size is updated by the layout manager,
|
||||
:class:`RecycleLayoutManagerBehavior`.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`rv`: :class:`RecycleView` instance
|
||||
The :class:`RecycleView` that caused the update.
|
||||
`viewport`: 4-tuple
|
||||
The coordinates of the bottom left and width height in layout
|
||||
manager coordinates. This may be larger than this view item.
|
||||
|
||||
:raises:
|
||||
`LayoutChangeException`: If the sizing or data changed during a
|
||||
call to this method, raising a `LayoutChangeException` exception
|
||||
will force a refresh. Useful when data changed and we don't want
|
||||
to layout further since it'll be overwritten again soon.
|
||||
'''
|
||||
w, h = layout.pop('size')
|
||||
if w is None:
|
||||
if h is not None:
|
||||
self.height = h
|
||||
else:
|
||||
if h is None:
|
||||
self.width = w
|
||||
else:
|
||||
self.size = w, h
|
||||
|
||||
for name, value in layout.items():
|
||||
setattr(self, name, value)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
pass
|
||||
|
||||
|
||||
class RecycleKVIDsDataViewBehavior(RecycleDataViewBehavior):
|
||||
"""Similar to :class:`RecycleDataViewBehavior`, except that the data keys
|
||||
can signify properties of an object named with an id in the root KV rule.
|
||||
|
||||
E.g. given a KV rule::
|
||||
|
||||
<MyRule@RecycleKVIDsDataViewBehavior+BoxLayout>:
|
||||
Label:
|
||||
id: name
|
||||
Label:
|
||||
id: value
|
||||
|
||||
Then setting the data list with
|
||||
``rv.data = [{'name.text': 'Kivy user', 'value.text': '12'}]`` would
|
||||
automatically set the corresponding labels.
|
||||
|
||||
So, if the key doesn't have a period, the named property of the root widget
|
||||
will be set to the corresponding value. If there is a period, the named
|
||||
property of the widget with the id listed before the period will be set to
|
||||
the corresponding value.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data.items():
|
||||
if key not in sizing_attrs:
|
||||
name, *ids = key.split('.')
|
||||
if ids:
|
||||
if len(ids) != 1:
|
||||
raise ValueError(
|
||||
f'Data key "{key}" has more than one period')
|
||||
setattr(self.ids[name], ids[0], value)
|
||||
else:
|
||||
setattr(self, name, value)
|
||||
|
||||
|
||||
class RecycleDataAdapter(EventDispatcher):
|
||||
'''The class that converts data to a view.
|
||||
|
||||
--- Internal details ---
|
||||
A view can have 3 states.
|
||||
|
||||
* It can be completely in sync with the data, which
|
||||
occurs when the view is displayed. These are stored in :attr:`views`.
|
||||
* It can be dirty, which occurs when the view is in sync with the data,
|
||||
except for the size/pos parameters which is controlled by the layout.
|
||||
This occurs when the view is not currently displayed but the data has
|
||||
not changed. These views are stored in :attr:`dirty_views`.
|
||||
* Finally the view can be dead which occurs when the data changes and
|
||||
the view was not updated or when a view is just created. Such views
|
||||
are typically added to the internal cache.
|
||||
|
||||
Typically what happens is that the layout manager lays out the data
|
||||
and then asks for views, using :meth:`set_visible_views`, for some specific
|
||||
data items that it displays.
|
||||
|
||||
These views are gotten from the current views, dirty or global cache. Then
|
||||
depending on the view state :meth:`refresh_view_attrs` is called to bring
|
||||
the view up to date with the data (except for sizing parameters). Finally,
|
||||
the layout manager gets these views, updates their size and displays them.
|
||||
'''
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`~kivy.uix.recycleview.RecycleViewBehavior` associated
|
||||
with this instance.
|
||||
'''
|
||||
|
||||
# internals
|
||||
views = {} # current displayed items
|
||||
# items whose attrs, except for pos/size is still accurate
|
||||
dirty_views = defaultdict(dict)
|
||||
|
||||
_sizing_attrs = {
|
||||
'size', 'width', 'height', 'size_hint', 'size_hint_x', 'size_hint_y',
|
||||
'pos', 'x', 'y', 'center', 'center_x', 'center_y', 'pos_hint',
|
||||
'size_hint_min', 'size_hint_min_x', 'size_hint_min_y', 'size_hint_max',
|
||||
'size_hint_max_x', 'size_hint_max_y'}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Fix for issue https://github.com/kivy/kivy/issues/5913:
|
||||
Scrolling RV A, then Scrolling RV B, content of A and B seemed
|
||||
to be getting mixed up
|
||||
"""
|
||||
self.views = {}
|
||||
self.dirty_views = defaultdict(dict)
|
||||
super(RecycleDataAdapter, self).__init__(**kwargs)
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
'''Associates a :class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
with this instance. It is stored in :attr:`recycleview`.
|
||||
'''
|
||||
self.recycleview = rv
|
||||
|
||||
def detach_recycleview(self):
|
||||
'''Removes the :class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
associated with this instance and clears :attr:`recycleview`.
|
||||
'''
|
||||
self.recycleview = None
|
||||
|
||||
def create_view(self, index, data_item, viewclass):
|
||||
'''(internal) Creates and initializes the view for the data at `index`.
|
||||
|
||||
The returned view is synced with the data, except for the pos/size
|
||||
information.
|
||||
'''
|
||||
if viewclass is None:
|
||||
return
|
||||
|
||||
view = viewclass()
|
||||
self.refresh_view_attrs(index, data_item, view)
|
||||
return view
|
||||
|
||||
def get_view(self, index, data_item, viewclass):
|
||||
'''(internal) Returns a view instance for the data at `index`
|
||||
|
||||
It looks through the various caches and finally creates a view if it
|
||||
doesn't exist. The returned view is synced with the data, except for
|
||||
the pos/size information.
|
||||
|
||||
If found in the cache it's removed from the source
|
||||
before returning. It doesn't check the current views.
|
||||
'''
|
||||
# is it in the dirtied views?
|
||||
dirty_views = self.dirty_views
|
||||
if viewclass is None:
|
||||
return
|
||||
stale = False
|
||||
view = None
|
||||
|
||||
if viewclass in dirty_views: # get it first from dirty list
|
||||
dirty_class = dirty_views[viewclass]
|
||||
if index in dirty_class:
|
||||
# we found ourself in the dirty list, no need to update data!
|
||||
view = dirty_class.pop(index)
|
||||
elif _cached_views[viewclass]:
|
||||
# global cache has this class, update data
|
||||
view, stale = _cached_views[viewclass].pop(), True
|
||||
elif dirty_class:
|
||||
# random any dirty view element - update data
|
||||
view, stale = dirty_class.popitem()[1], True
|
||||
elif _cached_views[viewclass]: # otherwise go directly to cache
|
||||
# global cache has this class, update data
|
||||
view, stale = _cached_views[viewclass].pop(), True
|
||||
|
||||
if view is None:
|
||||
view = self.create_view(index, data_item, viewclass)
|
||||
if view is None:
|
||||
return
|
||||
|
||||
if stale:
|
||||
self.refresh_view_attrs(index, data_item, view)
|
||||
return view
|
||||
|
||||
def refresh_view_attrs(self, index, data_item, view):
|
||||
'''(internal) Syncs the view and brings it up to date with the data.
|
||||
|
||||
This method calls :meth:`RecycleDataViewBehavior.refresh_view_attrs`
|
||||
if the view inherits from :class:`RecycleDataViewBehavior`. See that
|
||||
method for more details.
|
||||
|
||||
.. note::
|
||||
Any sizing and position info is skipped when syncing with the data.
|
||||
'''
|
||||
viewclass = view.__class__
|
||||
if viewclass not in _view_base_cache:
|
||||
_view_base_cache[viewclass] = isinstance(view,
|
||||
RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[viewclass]:
|
||||
view.refresh_view_attrs(self.recycleview, index, data_item)
|
||||
else:
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data_item.items():
|
||||
if key not in sizing_attrs:
|
||||
setattr(view, key, value)
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
'''Updates the sizing information of the view.
|
||||
|
||||
viewport is in coordinates of the layout manager.
|
||||
|
||||
This method calls :meth:`RecycleDataViewBehavior.refresh_view_attrs`
|
||||
if the view inherits from :class:`RecycleDataViewBehavior`. See that
|
||||
method for more details.
|
||||
|
||||
.. note::
|
||||
Any sizing and position info is skipped when syncing with the data.
|
||||
'''
|
||||
if view.__class__ not in _view_base_cache:
|
||||
_view_base_cache[view.__class__] = isinstance(
|
||||
view, RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[view.__class__]:
|
||||
view.refresh_view_layout(
|
||||
self.recycleview, index, layout, viewport)
|
||||
else:
|
||||
w, h = layout.pop('size')
|
||||
if w is None:
|
||||
if h is not None:
|
||||
view.height = h
|
||||
else:
|
||||
if h is None:
|
||||
view.width = w
|
||||
else:
|
||||
view.size = w, h
|
||||
|
||||
for name, value in layout.items():
|
||||
setattr(view, name, value)
|
||||
|
||||
def make_view_dirty(self, view, index):
|
||||
'''(internal) Used to flag this view as dirty, ready to be used for
|
||||
others. See :meth:`make_views_dirty`.
|
||||
'''
|
||||
del self.views[index]
|
||||
self.dirty_views[view.__class__][index] = view
|
||||
|
||||
def make_views_dirty(self):
|
||||
'''Makes all the current views dirty.
|
||||
|
||||
Dirty views are still in sync with the corresponding data. However, the
|
||||
size information may go out of sync. Therefore a dirty view can be
|
||||
reused by the same index by just updating the sizing information.
|
||||
|
||||
Once the underlying data of this index changes, the view should be
|
||||
removed from the dirty views and moved to the global cache with
|
||||
:meth:`invalidate`.
|
||||
|
||||
This is typically called when the layout manager needs to re-layout all
|
||||
the data.
|
||||
'''
|
||||
views = self.views
|
||||
if not views:
|
||||
return
|
||||
|
||||
dirty_views = self.dirty_views
|
||||
for index, view in views.items():
|
||||
dirty_views[view.__class__][index] = view
|
||||
self.views = {}
|
||||
|
||||
def invalidate(self):
|
||||
'''Moves all the current views into the global cache.
|
||||
|
||||
As opposed to making a view dirty where the view is in sync with the
|
||||
data except for sizing information, this will completely disconnect the
|
||||
view from the data, as it is assumed the data has gone out of sync with
|
||||
the view.
|
||||
|
||||
This is typically called when the data changes.
|
||||
'''
|
||||
global _cache_count
|
||||
for view in self.views.values():
|
||||
_cached_views[view.__class__].append(view)
|
||||
_cache_count += 1
|
||||
|
||||
for cls, views in self.dirty_views.items():
|
||||
_cached_views[cls].extend(views.values())
|
||||
_cache_count += len(views)
|
||||
|
||||
if _cache_count >= _max_cache_size:
|
||||
_clean_cache()
|
||||
self.views = {}
|
||||
self.dirty_views.clear()
|
||||
|
||||
def set_visible_views(self, indices, data, viewclasses):
|
||||
'''Gets a 3-tuple of the new, remaining, and old views for the current
|
||||
viewport.
|
||||
|
||||
The new views are synced to the data except for the size/pos
|
||||
properties.
|
||||
The old views need to be removed from the layout, and the new views
|
||||
added.
|
||||
|
||||
The new views are not necessarily *new*, but are all the currently
|
||||
visible views.
|
||||
'''
|
||||
visible_views = {}
|
||||
previous_views = self.views
|
||||
ret_new = []
|
||||
ret_remain = []
|
||||
get_view = self.get_view
|
||||
|
||||
# iterate though the visible view
|
||||
# add them into the container if not already done
|
||||
for index in indices:
|
||||
view = previous_views.pop(index, None)
|
||||
if view is not None: # was current view
|
||||
visible_views[index] = view
|
||||
ret_remain.append((index, view))
|
||||
else:
|
||||
view = get_view(index, data[index],
|
||||
viewclasses[index]['viewclass'])
|
||||
if view is None:
|
||||
continue
|
||||
visible_views[index] = view
|
||||
ret_new.append((index, view))
|
||||
|
||||
old_views = previous_views.items()
|
||||
self.make_views_dirty()
|
||||
self.views = visible_views
|
||||
return ret_new, ret_remain, old_views
|
||||
|
||||
def get_visible_view(self, index):
|
||||
'''Returns the currently visible view associated with ``index``.
|
||||
|
||||
If no view is currently displayed for ``index`` it returns ``None``.
|
||||
'''
|
||||
return self.views.get(index)
|
||||
@@ -0,0 +1,324 @@
|
||||
'''
|
||||
Relative Layout
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
|
||||
This layout allows you to set relative coordinates for children. If you want
|
||||
absolute positioning, use the :class:`~kivy.uix.floatlayout.FloatLayout`.
|
||||
|
||||
The :class:`RelativeLayout` class behaves just like the regular
|
||||
:class:`FloatLayout` except that its child widgets are positioned relative to
|
||||
the layout.
|
||||
|
||||
When a widget with position = (0,0) is added to a RelativeLayout,
|
||||
the child widget will also move when the position of the RelativeLayout
|
||||
is changed. The child widgets coordinates remain (0,0) as they are
|
||||
always relative to the parent layout.
|
||||
|
||||
Coordinate Systems
|
||||
------------------
|
||||
|
||||
Window coordinates
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, there's only one coordinate system that defines the position of
|
||||
widgets and touch events dispatched to them: the window coordinate system,
|
||||
which places (0, 0) at the bottom left corner of the window.
|
||||
Although there are other coordinate systems defined, e.g. local
|
||||
and parent coordinates, these coordinate systems are identical to the window
|
||||
coordinate system as long as a relative layout type widget is not in the
|
||||
widget's parent stack. When widget.pos is read or a touch is received,
|
||||
the coordinate values are in parent coordinates. But as mentioned, these are
|
||||
identical to window coordinates, even in complex widget stacks as long as
|
||||
there's no relative layout type widget in the widget's parent stack.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
BoxLayout:
|
||||
Label:
|
||||
text: 'Left'
|
||||
Button:
|
||||
text: 'Middle'
|
||||
on_touch_down: print('Middle: {}'.format(args[1].pos))
|
||||
BoxLayout:
|
||||
on_touch_down: print('Box: {}'.format(args[1].pos))
|
||||
Button:
|
||||
text: 'Right'
|
||||
on_touch_down: print('Right: {}'.format(args[1].pos))
|
||||
|
||||
When the middle button is clicked and the touch propagates through the
|
||||
different parent coordinate systems, it prints the following::
|
||||
|
||||
>>> Box: (430.0, 282.0)
|
||||
>>> Right: (430.0, 282.0)
|
||||
>>> Middle: (430.0, 282.0)
|
||||
|
||||
As claimed, the touch has identical coordinates to the window coordinates
|
||||
in every coordinate system. :meth:`~kivy.uix.widget.Widget.collide_point`
|
||||
for example, takes the point in window coordinates.
|
||||
|
||||
Parent coordinates
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Other :class:`RelativeLayout` type widgets are
|
||||
:class:`~kivy.uix.scatter.Scatter`,
|
||||
:class:`~kivy.uix.scatterlayout.ScatterLayout`,
|
||||
and :class:`~kivy.uix.scrollview.ScrollView`. If such a special widget is in
|
||||
the parent stack, only then does the parent and local coordinate system
|
||||
diverge from the window coordinate system. For each such widget in the stack,
|
||||
a coordinate system with (0, 0) of that coordinate system being at the bottom
|
||||
left corner of that widget is created. **Position and touch coordinates
|
||||
received and read by a widget are in the coordinate system of the most
|
||||
recent special widget in its parent stack (not including itself) or in window
|
||||
coordinates if there are none** (as in the first example). We call these
|
||||
coordinates parent coordinates.
|
||||
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
BoxLayout:
|
||||
Label:
|
||||
text: 'Left'
|
||||
Button:
|
||||
text: 'Middle'
|
||||
on_touch_down: print('Middle: {}'.format(args[1].pos))
|
||||
RelativeLayout:
|
||||
on_touch_down: print('Relative: {}'.format(args[1].pos))
|
||||
Button:
|
||||
text: 'Right'
|
||||
on_touch_down: print('Right: {}'.format(args[1].pos))
|
||||
|
||||
Clicking on the middle button prints::
|
||||
|
||||
>>> Relative: (396.0, 298.0)
|
||||
>>> Right: (-137.33, 298.0)
|
||||
>>> Middle: (396.0, 298.0)
|
||||
|
||||
As the touch propagates through the widgets, for each widget, the
|
||||
touch is received in parent coordinates. Because both the relative and middle
|
||||
widgets don't have these special widgets in their parent stack, the touch is
|
||||
the same as window coordinates. Only the right widget, which has a
|
||||
RelativeLayout in its parent stack, receives the touch in coordinates relative
|
||||
to that RelativeLayout which is different than window coordinates.
|
||||
|
||||
Local and Widget coordinates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When expressed in parent coordinates, the position is expressed in the
|
||||
coordinates of the most recent special widget in its parent stack, not
|
||||
including itself. When expressed in local or widget coordinates, the widgets
|
||||
themselves are also included.
|
||||
|
||||
Changing the above example to transform the parent coordinates into local
|
||||
coordinates:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
BoxLayout:
|
||||
Label:
|
||||
text: 'Left'
|
||||
Button:
|
||||
text: 'Middle'
|
||||
on_touch_down: print('Middle: {}'.format(\
|
||||
self.to_local(*args[1].pos)))
|
||||
RelativeLayout:
|
||||
on_touch_down: print('Relative: {}'.format(\
|
||||
self.to_local(*args[1].pos)))
|
||||
Button:
|
||||
text: 'Right'
|
||||
on_touch_down: print('Right: {}'.format(\
|
||||
self.to_local(*args[1].pos)))
|
||||
|
||||
Now, clicking on the middle button prints::
|
||||
|
||||
>>> Relative: (-135.33, 301.0)
|
||||
>>> Right: (-135.33, 301.0)
|
||||
>>> Middle: (398.0, 301.0)
|
||||
|
||||
This is because now the relative widget also expresses the coordinates
|
||||
relative to itself.
|
||||
|
||||
.. note::
|
||||
|
||||
Although all widgets including :class:`RelativeLayout` receive their touch
|
||||
events in ``on_touch_xxx`` in parent coordinates, these special widgets
|
||||
will transform the touch position to be in local coordinates before it
|
||||
calls ``super``. This may only be noticeable in a complex inheritance
|
||||
class.
|
||||
|
||||
Coordinate transformations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:class:`~kivy.uix.widget.Widget` provides 4 functions to transform coordinates
|
||||
between the various coordinate systems. For now, we assume that the `relative`
|
||||
keyword of these functions is `False`.
|
||||
:meth:`~kivy.uix.widget.Widget.to_widget` takes the coordinates expressed in
|
||||
window coordinates and returns them in local (widget) coordinates.
|
||||
:meth:`~kivy.uix.widget.Widget.to_window` takes the coordinates expressed in
|
||||
local coordinates and returns them in window coordinates.
|
||||
:meth:`~kivy.uix.widget.Widget.to_parent` takes the coordinates expressed in
|
||||
local coordinates and returns them in parent coordinates.
|
||||
:meth:`~kivy.uix.widget.Widget.to_local` takes the coordinates expressed in
|
||||
parent coordinates and returns them in local coordinates.
|
||||
|
||||
Each of the 4 transformation functions take a `relative` parameter. When the
|
||||
relative parameter is True, the coordinates are returned or originate in
|
||||
true relative coordinates - relative to a coordinate system with its (0, 0) at
|
||||
the bottom left corner of the widget in question.
|
||||
|
||||
.. _kivy-uix-relativelayout-common-pitfalls:
|
||||
|
||||
Common Pitfalls
|
||||
---------------
|
||||
|
||||
As all positions within a :class:`RelativeLayout` are relative to the position
|
||||
of the layout itself, the position of the layout should never be used in
|
||||
determining the position of sub-widgets or the layout's :attr:`canvas`.
|
||||
|
||||
Take the following kv code for example:
|
||||
|
||||
.. container:: align-right
|
||||
|
||||
.. figure:: images/relativelayout-fixedposition.png
|
||||
:scale: 50%
|
||||
|
||||
expected result
|
||||
|
||||
.. figure:: images/relativelayout-doubleposition.png
|
||||
:scale: 50%
|
||||
|
||||
actual result
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
FloatLayout:
|
||||
Widget:
|
||||
size_hint: None, None
|
||||
size: 200, 200
|
||||
pos: 200, 200
|
||||
|
||||
canvas:
|
||||
Color:
|
||||
rgba: 1, 1, 1, 1
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
|
||||
RelativeLayout:
|
||||
size_hint: None, None
|
||||
size: 200, 200
|
||||
pos: 200, 200
|
||||
|
||||
canvas:
|
||||
Color:
|
||||
rgba: 1, 0, 0, 0.5
|
||||
Rectangle:
|
||||
pos: self.pos # incorrect
|
||||
size: self.size
|
||||
|
||||
You might expect this to render a single pink rectangle; however, the content
|
||||
of the :class:`RelativeLayout` is already transformed, so the use of
|
||||
`pos: self.pos` will double that transformation. In this case, using
|
||||
`pos: 0, 0` or omitting `pos` completely will provide the expected result.
|
||||
|
||||
This also applies to the position of sub-widgets. Instead of positioning a
|
||||
:class:`~kivy.uix.widget.Widget` based on the layout's own position:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
RelativeLayout:
|
||||
Widget:
|
||||
pos: self.parent.pos
|
||||
Widget:
|
||||
center: self.parent.center
|
||||
|
||||
use the :attr:`pos_hint` property:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
RelativeLayout:
|
||||
Widget:
|
||||
pos_hint: {'x': 0, 'y': 0}
|
||||
Widget:
|
||||
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
Prior to version 1.7.0, the :class:`RelativeLayout` was implemented as a
|
||||
:class:`~kivy.uix.floatlayout.FloatLayout` inside a
|
||||
:class:`~kivy.uix.scatter.Scatter`. This behavior/widget has
|
||||
been renamed to `ScatterLayout`. The :class:`RelativeLayout` now only
|
||||
supports relative positions (and can't be rotated, scaled or translated on
|
||||
a multitouch system using two or more fingers). This was done so that the
|
||||
implementation could be optimized and avoid the heavier calculations of
|
||||
:class:`Scatter` (e.g. inverse matrix, recalculating multiple properties
|
||||
etc.)
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('RelativeLayout', )
|
||||
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
|
||||
|
||||
class RelativeLayout(FloatLayout):
|
||||
'''RelativeLayout class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
def __init__(self, **kw):
|
||||
super(RelativeLayout, self).__init__(**kw)
|
||||
funbind = self.funbind
|
||||
trigger = self._trigger_layout
|
||||
funbind('pos', trigger)
|
||||
funbind('pos_hint', trigger)
|
||||
|
||||
def do_layout(self, *args):
|
||||
super(RelativeLayout, self).do_layout(pos=(0, 0))
|
||||
|
||||
def to_parent(self, x, y, **k):
|
||||
return (x + self.x, y + self.y)
|
||||
|
||||
def to_local(self, x, y, **k):
|
||||
return (x - self.x, y - self.y)
|
||||
|
||||
def _apply_transform(self, m, pos=None):
|
||||
m.translate(self.x, self.y, 0)
|
||||
return super(RelativeLayout, self)._apply_transform(m, (0, 0))
|
||||
|
||||
def on_motion(self, etype, me):
|
||||
if me.type_id in self.motion_filter and 'pos' in me.profile:
|
||||
me.push()
|
||||
me.apply_transform_2d(self.to_local)
|
||||
ret = super().on_motion(etype, me)
|
||||
me.pop()
|
||||
return ret
|
||||
return super().on_motion(etype, me)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
x, y = touch.x, touch.y
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.to_local)
|
||||
ret = super(RelativeLayout, self).on_touch_down(touch)
|
||||
touch.pop()
|
||||
return ret
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
x, y = touch.x, touch.y
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.to_local)
|
||||
ret = super(RelativeLayout, self).on_touch_move(touch)
|
||||
touch.pop()
|
||||
return ret
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
x, y = touch.x, touch.y
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.to_local)
|
||||
ret = super(RelativeLayout, self).on_touch_up(touch)
|
||||
touch.pop()
|
||||
return ret
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,198 @@
|
||||
'''
|
||||
Sandbox
|
||||
=======
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. warning::
|
||||
|
||||
This is experimental and subject to change as long as this warning notice
|
||||
is present.
|
||||
|
||||
This is a widget that runs itself and all of its children in a Sandbox. That
|
||||
means if a child raises an Exception, it will be caught. The Sandbox
|
||||
itself runs its own Clock, Cache, etc.
|
||||
|
||||
The SandBox widget is still experimental and required for the Kivy designer.
|
||||
When the user designs their own widget, if they do something wrong (wrong size
|
||||
value, invalid python code), it will be caught correctly without breaking
|
||||
the whole application. Because it has been designed that way, we are still
|
||||
enhancing this widget and the :mod:`kivy.context` module.
|
||||
Don't use it unless you know what you are doing.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Sandbox', )
|
||||
|
||||
from functools import wraps
|
||||
from kivy.context import Context
|
||||
from kivy.base import ExceptionManagerBase
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
def sandbox(f):
|
||||
@wraps(f)
|
||||
def _f2(self, *args, **kwargs):
|
||||
ret = None
|
||||
with self:
|
||||
ret = f(self, *args, **kwargs)
|
||||
return ret
|
||||
return _f2
|
||||
|
||||
|
||||
class SandboxExceptionManager(ExceptionManagerBase):
|
||||
|
||||
def __init__(self, sandbox):
|
||||
ExceptionManagerBase.__init__(self)
|
||||
self.sandbox = sandbox
|
||||
|
||||
def handle_exception(self, e):
|
||||
if not self.sandbox.on_exception(e):
|
||||
return ExceptionManagerBase.RAISE
|
||||
return ExceptionManagerBase.PASS
|
||||
|
||||
|
||||
class SandboxContent(RelativeLayout):
|
||||
pass
|
||||
|
||||
|
||||
class Sandbox(FloatLayout):
|
||||
'''Sandbox widget, used to trap all the exceptions raised by child
|
||||
widgets.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._context = Context(init=True)
|
||||
self._context['ExceptionManager'] = SandboxExceptionManager(self)
|
||||
self._context.sandbox = self
|
||||
self._context.push()
|
||||
self.on_context_created()
|
||||
self._container = None
|
||||
super(Sandbox, self).__init__(**kwargs)
|
||||
self._container = SandboxContent(size=self.size, pos=self.pos)
|
||||
super(Sandbox, self).add_widget(self._container)
|
||||
self._context.pop()
|
||||
|
||||
# force SandboxClock's scheduling
|
||||
Clock.schedule_interval(self._clock_sandbox, 0)
|
||||
Clock.schedule_once(self._clock_sandbox_draw, -1)
|
||||
self.main_clock = object.__getattribute__(Clock, '_obj')
|
||||
|
||||
def __enter__(self):
|
||||
self._context.push()
|
||||
|
||||
def __exit__(self, _type, value, traceback):
|
||||
self._context.pop()
|
||||
if _type is not None:
|
||||
return self.on_exception(value, _traceback=traceback)
|
||||
|
||||
def on_context_created(self):
|
||||
'''Override this method in order to load your kv file or do anything
|
||||
else with the newly created context.
|
||||
'''
|
||||
pass
|
||||
|
||||
def on_exception(self, exception, _traceback=None):
|
||||
'''Override this method in order to catch all the exceptions from
|
||||
children.
|
||||
|
||||
If you return True, it will not reraise the exception.
|
||||
If you return False, the exception will be raised to the parent.
|
||||
'''
|
||||
import traceback
|
||||
traceback.print_tb(_traceback)
|
||||
return True
|
||||
|
||||
on_motion = sandbox(Widget.on_motion)
|
||||
on_touch_down = sandbox(Widget.on_touch_down)
|
||||
on_touch_move = sandbox(Widget.on_touch_move)
|
||||
on_touch_up = sandbox(Widget.on_touch_up)
|
||||
|
||||
@sandbox
|
||||
def add_widget(self, *args, **kwargs):
|
||||
self._container.add_widget(*args, **kwargs)
|
||||
|
||||
@sandbox
|
||||
def remove_widget(self, *args, **kwargs):
|
||||
self._container.remove_widget(*args, **kwargs)
|
||||
|
||||
@sandbox
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
self._container.clear_widgets(*args, **kwargs)
|
||||
|
||||
@sandbox
|
||||
def on_size(self, *args):
|
||||
if self._container:
|
||||
self._container.size = self.size
|
||||
|
||||
@sandbox
|
||||
def on_pos(self, *args):
|
||||
if self._container:
|
||||
self._container.pos = self.pos
|
||||
|
||||
@sandbox
|
||||
def _clock_sandbox(self, dt):
|
||||
# import pdb; pdb.set_trace()
|
||||
Clock.tick()
|
||||
Builder.sync()
|
||||
|
||||
@sandbox
|
||||
def _clock_sandbox_draw(self, dt):
|
||||
Clock.tick_draw()
|
||||
Builder.sync()
|
||||
self.main_clock.schedule_once(self._call_draw, 0)
|
||||
|
||||
def _call_draw(self, dt):
|
||||
self.main_clock.schedule_once(self._clock_sandbox_draw, -1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.button import Button
|
||||
|
||||
class TestButton(Button):
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
# raise Exception('fdfdfdfdfdfdfd')
|
||||
return super(TestButton, self).on_touch_up(touch)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
# raise Exception('')
|
||||
return super(TestButton, self).on_touch_down(touch)
|
||||
|
||||
s = Sandbox()
|
||||
with s:
|
||||
Builder.load_string('''
|
||||
<TestButton>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: (.3, .2, 0) if self.state == 'normal' else (.7, .7, 0)
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
Color:
|
||||
rgb: 1, 1, 1
|
||||
Rectangle:
|
||||
size: self.texture_size
|
||||
pos: self.center_x - self.texture_size[0] / 2.,\
|
||||
self.center_y - self.texture_size[1] / 2.
|
||||
texture: self.texture
|
||||
|
||||
# invalid... for testing.
|
||||
# on_touch_up: root.d()
|
||||
# on_touch_down: root.f()
|
||||
on_release: root.args()
|
||||
# on_press: root.args()
|
||||
''')
|
||||
b = TestButton(text='Hello World')
|
||||
s.add_widget(b)
|
||||
|
||||
# this exception is within the "with" block, but will be ignored by
|
||||
# default because the sandbox on_exception will return True
|
||||
raise Exception('hello')
|
||||
|
||||
runTouchApp(s)
|
||||
@@ -0,0 +1,645 @@
|
||||
'''
|
||||
Scatter
|
||||
=======
|
||||
|
||||
.. image:: images/scatter.gif
|
||||
:align: right
|
||||
|
||||
:class:`Scatter` is used to build interactive widgets that can be translated,
|
||||
rotated and scaled with two or more fingers on a multitouch system.
|
||||
|
||||
Scatter has its own matrix transformation: the modelview matrix is changed
|
||||
before the children are drawn and the previous matrix is restored when the
|
||||
drawing is finished. That makes it possible to perform rotation, scaling and
|
||||
translation over the entire children tree without changing any widget
|
||||
properties. That specific behavior makes the scatter unique, but there are some
|
||||
advantages / constraints that you should consider:
|
||||
|
||||
#. The children are positioned relative to the scatter similarly to a
|
||||
:mod:`~kivy.uix.relativelayout.RelativeLayout`. So when dragging the
|
||||
scatter, the position of the children don't change, only the position of
|
||||
the scatter does.
|
||||
#. The scatter size has no impact on the size of its children.
|
||||
#. If you want to resize the scatter, use scale, not size (read #2). Scale
|
||||
transforms both the scatter and its children, but does not change size.
|
||||
#. The scatter is not a layout. You must manage the size of the children
|
||||
yourself.
|
||||
|
||||
For touch events, the scatter converts from the parent matrix to the scatter
|
||||
matrix automatically in on_touch_down/move/up events. If you are doing things
|
||||
manually, you will need to use :meth:`~kivy.uix.widget.Widget.to_parent` and
|
||||
:meth:`~kivy.uix.widget.Widget.to_local`.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
By default, the Scatter does not have a graphical representation: it is a
|
||||
container only. The idea is to combine the Scatter with another widget, for
|
||||
example an :class:`~kivy.uix.image.Image`::
|
||||
|
||||
scatter = Scatter()
|
||||
image = Image(source='sun.jpg')
|
||||
scatter.add_widget(image)
|
||||
|
||||
Control Interactions
|
||||
--------------------
|
||||
|
||||
By default, all interactions are enabled. You can selectively disable
|
||||
them using the do_rotation, do_translation and do_scale properties.
|
||||
|
||||
Disable rotation::
|
||||
|
||||
scatter = Scatter(do_rotation=False)
|
||||
|
||||
Allow only translation::
|
||||
|
||||
scatter = Scatter(do_rotation=False, do_scale=False)
|
||||
|
||||
Allow only translation on x axis::
|
||||
|
||||
scatter = Scatter(do_rotation=False, do_scale=False,
|
||||
do_translation_y=False)
|
||||
|
||||
|
||||
Automatic Bring to Front
|
||||
------------------------
|
||||
|
||||
If the :attr:`Scatter.auto_bring_to_front` property is True, the scatter
|
||||
widget will be removed and re-added to the parent when it is touched
|
||||
(brought to front, above all other widgets in the parent). This is useful
|
||||
when you are manipulating several scatter widgets and don't want the active
|
||||
one to be partially hidden.
|
||||
|
||||
Scale Limitation
|
||||
----------------
|
||||
|
||||
We are using a 32-bit matrix in double representation. That means we have
|
||||
a limit for scaling. You cannot do infinite scaling down/up with our
|
||||
implementation. Generally, you don't hit the minimum scale (because you don't
|
||||
see it on the screen), but the maximum scale is 9.99506983235e+19 (2^66).
|
||||
|
||||
You can also limit the minimum and maximum scale allowed::
|
||||
|
||||
scatter = Scatter(scale_min=.5, scale_max=3.)
|
||||
|
||||
Behavior
|
||||
--------
|
||||
|
||||
.. versionchanged:: 1.1.0
|
||||
If no control interactions are enabled, then the touch handler will never
|
||||
return True.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Scatter', 'ScatterPlane')
|
||||
|
||||
from math import radians
|
||||
from kivy.properties import BooleanProperty, AliasProperty, \
|
||||
NumericProperty, ObjectProperty, BoundedNumericProperty
|
||||
from kivy.vector import Vector
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.graphics.transformation import Matrix
|
||||
|
||||
|
||||
class Scatter(Widget):
|
||||
'''Scatter class. See module documentation for more information.
|
||||
|
||||
:Events:
|
||||
`on_transform_with_touch`:
|
||||
Fired when the scatter has been transformed by user touch
|
||||
or multitouch, such as panning or zooming.
|
||||
`on_bring_to_front`:
|
||||
Fired when the scatter is brought to the front.
|
||||
|
||||
.. versionchanged:: 1.9.0
|
||||
Event `on_bring_to_front` added.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
Event `on_transform_with_touch` added.
|
||||
'''
|
||||
|
||||
__events__ = ('on_transform_with_touch', 'on_bring_to_front')
|
||||
|
||||
auto_bring_to_front = BooleanProperty(True)
|
||||
'''If True, the widget will be automatically pushed on the top of parent
|
||||
widget list for drawing.
|
||||
|
||||
:attr:`auto_bring_to_front` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
do_translation_x = BooleanProperty(True)
|
||||
'''Allow translation on the X axis.
|
||||
|
||||
:attr:`do_translation_x` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
do_translation_y = BooleanProperty(True)
|
||||
'''Allow translation on Y axis.
|
||||
|
||||
:attr:`do_translation_y` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
def _get_do_translation(self):
|
||||
return (self.do_translation_x, self.do_translation_y)
|
||||
|
||||
def _set_do_translation(self, value):
|
||||
if type(value) in (list, tuple):
|
||||
self.do_translation_x, self.do_translation_y = value
|
||||
else:
|
||||
self.do_translation_x = self.do_translation_y = bool(value)
|
||||
|
||||
do_translation = AliasProperty(_get_do_translation, _set_do_translation,
|
||||
bind=('do_translation_x',
|
||||
'do_translation_y'),
|
||||
cache=True)
|
||||
'''Allow translation on the X or Y axis.
|
||||
|
||||
:attr:`do_translation` is an :class:`~kivy.properties.AliasProperty` of
|
||||
(:attr:`do_translation_x` + :attr:`do_translation_y`)
|
||||
'''
|
||||
|
||||
translation_touches = BoundedNumericProperty(1, min=1)
|
||||
'''Determine whether translation was triggered by a single or multiple
|
||||
touches. This only has effect when :attr:`do_translation` = True.
|
||||
|
||||
:attr:`translation_touches` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 1.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
'''
|
||||
|
||||
do_rotation = BooleanProperty(True)
|
||||
'''Allow rotation.
|
||||
|
||||
:attr:`do_rotation` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
do_scale = BooleanProperty(True)
|
||||
'''Allow scaling.
|
||||
|
||||
:attr:`do_scale` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
do_collide_after_children = BooleanProperty(False)
|
||||
'''If True, the collision detection for limiting the touch inside the
|
||||
scatter will be done after dispaching the touch to the children.
|
||||
You can put children outside the bounding box of the scatter and still be
|
||||
able to touch them.
|
||||
|
||||
:attr:`do_collide_after_children` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to False.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
'''
|
||||
|
||||
scale_min = NumericProperty(0.01)
|
||||
'''Minimum scaling factor allowed.
|
||||
|
||||
:attr:`scale_min` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.01.
|
||||
'''
|
||||
|
||||
scale_max = NumericProperty(1e20)
|
||||
'''Maximum scaling factor allowed.
|
||||
|
||||
:attr:`scale_max` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1e20.
|
||||
'''
|
||||
|
||||
transform = ObjectProperty(Matrix())
|
||||
'''Transformation matrix.
|
||||
|
||||
:attr:`transform` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to the identity matrix.
|
||||
|
||||
.. note::
|
||||
|
||||
This matrix reflects the current state of the transformation matrix
|
||||
but setting it directly will erase previously applied
|
||||
transformations. To apply a transformation considering context,
|
||||
please use the :attr:`~Scatter.apply_transform` method.
|
||||
|
||||
'''
|
||||
|
||||
transform_inv = ObjectProperty(Matrix())
|
||||
'''Inverse of the transformation matrix.
|
||||
|
||||
:attr:`transform_inv` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to the identity matrix.
|
||||
'''
|
||||
|
||||
def _get_bbox(self):
|
||||
xmin, ymin = xmax, ymax = self.to_parent(0, 0)
|
||||
for point in [(self.width, 0), (0, self.height), self.size]:
|
||||
x, y = self.to_parent(*point)
|
||||
if x < xmin:
|
||||
xmin = x
|
||||
if y < ymin:
|
||||
ymin = y
|
||||
if x > xmax:
|
||||
xmax = x
|
||||
if y > ymax:
|
||||
ymax = y
|
||||
return (xmin, ymin), (xmax - xmin, ymax - ymin)
|
||||
|
||||
bbox = AliasProperty(_get_bbox, bind=('transform', 'width', 'height'))
|
||||
'''Bounding box of the widget in parent space::
|
||||
|
||||
((x, y), (w, h))
|
||||
# x, y = lower left corner
|
||||
|
||||
:attr:`bbox` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
def _get_rotation(self):
|
||||
v1 = Vector(0, 10)
|
||||
tp = self.to_parent
|
||||
v2 = Vector(*tp(*self.pos)) - tp(self.x, self.y + 10)
|
||||
return -1.0 * (v1.angle(v2) + 180) % 360
|
||||
|
||||
def _set_rotation(self, rotation):
|
||||
angle_change = self.rotation - rotation
|
||||
r = Matrix().rotate(-radians(angle_change), 0, 0, 1)
|
||||
self.apply_transform(r, post_multiply=True,
|
||||
anchor=self.to_local(*self.center))
|
||||
|
||||
rotation = AliasProperty(_get_rotation, _set_rotation,
|
||||
bind=('x', 'y', 'transform'))
|
||||
'''Rotation value of the scatter in degrees moving in a counterclockwise
|
||||
direction.
|
||||
|
||||
:attr:`rotation` is an :class:`~kivy.properties.AliasProperty` and defaults
|
||||
to 0.0.
|
||||
'''
|
||||
|
||||
def _get_scale(self):
|
||||
p1 = Vector(*self.to_parent(0, 0))
|
||||
p2 = Vector(*self.to_parent(1, 0))
|
||||
scale = p1.distance(p2)
|
||||
|
||||
# XXX float calculation are not accurate, and then, scale can be
|
||||
# thrown again even with only the position change. So to
|
||||
# prevent anything wrong with scale, just avoid to dispatch it
|
||||
# if the scale "visually" didn't change. #947
|
||||
# Remove this ugly hack when we'll be Python 3 only.
|
||||
if hasattr(self, '_scale_p'):
|
||||
if str(scale) == str(self._scale_p):
|
||||
return self._scale_p
|
||||
|
||||
self._scale_p = scale
|
||||
return scale
|
||||
|
||||
def _set_scale(self, scale):
|
||||
rescale = scale * 1.0 / self.scale
|
||||
self.apply_transform(Matrix().scale(rescale, rescale, rescale),
|
||||
post_multiply=True,
|
||||
anchor=self.to_local(*self.center))
|
||||
|
||||
scale = AliasProperty(_get_scale, _set_scale, bind=('x', 'y', 'transform'))
|
||||
'''Scale value of the scatter.
|
||||
|
||||
:attr:`scale` is an :class:`~kivy.properties.AliasProperty` and defaults to
|
||||
1.0.
|
||||
'''
|
||||
|
||||
def _get_center(self):
|
||||
return (self.bbox[0][0] + self.bbox[1][0] / 2.0,
|
||||
self.bbox[0][1] + self.bbox[1][1] / 2.0)
|
||||
|
||||
def _set_center(self, center):
|
||||
if center == self.center:
|
||||
return False
|
||||
t = Vector(*center) - self.center
|
||||
trans = Matrix().translate(t.x, t.y, 0)
|
||||
self.apply_transform(trans)
|
||||
|
||||
center = AliasProperty(_get_center, _set_center, bind=('bbox',))
|
||||
|
||||
def _get_pos(self):
|
||||
return self.bbox[0]
|
||||
|
||||
def _set_pos(self, pos):
|
||||
_pos = self.bbox[0]
|
||||
if pos == _pos:
|
||||
return
|
||||
t = Vector(*pos) - _pos
|
||||
trans = Matrix().translate(t.x, t.y, 0)
|
||||
self.apply_transform(trans)
|
||||
|
||||
pos = AliasProperty(_get_pos, _set_pos, bind=('bbox',))
|
||||
|
||||
def _get_x(self):
|
||||
return self.bbox[0][0]
|
||||
|
||||
def _set_x(self, x):
|
||||
if x == self.bbox[0][0]:
|
||||
return False
|
||||
self.pos = (x, self.y)
|
||||
return True
|
||||
|
||||
x = AliasProperty(_get_x, _set_x, bind=('bbox',))
|
||||
|
||||
def _get_y(self):
|
||||
return self.bbox[0][1]
|
||||
|
||||
def _set_y(self, y):
|
||||
if y == self.bbox[0][1]:
|
||||
return False
|
||||
self.pos = (self.x, y)
|
||||
return True
|
||||
|
||||
y = AliasProperty(_get_y, _set_y, bind=('bbox',))
|
||||
|
||||
def get_right(self):
|
||||
return self.x + self.bbox[1][0]
|
||||
|
||||
def set_right(self, value):
|
||||
self.x = value - self.bbox[1][0]
|
||||
|
||||
right = AliasProperty(get_right, set_right, bind=('x', 'bbox'))
|
||||
|
||||
def get_top(self):
|
||||
return self.y + self.bbox[1][1]
|
||||
|
||||
def set_top(self, value):
|
||||
self.y = value - self.bbox[1][1]
|
||||
|
||||
top = AliasProperty(get_top, set_top, bind=('y', 'bbox'))
|
||||
|
||||
def get_center_x(self):
|
||||
return self.x + self.bbox[1][0] / 2.
|
||||
|
||||
def set_center_x(self, value):
|
||||
self.x = value - self.bbox[1][0] / 2.
|
||||
|
||||
center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'bbox'))
|
||||
|
||||
def get_center_y(self):
|
||||
return self.y + self.bbox[1][1] / 2.
|
||||
|
||||
def set_center_y(self, value):
|
||||
self.y = value - self.bbox[1][1] / 2.
|
||||
|
||||
center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'bbox'))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._touches = []
|
||||
self._last_touch_pos = {}
|
||||
super(Scatter, self).__init__(**kwargs)
|
||||
|
||||
def on_transform(self, instance, value):
|
||||
self.transform_inv = value.inverse()
|
||||
|
||||
def collide_point(self, x, y):
|
||||
x, y = self.to_local(x, y)
|
||||
return 0 <= x <= self.width and 0 <= y <= self.height
|
||||
|
||||
def to_parent(self, x, y, **k):
|
||||
p = self.transform.transform_point(x, y, 0)
|
||||
return (p[0], p[1])
|
||||
|
||||
def to_local(self, x, y, **k):
|
||||
p = self.transform_inv.transform_point(x, y, 0)
|
||||
return (p[0], p[1])
|
||||
|
||||
def _apply_transform(self, m, pos=None):
|
||||
m = self.transform.multiply(m)
|
||||
return super(Scatter, self)._apply_transform(m, (0, 0))
|
||||
|
||||
def apply_transform(self, trans, post_multiply=False, anchor=(0, 0)):
|
||||
'''
|
||||
Transforms the scatter by applying the "trans" transformation
|
||||
matrix (on top of its current transformation state). The resultant
|
||||
matrix can be found in the :attr:`~Scatter.transform` property.
|
||||
|
||||
:Parameters:
|
||||
`trans`: :class:`~kivy.graphics.transformation.Matrix`.
|
||||
Transformation matrix to be applied to the scatter widget.
|
||||
`anchor`: tuple, defaults to (0, 0).
|
||||
The point to use as the origin of the transformation
|
||||
(uses local widget space).
|
||||
`post_multiply`: bool, defaults to False.
|
||||
If True, the transform matrix is post multiplied
|
||||
(as if applied before the current transform).
|
||||
|
||||
Usage example::
|
||||
|
||||
from kivy.graphics.transformation import Matrix
|
||||
mat = Matrix().scale(3, 3, 3)
|
||||
scatter_instance.apply_transform(mat)
|
||||
|
||||
'''
|
||||
t = Matrix().translate(anchor[0], anchor[1], 0)
|
||||
t = t.multiply(trans)
|
||||
t = t.multiply(Matrix().translate(-anchor[0], -anchor[1], 0))
|
||||
|
||||
if post_multiply:
|
||||
self.transform = self.transform.multiply(t)
|
||||
else:
|
||||
self.transform = t.multiply(self.transform)
|
||||
|
||||
def transform_with_touch(self, touch):
|
||||
# just do a simple one finger drag
|
||||
changed = False
|
||||
if len(self._touches) == self.translation_touches:
|
||||
# _last_touch_pos has last pos in correct parent space,
|
||||
# just like incoming touch
|
||||
dx = (touch.x - self._last_touch_pos[touch][0]) \
|
||||
* self.do_translation_x
|
||||
dy = (touch.y - self._last_touch_pos[touch][1]) \
|
||||
* self.do_translation_y
|
||||
dx = dx / self.translation_touches
|
||||
dy = dy / self.translation_touches
|
||||
self.apply_transform(Matrix().translate(dx, dy, 0))
|
||||
changed = True
|
||||
|
||||
if len(self._touches) == 1:
|
||||
return changed
|
||||
|
||||
# we have more than one touch... list of last known pos
|
||||
points = [Vector(self._last_touch_pos[t]) for t in self._touches
|
||||
if t is not touch]
|
||||
# add current touch last
|
||||
points.append(Vector(touch.pos))
|
||||
|
||||
# we only want to transform if the touch is part of the two touches
|
||||
# farthest apart! So first we find anchor, the point to transform
|
||||
# around as another touch farthest away from current touch's pos
|
||||
anchor = max(points[:-1], key=lambda p: p.distance(touch.pos))
|
||||
|
||||
# now we find the touch farthest away from anchor, if its not the
|
||||
# same as touch. Touch is not one of the two touches used to transform
|
||||
farthest = max(points, key=anchor.distance)
|
||||
if farthest is not points[-1]:
|
||||
return changed
|
||||
|
||||
# ok, so we have touch, and anchor, so we can actually compute the
|
||||
# transformation
|
||||
old_line = Vector(*touch.ppos) - anchor
|
||||
new_line = Vector(*touch.pos) - anchor
|
||||
if not old_line.length(): # div by zero
|
||||
return changed
|
||||
|
||||
angle = radians(new_line.angle(old_line)) * self.do_rotation
|
||||
if angle:
|
||||
changed = True
|
||||
self.apply_transform(Matrix().rotate(angle, 0, 0, 1), anchor=anchor)
|
||||
|
||||
if self.do_scale:
|
||||
scale = new_line.length() / old_line.length()
|
||||
new_scale = scale * self.scale
|
||||
if new_scale < self.scale_min:
|
||||
scale = self.scale_min / self.scale
|
||||
elif new_scale > self.scale_max:
|
||||
scale = self.scale_max / self.scale
|
||||
self.apply_transform(Matrix().scale(scale, scale, scale),
|
||||
anchor=anchor)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
def _bring_to_front(self, touch):
|
||||
# auto bring to front
|
||||
if self.auto_bring_to_front and self.parent:
|
||||
parent = self.parent
|
||||
if parent.children[0] is self:
|
||||
return
|
||||
parent.remove_widget(self)
|
||||
parent.add_widget(self)
|
||||
self.dispatch('on_bring_to_front', touch)
|
||||
|
||||
def on_motion(self, etype, me):
|
||||
if me.type_id in self.motion_filter and 'pos' in me.profile:
|
||||
me.push()
|
||||
me.apply_transform_2d(self.to_local)
|
||||
ret = super().on_motion(etype, me)
|
||||
me.pop()
|
||||
return ret
|
||||
return super().on_motion(etype, me)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
x, y = touch.x, touch.y
|
||||
|
||||
# if the touch isn't on the widget we do nothing
|
||||
if not self.do_collide_after_children:
|
||||
if not self.collide_point(x, y):
|
||||
return False
|
||||
|
||||
# let the child widgets handle the event if they want
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.to_local)
|
||||
if super(Scatter, self).on_touch_down(touch):
|
||||
touch.pop()
|
||||
self._bring_to_front(touch)
|
||||
return True
|
||||
touch.pop()
|
||||
|
||||
# if our child didn't do anything, and if we don't have any active
|
||||
# interaction control, then don't accept the touch.
|
||||
if not self.do_translation_x and \
|
||||
not self.do_translation_y and \
|
||||
not self.do_rotation and \
|
||||
not self.do_scale:
|
||||
return False
|
||||
|
||||
if self.do_collide_after_children:
|
||||
if not self.collide_point(x, y):
|
||||
return False
|
||||
|
||||
if 'multitouch_sim' in touch.profile:
|
||||
touch.multitouch_sim = True
|
||||
# grab the touch so we get all it later move events for sure
|
||||
self._bring_to_front(touch)
|
||||
touch.grab(self)
|
||||
self._touches.append(touch)
|
||||
self._last_touch_pos[touch] = touch.pos
|
||||
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
x, y = touch.x, touch.y
|
||||
# let the child widgets handle the event if they want
|
||||
if self.collide_point(x, y) and not touch.grab_current == self:
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.to_local)
|
||||
if super(Scatter, self).on_touch_move(touch):
|
||||
touch.pop()
|
||||
return True
|
||||
touch.pop()
|
||||
|
||||
# rotate/scale/translate
|
||||
if touch in self._touches and touch.grab_current == self:
|
||||
if self.transform_with_touch(touch):
|
||||
self.dispatch('on_transform_with_touch', touch)
|
||||
self._last_touch_pos[touch] = touch.pos
|
||||
|
||||
# stop propagating if its within our bounds
|
||||
if self.collide_point(x, y):
|
||||
return True
|
||||
|
||||
def on_transform_with_touch(self, touch):
|
||||
'''
|
||||
Called when a touch event has transformed the scatter widget.
|
||||
By default this does nothing, but can be overridden by derived
|
||||
classes that need to react to transformations caused by user
|
||||
input.
|
||||
|
||||
:Parameters:
|
||||
`touch`:
|
||||
The touch object which triggered the transformation.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
pass
|
||||
|
||||
def on_bring_to_front(self, touch):
|
||||
'''
|
||||
Called when a touch event causes the scatter to be brought to the
|
||||
front of the parent (only if :attr:`auto_bring_to_front` is True)
|
||||
|
||||
:Parameters:
|
||||
`touch`:
|
||||
The touch object which brought the scatter to front.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
pass
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
x, y = touch.x, touch.y
|
||||
# if the touch isn't on the widget we do nothing, just try children
|
||||
if not touch.grab_current == self:
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.to_local)
|
||||
if super(Scatter, self).on_touch_up(touch):
|
||||
touch.pop()
|
||||
return True
|
||||
touch.pop()
|
||||
|
||||
# remove it from our saved touches
|
||||
if touch in self._touches and touch.grab_state:
|
||||
touch.ungrab(self)
|
||||
del self._last_touch_pos[touch]
|
||||
self._touches.remove(touch)
|
||||
|
||||
# stop propagating if its within our bounds
|
||||
if self.collide_point(x, y):
|
||||
return True
|
||||
|
||||
|
||||
class ScatterPlane(Scatter):
|
||||
'''This is essentially an unbounded Scatter widget. It's a convenience
|
||||
class to make it easier to handle infinite planes.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if 'auto_bring_to_front' not in kwargs:
|
||||
self.auto_bring_to_front = False
|
||||
super(ScatterPlane, self).__init__(**kwargs)
|
||||
|
||||
def collide_point(self, x, y):
|
||||
return True
|
||||
@@ -0,0 +1,93 @@
|
||||
'''
|
||||
Scatter Layout
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
This layout behaves just like a
|
||||
:class:`~kivy.uix.relativelayout.RelativeLayout`.
|
||||
When a widget is added with position = (0,0) to a :class:`ScatterLayout`,
|
||||
the child widget will also move when you change the position of the
|
||||
:class:`ScatterLayout`. The child widget's coordinates remain
|
||||
(0,0) as they are relative to the parent layout.
|
||||
|
||||
However, since :class:`ScatterLayout` is implemented using a
|
||||
:class:`~kivy.uix.scatter.Scatter`
|
||||
widget, you can also translate, rotate and scale the layout using touches
|
||||
or clicks, just like in the case of a normal Scatter widget, and the child
|
||||
widgets will behave as expected.
|
||||
|
||||
In contrast to a Scatter, the Layout favors 'hint' properties, such as
|
||||
size_hint, size_hint_x, size_hint_y and pos_hint.
|
||||
|
||||
.. note::
|
||||
|
||||
The :class:`ScatterLayout` is implemented as a
|
||||
:class:`~kivy.uix.floatlayout.FloatLayout`
|
||||
inside a :class:`~kivy.uix.scatter.Scatter`.
|
||||
|
||||
.. warning::
|
||||
|
||||
Since the actual :class:`ScatterLayout` is a
|
||||
:class:`~kivy.uix.scatter.Scatter`, its
|
||||
add_widget and remove_widget functions are overridden to add children
|
||||
to the embedded :class:`~kivy.uix.floatlayout.FloatLayout` (accessible as
|
||||
the `content` property of :class:`~kivy.uix.scatter.Scatter`)
|
||||
automatically. So if you want to access the added child elements,
|
||||
you need self.content.children instead of self.children.
|
||||
|
||||
.. warning::
|
||||
|
||||
The :class:`ScatterLayout` was introduced in 1.7.0 and was called
|
||||
:class:`~kivy.uix.relativelayout.RelativeLayout` in prior versions.
|
||||
The :class:`~kivy.uix.relativelayout.RelativeLayout` is now an optimized
|
||||
implementation that uses only a positional transform to avoid some of the
|
||||
heavier calculation involved for :class:`~kivy.uix.scatter.Scatter`.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ScatterLayout', 'ScatterPlaneLayout')
|
||||
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.scatter import Scatter, ScatterPlane
|
||||
from kivy.properties import ObjectProperty
|
||||
|
||||
|
||||
class ScatterLayout(Scatter):
|
||||
'''ScatterLayout class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
content = ObjectProperty()
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.content = FloatLayout()
|
||||
super(ScatterLayout, self).__init__(**kw)
|
||||
if self.content.size != self.size:
|
||||
self.content.size = self.size
|
||||
super(ScatterLayout, self).add_widget(self.content)
|
||||
self.fbind('size', self.update_size)
|
||||
|
||||
def update_size(self, instance, size):
|
||||
self.content.size = size
|
||||
|
||||
def add_widget(self, *args, **kwargs):
|
||||
self.content.add_widget(*args, **kwargs)
|
||||
|
||||
def remove_widget(self, *args, **kwargs):
|
||||
self.content.remove_widget(*args, **kwargs)
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
self.content.clear_widgets(*args, **kwargs)
|
||||
|
||||
|
||||
class ScatterPlaneLayout(ScatterPlane):
|
||||
'''ScatterPlaneLayout class, see module documentation for more information.
|
||||
|
||||
Similar to ScatterLayout, but based on ScatterPlane - so the input is not
|
||||
bounded.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
def collide_point(self, x, y):
|
||||
return True
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
Slider
|
||||
======
|
||||
|
||||
.. image:: images/slider.jpg
|
||||
|
||||
The :class:`Slider` widget looks like a scrollbar. It supports horizontal and
|
||||
vertical orientations, min/max values and a default value.
|
||||
|
||||
To create a slider from -100 to 100 starting from 25::
|
||||
|
||||
from kivy.uix.slider import Slider
|
||||
s = Slider(min=-100, max=100, value=25)
|
||||
|
||||
To create a vertical slider::
|
||||
|
||||
from kivy.uix.slider import Slider
|
||||
s = Slider(orientation='vertical')
|
||||
|
||||
To create a slider with a red line tracking the value::
|
||||
|
||||
from kivy.uix.slider import Slider
|
||||
s = Slider(value_track=True, value_track_color=[1, 0, 0, 1])
|
||||
|
||||
|
||||
Kv Example::
|
||||
|
||||
BoxLayout:
|
||||
Slider:
|
||||
id: slider
|
||||
min: 0
|
||||
max: 100
|
||||
step: 1
|
||||
orientation: 'vertical'
|
||||
|
||||
Label:
|
||||
text: str(slider.value)
|
||||
|
||||
"""
|
||||
__all__ = ('Slider', )
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.properties import (NumericProperty, AliasProperty, OptionProperty,
|
||||
ReferenceListProperty, BoundedNumericProperty,
|
||||
StringProperty, ListProperty, BooleanProperty,
|
||||
ColorProperty)
|
||||
|
||||
|
||||
class Slider(Widget):
|
||||
"""Class for creating a Slider widget.
|
||||
|
||||
Check module documentation for more details.
|
||||
"""
|
||||
|
||||
value = NumericProperty(0.)
|
||||
'''Current value used for the slider.
|
||||
|
||||
:attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 0.'''
|
||||
|
||||
min = NumericProperty(0.)
|
||||
'''Minimum value allowed for :attr:`value`.
|
||||
|
||||
:attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
0.'''
|
||||
|
||||
max = NumericProperty(100.)
|
||||
'''Maximum value allowed for :attr:`value`.
|
||||
|
||||
:attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
100.'''
|
||||
|
||||
padding = NumericProperty('16sp')
|
||||
'''Padding of the slider. The padding is used for graphical representation
|
||||
and interaction. It prevents the cursor from going out of the bounds of the
|
||||
slider bounding box.
|
||||
|
||||
By default, padding is 16sp. The range of the slider is reduced from
|
||||
padding \\*2 on the screen. It allows drawing the default cursor of 32sp
|
||||
width without having the cursor go out of the widget.
|
||||
|
||||
:attr:`padding` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 16sp.'''
|
||||
|
||||
orientation = OptionProperty('horizontal', options=(
|
||||
'vertical', 'horizontal'))
|
||||
'''Orientation of the slider.
|
||||
|
||||
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'horizontal'. Can take a value of 'vertical' or 'horizontal'.
|
||||
'''
|
||||
|
||||
range = ReferenceListProperty(min, max)
|
||||
'''Range of the slider in the format (minimum value, maximum value)::
|
||||
|
||||
>>> slider = Slider(min=10, max=80)
|
||||
>>> slider.range
|
||||
[10, 80]
|
||||
>>> slider.range = (20, 100)
|
||||
>>> slider.min
|
||||
20
|
||||
>>> slider.max
|
||||
100
|
||||
|
||||
:attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of
|
||||
(:attr:`min`, :attr:`max`) properties.
|
||||
'''
|
||||
|
||||
step = BoundedNumericProperty(0, min=0)
|
||||
'''Step size of the slider.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
Determines the size of each interval or step the slider takes between
|
||||
:attr:`min` and :attr:`max`. If the value range can't be evenly
|
||||
divisible by step the last step will be capped by slider.max.
|
||||
A zero value will result in the smallest possible intervals/steps,
|
||||
calculated from the (pixel) position of the slider.
|
||||
|
||||
:attr:`step` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 0.'''
|
||||
|
||||
background_horizontal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/sliderh_background')
|
||||
"""Background of the slider used in the horizontal orientation.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`background_horizontal` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `atlas://data/images/defaulttheme/sliderh_background`.
|
||||
"""
|
||||
|
||||
background_disabled_horizontal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/sliderh_background_disabled')
|
||||
"""Background of the disabled slider used in the horizontal orientation.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`background_disabled_horizontal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
`atlas://data/images/defaulttheme/sliderh_background_disabled`.
|
||||
"""
|
||||
|
||||
background_vertical = StringProperty(
|
||||
'atlas://data/images/defaulttheme/sliderv_background')
|
||||
"""Background of the slider used in the vertical orientation.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`background_vertical` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `atlas://data/images/defaulttheme/sliderv_background`.
|
||||
"""
|
||||
|
||||
background_disabled_vertical = StringProperty(
|
||||
'atlas://data/images/defaulttheme/sliderv_background_disabled')
|
||||
"""Background of the disabled slider used in the vertical orientation.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`background_disabled_vertical` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
`atlas://data/images/defaulttheme/sliderv_background_disabled`.
|
||||
"""
|
||||
|
||||
background_width = NumericProperty('36sp')
|
||||
"""Slider's background's width (thickness), used in both horizontal
|
||||
and vertical orientations.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`background_width` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 36sp.
|
||||
"""
|
||||
|
||||
cursor_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/slider_cursor')
|
||||
"""Path of the image used to draw the slider cursor.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`cursor_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `atlas://data/images/defaulttheme/slider_cursor`.
|
||||
"""
|
||||
|
||||
cursor_disabled_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/slider_cursor_disabled')
|
||||
"""Path of the image used to draw the disabled slider cursor.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`cursor_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `atlas://data/images/defaulttheme/slider_cursor_disabled`.
|
||||
"""
|
||||
|
||||
cursor_width = NumericProperty('32sp')
|
||||
"""Width of the cursor image.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`cursor_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 32sp.
|
||||
"""
|
||||
|
||||
cursor_height = NumericProperty('32sp')
|
||||
"""Height of the cursor image.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`cursor_height` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 32sp.
|
||||
"""
|
||||
|
||||
cursor_size = ReferenceListProperty(cursor_width, cursor_height)
|
||||
"""Size of the cursor image.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`cursor_size` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`cursor_width`, :attr:`cursor_height`) properties.
|
||||
"""
|
||||
|
||||
border_horizontal = ListProperty([0, 18, 0, 18])
|
||||
"""Border used to draw the slider background in horizontal orientation.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty`
|
||||
and defaults to [0, 18, 0, 18].
|
||||
"""
|
||||
|
||||
border_vertical = ListProperty([18, 0, 18, 0])
|
||||
"""Border used to draw the slider background in vertical orientation.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty`
|
||||
and defaults to [18, 0, 18, 0].
|
||||
"""
|
||||
|
||||
value_track = BooleanProperty(False)
|
||||
"""Decides if slider should draw the line indicating the
|
||||
space between :attr:`min` and :attr:`value` properties values.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`value_track` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
"""
|
||||
|
||||
value_track_color = ColorProperty([1, 1, 1, 1])
|
||||
"""Color of the :attr:`value_line` in rgba format.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`value_track_color` is a :class:`~kivy.properties.ColorProperty`
|
||||
and defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
"""
|
||||
|
||||
value_track_width = NumericProperty('3dp')
|
||||
"""Width of the track line.
|
||||
|
||||
.. versionadded 1.10.0
|
||||
|
||||
:attr:`value_track_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 3dp.
|
||||
"""
|
||||
|
||||
sensitivity = OptionProperty('all', options=('all', 'handle'))
|
||||
"""Whether the touch collides with the whole body of the widget
|
||||
or with the slider handle part only.
|
||||
|
||||
.. versionadded:: 1.10.1
|
||||
|
||||
:attr:`sensitivity` is a :class:`~kivy.properties.OptionProperty`
|
||||
and defaults to 'all'. Can take a value of 'all' or 'handle'.
|
||||
"""
|
||||
|
||||
# The following two methods constrain the slider's value
|
||||
# to range(min,max). Otherwise it may happen that self.value < self.min
|
||||
# at init.
|
||||
|
||||
def on_min(self, *largs):
|
||||
self.value = min(self.max, max(self.min, self.value))
|
||||
|
||||
def on_max(self, *largs):
|
||||
self.value = min(self.max, max(self.min, self.value))
|
||||
|
||||
def get_norm_value(self):
|
||||
vmin = self.min
|
||||
d = self.max - vmin
|
||||
if d == 0:
|
||||
return 0
|
||||
return (self.value - vmin) / float(d)
|
||||
|
||||
def set_norm_value(self, value):
|
||||
vmin = self.min
|
||||
vmax = self.max
|
||||
step = self.step
|
||||
val = min(value * (vmax - vmin) + vmin, vmax)
|
||||
if step == 0:
|
||||
self.value = val
|
||||
else:
|
||||
self.value = min(round((val - vmin) / step) * step + vmin,
|
||||
vmax)
|
||||
|
||||
value_normalized = AliasProperty(get_norm_value, set_norm_value,
|
||||
bind=('value', 'min', 'max'),
|
||||
cache=True)
|
||||
'''Normalized value inside the :attr:`range` (min/max) to 0-1 range::
|
||||
|
||||
>>> slider = Slider(value=50, min=0, max=100)
|
||||
>>> slider.value
|
||||
50
|
||||
>>> slider.value_normalized
|
||||
0.5
|
||||
>>> slider.value = 0
|
||||
>>> slider.value_normalized
|
||||
0
|
||||
>>> slider.value = 100
|
||||
>>> slider.value_normalized
|
||||
1
|
||||
|
||||
You can also use it for setting the real value without knowing the minimum
|
||||
and maximum::
|
||||
|
||||
>>> slider = Slider(min=0, max=200)
|
||||
>>> slider.value_normalized = .5
|
||||
>>> slider.value
|
||||
100
|
||||
>>> slider.value_normalized = 1.
|
||||
>>> slider.value
|
||||
200
|
||||
|
||||
:attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
def get_value_pos(self):
|
||||
padding = self.padding
|
||||
x = self.x
|
||||
y = self.y
|
||||
nval = self.value_normalized
|
||||
if self.orientation == 'horizontal':
|
||||
return (x + padding + nval * (self.width - 2 * padding), y)
|
||||
else:
|
||||
return (x, y + padding + nval * (self.height - 2 * padding))
|
||||
|
||||
def set_value_pos(self, pos):
|
||||
padding = self.padding
|
||||
x = min(self.right - padding, max(pos[0], self.x + padding))
|
||||
y = min(self.top - padding, max(pos[1], self.y + padding))
|
||||
if self.orientation == 'horizontal':
|
||||
if self.width == 0:
|
||||
self.value_normalized = 0
|
||||
else:
|
||||
self.value_normalized = (x - self.x - padding
|
||||
) / float(self.width - 2 * padding)
|
||||
else:
|
||||
if self.height == 0:
|
||||
self.value_normalized = 0
|
||||
else:
|
||||
self.value_normalized = (y - self.y - padding
|
||||
) / float(self.height - 2 * padding)
|
||||
|
||||
value_pos = AliasProperty(get_value_pos, set_value_pos,
|
||||
bind=('pos', 'size', 'min', 'max', 'padding',
|
||||
'value_normalized', 'orientation'),
|
||||
cache=True)
|
||||
'''Position of the internal cursor, based on the normalized value.
|
||||
|
||||
:attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.disabled or not self.collide_point(*touch.pos):
|
||||
return
|
||||
if touch.is_mouse_scrolling:
|
||||
if 'down' in touch.button or 'left' in touch.button:
|
||||
if self.step:
|
||||
self.value = min(self.max, self.value + self.step)
|
||||
else:
|
||||
self.value = min(
|
||||
self.max,
|
||||
self.value + (self.max - self.min) / 20)
|
||||
if 'up' in touch.button or 'right' in touch.button:
|
||||
if self.step:
|
||||
self.value = max(self.min, self.value - self.step)
|
||||
else:
|
||||
self.value = max(
|
||||
self.min,
|
||||
self.value - (self.max - self.min) / 20)
|
||||
elif self.sensitivity == 'handle':
|
||||
if self.children[0].collide_point(*touch.pos):
|
||||
touch.grab(self)
|
||||
else:
|
||||
touch.grab(self)
|
||||
self.value_pos = touch.pos
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current == self:
|
||||
self.value_pos = touch.pos
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current == self:
|
||||
self.value_pos = touch.pos
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.app import App
|
||||
|
||||
class SliderApp(App):
|
||||
def build(self):
|
||||
return Slider(padding=25)
|
||||
|
||||
SliderApp().run()
|
||||
@@ -0,0 +1,221 @@
|
||||
'''
|
||||
Spinner
|
||||
=======
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
.. image:: images/spinner.jpg
|
||||
:align: right
|
||||
|
||||
Spinner is a widget that provides a quick way to select one value from a set.
|
||||
In the default state, a spinner shows its currently selected value.
|
||||
Touching the spinner displays a dropdown menu with all the other available
|
||||
values from which the user can select a new one.
|
||||
|
||||
Example::
|
||||
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.spinner import Spinner
|
||||
|
||||
spinner = Spinner(
|
||||
# default value shown
|
||||
text='Home',
|
||||
# available values
|
||||
values=('Home', 'Work', 'Other', 'Custom'),
|
||||
# just for positioning in our example
|
||||
size_hint=(None, None),
|
||||
size=(100, 44),
|
||||
pos_hint={'center_x': .5, 'center_y': .5})
|
||||
|
||||
def show_selected_value(spinner, text):
|
||||
print('The spinner', spinner, 'has text', text)
|
||||
|
||||
spinner.bind(text=show_selected_value)
|
||||
|
||||
runTouchApp(spinner)
|
||||
|
||||
|
||||
Kv Example::
|
||||
|
||||
FloatLayout:
|
||||
Spinner:
|
||||
size_hint: None, None
|
||||
size: 100, 44
|
||||
pos_hint: {'center': (.5, .5)}
|
||||
text: 'Home'
|
||||
values: 'Home', 'Work', 'Other', 'Custom'
|
||||
on_text:
|
||||
print("The spinner {} has text {}".format(self, self.text))
|
||||
'''
|
||||
|
||||
__all__ = ('Spinner', 'SpinnerOption')
|
||||
|
||||
from kivy.compat import string_types
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import ListProperty, ObjectProperty, BooleanProperty
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.dropdown import DropDown
|
||||
|
||||
|
||||
class SpinnerOption(Button):
|
||||
'''Special button used in the :class:`Spinner` dropdown list. By default,
|
||||
this is just a :class:`~kivy.uix.button.Button` with a size_hint_y of None
|
||||
and a height of :meth:`48dp <kivy.metrics.dp>`.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class Spinner(Button):
|
||||
'''Spinner class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
values = ListProperty()
|
||||
'''Values that can be selected by the user. It must be a list of strings.
|
||||
|
||||
:attr:`values` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
[].
|
||||
'''
|
||||
|
||||
text_autoupdate = BooleanProperty(False)
|
||||
'''Indicates if the spinner's :attr:`text` should be automatically
|
||||
updated with the first value of the :attr:`values` property.
|
||||
Setting it to True will cause the spinner to update its :attr:`text`
|
||||
property every time attr:`values` are changed.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`text_autoupdate` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
option_cls = ObjectProperty(SpinnerOption)
|
||||
'''Class used to display the options within the dropdown list displayed
|
||||
under the Spinner. The `text` property of the class will be used to
|
||||
represent the value.
|
||||
|
||||
The option class requires:
|
||||
|
||||
- a `text` property, used to display the value.
|
||||
- an `on_release` event, used to trigger the option when pressed/touched.
|
||||
- a :attr:`~kivy.uix.widget.Widget.size_hint_y` of None.
|
||||
- the :attr:`~kivy.uix.widget.Widget.height` to be set.
|
||||
|
||||
:attr:`option_cls` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to :class:`SpinnerOption`.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
If you set a string, the :class:`~kivy.factory.Factory` will be used to
|
||||
resolve the class.
|
||||
|
||||
'''
|
||||
|
||||
dropdown_cls = ObjectProperty(DropDown)
|
||||
'''Class used to display the dropdown list when the Spinner is pressed.
|
||||
|
||||
:attr:`dropdown_cls` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to :class:`~kivy.uix.dropdown.DropDown`.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
If set to a string, the :class:`~kivy.factory.Factory` will be used to
|
||||
resolve the class name.
|
||||
|
||||
'''
|
||||
|
||||
is_open = BooleanProperty(False)
|
||||
'''By default, the spinner is not open. Set to True to open it.
|
||||
|
||||
:attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
'''
|
||||
|
||||
sync_height = BooleanProperty(False)
|
||||
'''Each element in a dropdown list uses a default/user-supplied height.
|
||||
Set to True to propagate the Spinner's height value to each dropdown
|
||||
list element.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`sync_height` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._dropdown = None
|
||||
super(Spinner, self).__init__(**kwargs)
|
||||
fbind = self.fbind
|
||||
build_dropdown = self._build_dropdown
|
||||
fbind('on_release', self._toggle_dropdown)
|
||||
fbind('dropdown_cls', build_dropdown)
|
||||
fbind('option_cls', build_dropdown)
|
||||
fbind('values', self._update_dropdown)
|
||||
fbind('size', self._update_dropdown_size)
|
||||
fbind('text_autoupdate', self._update_dropdown)
|
||||
build_dropdown()
|
||||
|
||||
def _build_dropdown(self, *largs):
|
||||
if self._dropdown:
|
||||
self._dropdown.unbind(on_select=self._on_dropdown_select)
|
||||
self._dropdown.unbind(on_dismiss=self._close_dropdown)
|
||||
self._dropdown.dismiss()
|
||||
self._dropdown = None
|
||||
cls = self.dropdown_cls
|
||||
if isinstance(cls, string_types):
|
||||
cls = Factory.get(cls)
|
||||
self._dropdown = cls()
|
||||
self._dropdown.bind(on_select=self._on_dropdown_select)
|
||||
self._dropdown.bind(on_dismiss=self._close_dropdown)
|
||||
self._update_dropdown()
|
||||
|
||||
def _update_dropdown_size(self, *largs):
|
||||
if not self.sync_height:
|
||||
return
|
||||
dp = self._dropdown
|
||||
if not dp:
|
||||
return
|
||||
|
||||
container = dp.container
|
||||
if not container:
|
||||
return
|
||||
h = self.height
|
||||
for item in container.children[:]:
|
||||
item.height = h
|
||||
|
||||
def _update_dropdown(self, *largs):
|
||||
dp = self._dropdown
|
||||
cls = self.option_cls
|
||||
values = self.values
|
||||
text_autoupdate = self.text_autoupdate
|
||||
if isinstance(cls, string_types):
|
||||
cls = Factory.get(cls)
|
||||
dp.clear_widgets()
|
||||
for value in values:
|
||||
item = cls(text=value)
|
||||
item.height = self.height if self.sync_height else item.height
|
||||
item.bind(on_release=lambda option: dp.select(option.text))
|
||||
dp.add_widget(item)
|
||||
if text_autoupdate:
|
||||
if values:
|
||||
if not self.text or self.text not in values:
|
||||
self.text = values[0]
|
||||
else:
|
||||
self.text = ''
|
||||
|
||||
def _toggle_dropdown(self, *largs):
|
||||
if self.values:
|
||||
self.is_open = not self.is_open
|
||||
|
||||
def _close_dropdown(self, *largs):
|
||||
self.is_open = False
|
||||
|
||||
def _on_dropdown_select(self, instance, data, *largs):
|
||||
self.text = data
|
||||
self.is_open = False
|
||||
|
||||
def on_is_open(self, instance, value):
|
||||
if value:
|
||||
self._dropdown.open(self)
|
||||
else:
|
||||
if self._dropdown.attach_to:
|
||||
self._dropdown.dismiss()
|
||||
@@ -0,0 +1,430 @@
|
||||
'''Splitter
|
||||
======
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. image:: images/splitter.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`Splitter` is a widget that helps you re-size its child
|
||||
widget/layout by letting you re-size it via dragging the boundary or
|
||||
double tapping the boundary. This widget is similar to the
|
||||
:class:`~kivy.uix.scrollview.ScrollView` in that it allows only one
|
||||
child widget.
|
||||
|
||||
Usage::
|
||||
|
||||
splitter = Splitter(sizable_from = 'right')
|
||||
splitter.add_widget(layout_or_widget_instance)
|
||||
splitter.min_size = 100
|
||||
splitter.max_size = 250
|
||||
|
||||
To change the size of the strip/border used for resizing::
|
||||
|
||||
splitter.strip_size = '10pt'
|
||||
|
||||
To change its appearance::
|
||||
|
||||
splitter.strip_cls = your_custom_class
|
||||
|
||||
You can also change the appearance of the `strip_cls`, which defaults to
|
||||
:class:`SplitterStrip`, by overriding the `kv` rule in your app:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<SplitterStrip>:
|
||||
horizontal: True if self.parent and self.parent.sizable_from[0] \
|
||||
in ('t', 'b') else False
|
||||
background_normal: 'path to normal horizontal image' \
|
||||
if self.horizontal else 'path to vertical normal image'
|
||||
background_down: 'path to pressed horizontal image' \
|
||||
if self.horizontal else 'path to vertical pressed image'
|
||||
|
||||
'''
|
||||
|
||||
|
||||
__all__ = ('Splitter', )
|
||||
|
||||
from kivy.factory import Factory
|
||||
from kivy.uix.button import Button
|
||||
from kivy.properties import (OptionProperty, NumericProperty, ObjectProperty,
|
||||
ListProperty, BooleanProperty)
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
|
||||
|
||||
class SplitterStrip(Button):
|
||||
'''Class used for the graphical representation of a
|
||||
:class:`kivy.uix.splitter.SplitterStripe`.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class Splitter(BoxLayout):
|
||||
'''See module documentation.
|
||||
|
||||
:Events:
|
||||
`on_press`:
|
||||
Fired when the splitter is pressed.
|
||||
`on_release`:
|
||||
Fired when the splitter is released.
|
||||
|
||||
.. versionchanged:: 1.6.0
|
||||
Added `on_press` and `on_release` events.
|
||||
|
||||
'''
|
||||
|
||||
border = ListProperty([4, 4, 4, 4])
|
||||
'''Border used for the
|
||||
:class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction.
|
||||
|
||||
This must be a list of four values: (bottom, right, top, left).
|
||||
Read the BorderImage instructions for more information about how
|
||||
to use it.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to (4, 4, 4, 4).
|
||||
'''
|
||||
|
||||
strip_cls = ObjectProperty(SplitterStrip)
|
||||
'''Specifies the class of the resize Strip.
|
||||
|
||||
:attr:`strip_cls` is an :class:`kivy.properties.ObjectProperty` and
|
||||
defaults to :class:`~kivy.uix.splitter.SplitterStrip`, which is of type
|
||||
:class:`~kivy.uix.button.Button`.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
If you set a string, the :class:`~kivy.factory.Factory` will be used to
|
||||
resolve the class.
|
||||
|
||||
'''
|
||||
|
||||
sizable_from = OptionProperty('left', options=(
|
||||
'left', 'right', 'top', 'bottom'))
|
||||
'''Specifies whether the widget is resizable. Options are:
|
||||
`left`, `right`, `top` or `bottom`
|
||||
|
||||
:attr:`sizable_from` is an :class:`~kivy.properties.OptionProperty`
|
||||
and defaults to `left`.
|
||||
'''
|
||||
|
||||
strip_size = NumericProperty('10pt')
|
||||
'''Specifies the size of resize strip
|
||||
|
||||
:attr:`strp_size` is a :class:`~kivy.properties.NumericProperty`
|
||||
defaults to `10pt`
|
||||
'''
|
||||
|
||||
min_size = NumericProperty('100pt')
|
||||
'''Specifies the minimum size beyond which the widget is not resizable.
|
||||
|
||||
:attr:`min_size` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to `100pt`.
|
||||
'''
|
||||
|
||||
max_size = NumericProperty('500pt')
|
||||
'''Specifies the maximum size beyond which the widget is not resizable.
|
||||
|
||||
:attr:`max_size` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `500pt`.
|
||||
'''
|
||||
|
||||
_parent_proportion = NumericProperty(0.)
|
||||
'''(internal) Specifies the distance that the slider has travelled
|
||||
across its parent, used to automatically maintain a sensible
|
||||
position if the parent is resized.
|
||||
|
||||
:attr:`_parent_proportion` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to 0.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
_bound_parent = ObjectProperty(None, allownone=True)
|
||||
'''(internal) References the widget whose size is currently being
|
||||
tracked by :attr:`_parent_proportion`.
|
||||
|
||||
:attr:`_bound_parent` is a
|
||||
:class:`~kivy.properties.ObjectProperty` and defaults to None.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
keep_within_parent = BooleanProperty(False)
|
||||
'''If True, will limit the splitter to stay within its parent widget.
|
||||
|
||||
:attr:`keep_within_parent` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to False.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
rescale_with_parent = BooleanProperty(False)
|
||||
'''If True, will automatically change size to take up the same
|
||||
proportion of the parent widget when it is resized, while
|
||||
staying within :attr:`min_size` and :attr:`max_size`. As long as
|
||||
these attributes can be satisfied, this stops the
|
||||
:class:`Splitter` from exceeding the parent size during rescaling.
|
||||
|
||||
:attr:`rescale_with_parent` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to False.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
__events__ = ('on_press', 'on_release')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._container = None
|
||||
self._strip = None
|
||||
super(Splitter, self).__init__(**kwargs)
|
||||
|
||||
do_size = self._do_size
|
||||
fbind = self.fbind
|
||||
fbind('max_size', do_size)
|
||||
fbind('min_size', do_size)
|
||||
fbind('parent', self._rebind_parent)
|
||||
|
||||
def on_sizable_from(self, instance, sizable_from):
|
||||
if not instance._container:
|
||||
return
|
||||
|
||||
sup = super(Splitter, instance)
|
||||
_strp = instance._strip
|
||||
if _strp:
|
||||
# remove any previous binds
|
||||
_strp.unbind(on_touch_down=instance.strip_down)
|
||||
_strp.unbind(on_touch_move=instance.strip_move)
|
||||
_strp.unbind(on_touch_up=instance.strip_up)
|
||||
self.unbind(disabled=_strp.setter('disabled'))
|
||||
|
||||
sup.remove_widget(instance._strip)
|
||||
|
||||
cls = instance.strip_cls
|
||||
if not isinstance(_strp, cls):
|
||||
if isinstance(cls, str):
|
||||
cls = Factory.get(cls)
|
||||
instance._strip = _strp = cls()
|
||||
|
||||
sz_frm = instance.sizable_from[0]
|
||||
if sz_frm in ('l', 'r'):
|
||||
_strp.size_hint = None, 1
|
||||
_strp.width = instance.strip_size
|
||||
instance.orientation = 'horizontal'
|
||||
instance.unbind(strip_size=_strp.setter('width'))
|
||||
instance.bind(strip_size=_strp.setter('width'))
|
||||
else:
|
||||
_strp.size_hint = 1, None
|
||||
_strp.height = instance.strip_size
|
||||
instance.orientation = 'vertical'
|
||||
instance.unbind(strip_size=_strp.setter('height'))
|
||||
instance.bind(strip_size=_strp.setter('height'))
|
||||
|
||||
index = 1
|
||||
if sz_frm in ('r', 'b'):
|
||||
index = 0
|
||||
sup.add_widget(_strp, index)
|
||||
|
||||
_strp.bind(on_touch_down=instance.strip_down)
|
||||
_strp.bind(on_touch_move=instance.strip_move)
|
||||
_strp.bind(on_touch_up=instance.strip_up)
|
||||
_strp.disabled = self.disabled
|
||||
self.bind(disabled=_strp.setter('disabled'))
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
if self._container or not widget:
|
||||
return Exception('Splitter accepts only one Child')
|
||||
self._container = widget
|
||||
sz_frm = self.sizable_from[0]
|
||||
if sz_frm in ('l', 'r'):
|
||||
widget.size_hint_x = 1
|
||||
else:
|
||||
widget.size_hint_y = 1
|
||||
|
||||
index = 0
|
||||
if sz_frm in ('r', 'b'):
|
||||
index = 1
|
||||
super(Splitter, self).add_widget(widget, index, *args, **kwargs)
|
||||
self.on_sizable_from(self, self.sizable_from)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
super(Splitter, self).remove_widget(widget, *args, **kwargs)
|
||||
if widget == self._container:
|
||||
self._container = None
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
self.remove_widget(self._container)
|
||||
|
||||
def strip_down(self, instance, touch):
|
||||
if not instance.collide_point(*touch.pos):
|
||||
return False
|
||||
touch.grab(self)
|
||||
self.dispatch('on_press')
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def _rebind_parent(self, instance, new_parent):
|
||||
if self._bound_parent is not None:
|
||||
self._bound_parent.unbind(size=self.rescale_parent_proportion)
|
||||
if self.parent is not None:
|
||||
new_parent.bind(size=self.rescale_parent_proportion)
|
||||
self._bound_parent = new_parent
|
||||
self.rescale_parent_proportion()
|
||||
|
||||
def rescale_parent_proportion(self, *args):
|
||||
if not self.parent:
|
||||
return
|
||||
if self.rescale_with_parent:
|
||||
parent_proportion = self._parent_proportion
|
||||
if self.sizable_from in ('top', 'bottom'):
|
||||
new_height = parent_proportion * self.parent.height
|
||||
self.height = max(self.min_size,
|
||||
min(new_height, self.max_size))
|
||||
else:
|
||||
new_width = parent_proportion * self.parent.width
|
||||
self.width = max(self.min_size, min(new_width, self.max_size))
|
||||
|
||||
def _do_size(self, instance, value):
|
||||
if self.sizable_from[0] in ('l', 'r'):
|
||||
self.width = max(self.min_size, min(self.width, self.max_size))
|
||||
else:
|
||||
self.height = max(self.min_size, min(self.height, self.max_size))
|
||||
|
||||
@staticmethod
|
||||
def _is_moving(sz_frm, diff, pos, minpos, maxpos):
|
||||
if sz_frm in ('l', 'b'):
|
||||
cmp = minpos
|
||||
else:
|
||||
cmp = maxpos
|
||||
if diff == 0:
|
||||
return False
|
||||
elif diff > 0 and pos <= cmp:
|
||||
return False
|
||||
elif diff < 0 and pos >= cmp:
|
||||
return False
|
||||
return True
|
||||
|
||||
def strip_move(self, instance, touch):
|
||||
if touch.grab_current is not instance:
|
||||
return False
|
||||
max_size = self.max_size
|
||||
min_size = self.min_size
|
||||
sz_frm = self.sizable_from[0]
|
||||
|
||||
if sz_frm in ('t', 'b'):
|
||||
diff_y = (touch.dy)
|
||||
self_y = self.y
|
||||
self_top = self.top
|
||||
if not self._is_moving(sz_frm, diff_y, touch.y, self_y, self_top):
|
||||
return
|
||||
if self.keep_within_parent:
|
||||
if sz_frm == 't' and (self_top + diff_y) > self.parent.top:
|
||||
diff_y = self.parent.top - self_top
|
||||
elif sz_frm == 'b' and (self_y + diff_y) < self.parent.y:
|
||||
diff_y = self.parent.y - self_y
|
||||
if sz_frm == 'b':
|
||||
diff_y *= -1
|
||||
if self.size_hint_y:
|
||||
self.size_hint_y = None
|
||||
if self.height > 0:
|
||||
self.height += diff_y
|
||||
else:
|
||||
self.height = 1
|
||||
|
||||
height = self.height
|
||||
self.height = max(min_size, min(height, max_size))
|
||||
|
||||
self._parent_proportion = self.height / self.parent.height
|
||||
else:
|
||||
diff_x = (touch.dx)
|
||||
self_x = self.x
|
||||
self_right = self.right
|
||||
if not self._is_moving(sz_frm, diff_x, touch.x, self_x, self_right):
|
||||
return
|
||||
if self.keep_within_parent:
|
||||
if sz_frm == 'l' and (self_x + diff_x) < self.parent.x:
|
||||
diff_x = self.parent.x - self_x
|
||||
elif (sz_frm == 'r' and
|
||||
(self_right + diff_x) > self.parent.right):
|
||||
diff_x = self.parent.right - self_right
|
||||
if sz_frm == 'l':
|
||||
diff_x *= -1
|
||||
if self.size_hint_x:
|
||||
self.size_hint_x = None
|
||||
if self.width > 0:
|
||||
self.width += diff_x
|
||||
else:
|
||||
self.width = 1
|
||||
|
||||
width = self.width
|
||||
self.width = max(min_size, min(width, max_size))
|
||||
|
||||
self._parent_proportion = self.width / self.parent.width
|
||||
|
||||
def strip_up(self, instance, touch):
|
||||
if touch.grab_current is not instance:
|
||||
return
|
||||
|
||||
if touch.is_double_tap:
|
||||
max_size = self.max_size
|
||||
min_size = self.min_size
|
||||
sz_frm = self.sizable_from[0]
|
||||
s = self.size
|
||||
|
||||
if sz_frm in ('t', 'b'):
|
||||
if self.size_hint_y:
|
||||
self.size_hint_y = None
|
||||
if s[1] - min_size <= max_size - s[1]:
|
||||
self.height = max_size
|
||||
else:
|
||||
self.height = min_size
|
||||
else:
|
||||
if self.size_hint_x:
|
||||
self.size_hint_x = None
|
||||
if s[0] - min_size <= max_size - s[0]:
|
||||
self.width = max_size
|
||||
else:
|
||||
self.width = min_size
|
||||
touch.ungrab(instance)
|
||||
self.dispatch('on_release')
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.app import App
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
|
||||
class SplitterApp(App):
|
||||
|
||||
def build(self):
|
||||
root = FloatLayout()
|
||||
bx = BoxLayout()
|
||||
bx.add_widget(Button())
|
||||
bx.add_widget(Button())
|
||||
bx2 = BoxLayout()
|
||||
bx2.add_widget(Button())
|
||||
bx2.add_widget(Button())
|
||||
bx2.add_widget(Button())
|
||||
spl = Splitter(
|
||||
size_hint=(1, .25),
|
||||
pos_hint={'top': 1},
|
||||
sizable_from='bottom')
|
||||
spl1 = Splitter(
|
||||
sizable_from='left',
|
||||
size_hint=(None, 1), width=90)
|
||||
spl1.add_widget(Button())
|
||||
bx.add_widget(spl1)
|
||||
spl.add_widget(bx)
|
||||
|
||||
spl2 = Splitter(size_hint=(.25, 1))
|
||||
spl2.add_widget(bx2)
|
||||
spl2.sizable_from = 'right'
|
||||
root.add_widget(spl)
|
||||
root.add_widget(spl2)
|
||||
return root
|
||||
|
||||
SplitterApp().run()
|
||||
@@ -0,0 +1,331 @@
|
||||
'''
|
||||
Stack Layout
|
||||
============
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: images/stacklayout.gif
|
||||
:align: right
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: images/stacklayout.png
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.0.5
|
||||
|
||||
The :class:`StackLayout` arranges children vertically or horizontally, as many
|
||||
as the layout can fit. The size of the individual children widgets do not
|
||||
have to be uniform.
|
||||
|
||||
For example, to display widgets that get progressively larger in width::
|
||||
|
||||
root = StackLayout()
|
||||
for i in range(25):
|
||||
btn = Button(text=str(i), width=40 + i * 5, size_hint=(None, 0.15))
|
||||
root.add_widget(btn)
|
||||
|
||||
.. image:: images/stacklayout_sizing.png
|
||||
:align: left
|
||||
'''
|
||||
|
||||
__all__ = ('StackLayout', )
|
||||
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import NumericProperty, OptionProperty, \
|
||||
ReferenceListProperty, VariableListProperty
|
||||
|
||||
|
||||
def _compute_size(c, available_size, idx):
|
||||
sh_min = c.size_hint_min[idx]
|
||||
sh_max = c.size_hint_max[idx]
|
||||
val = c.size_hint[idx] * available_size
|
||||
|
||||
if sh_min is not None:
|
||||
if sh_max is not None:
|
||||
return max(min(sh_max, val), sh_min)
|
||||
return max(val, sh_min)
|
||||
if sh_max is not None:
|
||||
return min(sh_max, val)
|
||||
return val
|
||||
|
||||
|
||||
class StackLayout(Layout):
|
||||
'''Stack layout class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
spacing = VariableListProperty([0, 0], length=2)
|
||||
'''Spacing between children: [spacing_horizontal, spacing_vertical].
|
||||
|
||||
spacing also accepts a single argument form [spacing].
|
||||
|
||||
:attr:`spacing` is a
|
||||
:class:`~kivy.properties.VariableListProperty` and defaults to [0, 0].
|
||||
|
||||
'''
|
||||
|
||||
padding = VariableListProperty([0, 0, 0, 0])
|
||||
'''Padding between the layout box and it's children: [padding_left,
|
||||
padding_top, padding_right, padding_bottom].
|
||||
|
||||
padding also accepts a two argument form [padding_horizontal,
|
||||
padding_vertical] and a single argument form [padding].
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
Replaced the NumericProperty with a VariableListProperty.
|
||||
|
||||
:attr:`padding` is a
|
||||
:class:`~kivy.properties.VariableListProperty` and defaults to
|
||||
[0, 0, 0, 0].
|
||||
|
||||
'''
|
||||
|
||||
orientation = OptionProperty('lr-tb', options=(
|
||||
'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt', 'bt-lr', 'rl-bt',
|
||||
'bt-rl'))
|
||||
'''Orientation of the layout.
|
||||
|
||||
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'lr-tb'.
|
||||
|
||||
Valid orientations are 'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt',
|
||||
'bt-lr', 'rl-bt' and 'bt-rl'.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
:attr:`orientation` now correctly handles all valid combinations of
|
||||
'lr','rl','tb','bt'. Before this version only 'lr-tb' and
|
||||
'tb-lr' were supported, and 'tb-lr' was misnamed and placed
|
||||
widgets from bottom to top and from right to left (reversed compared
|
||||
to what was expected).
|
||||
|
||||
.. note::
|
||||
|
||||
'lr' means Left to Right.
|
||||
'rl' means Right to Left.
|
||||
'tb' means Top to Bottom.
|
||||
'bt' means Bottom to Top.
|
||||
'''
|
||||
|
||||
minimum_width = NumericProperty(0)
|
||||
'''Minimum width needed to contain all children. It is automatically set
|
||||
by the layout.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`minimum_width` is a :class:`kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
minimum_height = NumericProperty(0)
|
||||
'''Minimum height needed to contain all children. It is automatically set
|
||||
by the layout.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`minimum_height` is a :class:`kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
minimum_size = ReferenceListProperty(minimum_width, minimum_height)
|
||||
'''Minimum size needed to contain all children. It is automatically set
|
||||
by the layout.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`minimum_size` is a
|
||||
:class:`~kivy.properties.ReferenceListProperty` of
|
||||
(:attr:`minimum_width`, :attr:`minimum_height`) properties.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(StackLayout, self).__init__(**kwargs)
|
||||
trigger = self._trigger_layout
|
||||
fbind = self.fbind
|
||||
fbind('padding', trigger)
|
||||
fbind('spacing', trigger)
|
||||
fbind('children', trigger)
|
||||
fbind('orientation', trigger)
|
||||
fbind('size', trigger)
|
||||
fbind('pos', trigger)
|
||||
|
||||
def do_layout(self, *largs):
|
||||
if not self.children:
|
||||
self.minimum_size = (0., 0.)
|
||||
return
|
||||
|
||||
# optimize layout by preventing looking at the same attribute in a loop
|
||||
selfpos = self.pos
|
||||
selfsize = self.size
|
||||
orientation = self.orientation.split('-')
|
||||
padding_left = self.padding[0]
|
||||
padding_top = self.padding[1]
|
||||
padding_right = self.padding[2]
|
||||
padding_bottom = self.padding[3]
|
||||
|
||||
padding_x = padding_left + padding_right
|
||||
padding_y = padding_top + padding_bottom
|
||||
spacing_x, spacing_y = self.spacing
|
||||
|
||||
# Determine which direction and in what order to place the widgets
|
||||
posattr = [0] * 2
|
||||
posdelta = [0] * 2
|
||||
posstart = [0] * 2
|
||||
for i in (0, 1):
|
||||
posattr[i] = 1 * (orientation[i] in ('tb', 'bt'))
|
||||
k = posattr[i]
|
||||
if orientation[i] == 'lr':
|
||||
# left to right
|
||||
posdelta[i] = 1
|
||||
posstart[i] = selfpos[k] + padding_left
|
||||
elif orientation[i] == 'bt':
|
||||
# bottom to top
|
||||
posdelta[i] = 1
|
||||
posstart[i] = selfpos[k] + padding_bottom
|
||||
elif orientation[i] == 'rl':
|
||||
# right to left
|
||||
posdelta[i] = -1
|
||||
posstart[i] = selfpos[k] + selfsize[k] - padding_right
|
||||
else:
|
||||
# top to bottom
|
||||
posdelta[i] = -1
|
||||
posstart[i] = selfpos[k] + selfsize[k] - padding_top
|
||||
|
||||
innerattr, outerattr = posattr
|
||||
ustart, vstart = posstart
|
||||
deltau, deltav = posdelta
|
||||
del posattr, posdelta, posstart
|
||||
|
||||
u = ustart # inner loop position variable
|
||||
v = vstart # outer loop position variable
|
||||
|
||||
# space calculation, used for determining when a row or column is full
|
||||
|
||||
if orientation[0] in ('lr', 'rl'):
|
||||
sv = padding_y # size in v-direction, for minimum_size property
|
||||
su = padding_x # size in h-direction
|
||||
spacing_u = spacing_x
|
||||
spacing_v = spacing_y
|
||||
padding_u = padding_x
|
||||
padding_v = padding_y
|
||||
else:
|
||||
sv = padding_x # size in v-direction, for minimum_size property
|
||||
su = padding_y # size in h-direction
|
||||
spacing_u = spacing_y
|
||||
spacing_v = spacing_x
|
||||
padding_u = padding_y
|
||||
padding_v = padding_x
|
||||
|
||||
# space calculation, row height or column width, for arranging widgets
|
||||
lv = 0
|
||||
|
||||
urev = (deltau < 0)
|
||||
vrev = (deltav < 0)
|
||||
firstchild = self.children[0]
|
||||
sizes = []
|
||||
lc = []
|
||||
for c in reversed(self.children):
|
||||
if c.size_hint[outerattr] is not None:
|
||||
c.size[outerattr] = max(
|
||||
1, _compute_size(c, selfsize[outerattr] - padding_v,
|
||||
outerattr))
|
||||
|
||||
# does the widget fit in the row/column?
|
||||
ccount = len(lc)
|
||||
totalsize = availsize = max(
|
||||
0, selfsize[innerattr] - padding_u - spacing_u * ccount)
|
||||
if not lc:
|
||||
if c.size_hint[innerattr] is not None:
|
||||
childsize = max(1, _compute_size(c, totalsize, innerattr))
|
||||
else:
|
||||
childsize = max(0, c.size[innerattr])
|
||||
availsize = selfsize[innerattr] - padding_u - childsize
|
||||
testsizes = [childsize]
|
||||
else:
|
||||
testsizes = [0] * (ccount + 1)
|
||||
for i, child in enumerate(lc):
|
||||
if availsize <= 0:
|
||||
# no space left but we're trying to add another widget.
|
||||
availsize = -1
|
||||
break
|
||||
if child.size_hint[innerattr] is not None:
|
||||
testsizes[i] = childsize = max(
|
||||
1, _compute_size(child, totalsize, innerattr))
|
||||
else:
|
||||
childsize = max(0, child.size[innerattr])
|
||||
testsizes[i] = childsize
|
||||
availsize -= childsize
|
||||
if c.size_hint[innerattr] is not None:
|
||||
testsizes[-1] = max(
|
||||
1, _compute_size(c, totalsize, innerattr))
|
||||
else:
|
||||
testsizes[-1] = max(0, c.size[innerattr])
|
||||
availsize -= testsizes[-1]
|
||||
|
||||
# Tiny value added in order to avoid issues with float precision
|
||||
# causing unexpected children reordering when parent resizes.
|
||||
# e.g. if size is 101 and children size_hint_x is 1./5
|
||||
# 5 children would not fit in one line because 101*(1./5) > 101/5
|
||||
if (availsize + 1e-10) >= 0 or not lc:
|
||||
# even if there's no space, we always add one widget to a row
|
||||
lc.append(c)
|
||||
sizes = testsizes
|
||||
lv = max(lv, c.size[outerattr])
|
||||
continue
|
||||
|
||||
# apply the sizes
|
||||
for i, child in enumerate(lc):
|
||||
if child.size_hint[innerattr] is not None:
|
||||
child.size[innerattr] = sizes[i]
|
||||
|
||||
# push the line
|
||||
sv += lv + spacing_v
|
||||
for c2 in lc:
|
||||
if urev:
|
||||
u -= c2.size[innerattr]
|
||||
c2.pos[innerattr] = u
|
||||
pos_outer = v
|
||||
if vrev:
|
||||
# v position is actually the top/right side of the widget
|
||||
# when going from high to low coordinate values,
|
||||
# we need to subtract the height/width from the position.
|
||||
pos_outer -= c2.size[outerattr]
|
||||
c2.pos[outerattr] = pos_outer
|
||||
if urev:
|
||||
u -= spacing_u
|
||||
else:
|
||||
u += c2.size[innerattr] + spacing_u
|
||||
|
||||
v += deltav * lv
|
||||
v += deltav * spacing_v
|
||||
lc = [c]
|
||||
lv = c.size[outerattr]
|
||||
if c.size_hint[innerattr] is not None:
|
||||
sizes = [
|
||||
max(1, _compute_size(c, selfsize[innerattr] - padding_u,
|
||||
innerattr))]
|
||||
else:
|
||||
sizes = [max(0, c.size[innerattr])]
|
||||
u = ustart
|
||||
|
||||
if lc:
|
||||
# apply the sizes
|
||||
for i, child in enumerate(lc):
|
||||
if child.size_hint[innerattr] is not None:
|
||||
child.size[innerattr] = sizes[i]
|
||||
|
||||
# push the last (incomplete) line
|
||||
sv += lv + spacing_v
|
||||
for c2 in lc:
|
||||
if urev:
|
||||
u -= c2.size[innerattr]
|
||||
c2.pos[innerattr] = u
|
||||
pos_outer = v
|
||||
if vrev:
|
||||
pos_outer -= c2.size[outerattr]
|
||||
c2.pos[outerattr] = pos_outer
|
||||
if urev:
|
||||
u -= spacing_u
|
||||
else:
|
||||
u += c2.size[innerattr] + spacing_u
|
||||
|
||||
self.minimum_size[outerattr] = sv
|
||||
@@ -0,0 +1,40 @@
|
||||
'''
|
||||
Stencil View
|
||||
============
|
||||
|
||||
.. image:: images/stencilview.gif
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
:class:`StencilView` limits the drawing of child widgets to the StencilView's
|
||||
bounding box. Any drawing outside the bounding box will be clipped (trashed).
|
||||
|
||||
The StencilView uses the stencil graphics instructions under the hood. It
|
||||
provides an efficient way to clip the drawing area of children.
|
||||
|
||||
.. note::
|
||||
|
||||
As with the stencil graphics instructions, you cannot stack more than 128
|
||||
stencil-aware widgets.
|
||||
|
||||
.. note::
|
||||
|
||||
StencilView is not a layout. Consequently, you have to manage the size and
|
||||
position of its children directly. You can combine (subclass both)
|
||||
a StencilView and a Layout in order to achieve a layout's behavior.
|
||||
For example::
|
||||
|
||||
class BoxStencil(BoxLayout, StencilView):
|
||||
pass
|
||||
'''
|
||||
|
||||
__all__ = ('StencilView', )
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
|
||||
class StencilView(Widget):
|
||||
'''StencilView class. See module documentation for more information.
|
||||
'''
|
||||
pass
|
||||
@@ -0,0 +1,122 @@
|
||||
'''
|
||||
Switch
|
||||
======
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
.. image:: images/switch-on.jpg
|
||||
:align: right
|
||||
|
||||
.. image:: images/switch-off.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`Switch` widget is active or inactive, like a mechanical light
|
||||
switch. The user can swipe to the left/right to activate/deactivate it::
|
||||
|
||||
switch = Switch(active=True)
|
||||
|
||||
To attach a callback that listens to the activation state::
|
||||
|
||||
def callback(instance, value):
|
||||
print('the switch', instance, 'is', value)
|
||||
|
||||
switch = Switch()
|
||||
switch.bind(active=callback)
|
||||
|
||||
By default, the representation of the widget is static. The minimum size
|
||||
required is 83x32 pixels (defined by the background image). The image is
|
||||
centered within the widget.
|
||||
|
||||
The entire widget is active, not just the part with graphics. As long as you
|
||||
swipe over the widget's bounding box, it will work.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to control the state with a single touch instead of a swipe,
|
||||
use the :class:`ToggleButton` instead.
|
||||
|
||||
Kv Example::
|
||||
|
||||
BoxLayout:
|
||||
Label:
|
||||
text: 'power up'
|
||||
Switch:
|
||||
id: switch
|
||||
Label:
|
||||
text: 'woooooooooooh' if switch.active else ''
|
||||
'''
|
||||
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.animation import Animation
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
|
||||
|
||||
class Switch(Widget):
|
||||
'''Switch class. See module documentation for more information.
|
||||
'''
|
||||
active = BooleanProperty(False)
|
||||
'''Indicate whether the switch is active or inactive.
|
||||
|
||||
:attr:`active` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
touch_control = ObjectProperty(None, allownone=True)
|
||||
'''(internal) Contains the touch that currently interacts with the switch.
|
||||
|
||||
:attr:`touch_control` is an :class:`~kivy.properties.ObjectProperty`
|
||||
and defaults to None.
|
||||
'''
|
||||
|
||||
touch_distance = NumericProperty(0)
|
||||
'''(internal) Contains the distance between the initial position of the
|
||||
touch and the current position to determine if the swipe is from the left
|
||||
or right.
|
||||
|
||||
:attr:`touch_distance` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
active_norm_pos = NumericProperty(0)
|
||||
'''(internal) Contains the normalized position of the movable element
|
||||
inside the switch, in the 0-1 range.
|
||||
|
||||
:attr:`active_norm_pos` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.disabled or self.touch_control is not None:
|
||||
return
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
touch.grab(self)
|
||||
self.touch_distance = 0
|
||||
self.touch_control = touch
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
self.touch_distance = touch.x - touch.ox
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
# depending of the distance, activate by norm pos or invert
|
||||
if abs(touch.ox - touch.x) < 5:
|
||||
self.active = not self.active
|
||||
else:
|
||||
self.active = self.active_norm_pos > 0.5
|
||||
Animation(active_norm_pos=int(self.active), t='out_quad',
|
||||
d=.2).start(self)
|
||||
self.touch_control = None
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
runTouchApp(Switch())
|
||||
@@ -0,0 +1,880 @@
|
||||
'''
|
||||
TabbedPanel
|
||||
===========
|
||||
|
||||
.. image:: images/tabbed_panel.jpg
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
|
||||
The `TabbedPanel` widget manages different widgets in tabs, with a header area
|
||||
for the actual tab buttons and a content area for showing the current tab
|
||||
content.
|
||||
|
||||
The :class:`TabbedPanel` provides one default tab.
|
||||
|
||||
Simple example
|
||||
--------------
|
||||
|
||||
.. include:: ../../examples/widgets/tabbedpanel.py
|
||||
:literal:
|
||||
|
||||
.. note::
|
||||
|
||||
A new class :class:`TabbedPanelItem` has been introduced in 1.5.0 for
|
||||
convenience. So now one can simply add a :class:`TabbedPanelItem` to a
|
||||
:class:`TabbedPanel` and `content` to the :class:`TabbedPanelItem`
|
||||
as in the example provided above.
|
||||
|
||||
Customize the Tabbed Panel
|
||||
--------------------------
|
||||
|
||||
You can choose the position in which the tabs are displayed::
|
||||
|
||||
tab_pos = 'top_mid'
|
||||
|
||||
An individual tab is called a TabbedPanelHeader. It is a special button
|
||||
containing a `content` property. You add the TabbedPanelHeader first, and set
|
||||
its `content` property separately::
|
||||
|
||||
tp = TabbedPanel()
|
||||
th = TabbedPanelHeader(text='Tab2')
|
||||
tp.add_widget(th)
|
||||
|
||||
An individual tab, represented by a TabbedPanelHeader, needs its content set.
|
||||
This content can be any widget. It could be a layout with a deep
|
||||
hierarchy of widgets, or it could be an individual widget, such as a label or a
|
||||
button::
|
||||
|
||||
th.content = your_content_instance
|
||||
|
||||
There is one "shared" main content area active at any given time, for all
|
||||
the tabs. Your app is responsible for adding the content of individual tabs
|
||||
and for managing them, but it's not responsible for content switching. The
|
||||
tabbed panel handles switching of the main content object as per user action.
|
||||
|
||||
There is a default tab added when the tabbed panel is instantiated.
|
||||
Tabs that you add individually as above, are added in addition to the default
|
||||
tab. Thus, depending on your needs and design, you will want to customize the
|
||||
default tab::
|
||||
|
||||
tp.default_tab_text = 'Something Specific To Your Use'
|
||||
|
||||
|
||||
The default tab machinery requires special consideration and management.
|
||||
Accordingly, an `on_default_tab` event is provided for associating a callback::
|
||||
|
||||
tp.bind(default_tab = my_default_tab_callback)
|
||||
|
||||
It's important to note that by default, :attr:`default_tab_cls` is of type
|
||||
:class:`TabbedPanelHeader` and thus has the same properties as other tabs.
|
||||
|
||||
Since 1.5.0, it is now possible to disable the creation of the
|
||||
:attr:`default_tab` by setting :attr:`do_default_tab` to False.
|
||||
|
||||
Tabs and content can be removed in several ways::
|
||||
|
||||
tp.remove_widget(widget/tabbed_panel_header)
|
||||
or
|
||||
tp.clear_widgets() # to clear all the widgets in the content area
|
||||
or
|
||||
tp.clear_tabs() # to remove the TabbedPanelHeaders
|
||||
|
||||
To access the children of the tabbed panel, use content.children::
|
||||
|
||||
tp.content.children
|
||||
|
||||
To access the list of tabs::
|
||||
|
||||
tp.tab_list
|
||||
|
||||
To change the appearance of the main tabbed panel content::
|
||||
|
||||
background_color = (1, 0, 0, .5) #50% translucent red
|
||||
border = [0, 0, 0, 0]
|
||||
background_image = 'path/to/background/image'
|
||||
|
||||
To change the background of a individual tab, use these two properties::
|
||||
|
||||
tab_header_instance.background_normal = 'path/to/tab_head/img'
|
||||
tab_header_instance.background_down = 'path/to/tab_head/img_pressed'
|
||||
|
||||
A TabbedPanelStrip contains the individual tab headers. To change the
|
||||
appearance of this tab strip, override the canvas of TabbedPanelStrip.
|
||||
For example, in the kv language:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<TabbedPanelStrip>
|
||||
canvas:
|
||||
Color:
|
||||
rgba: (0, 1, 0, 1) # green
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
|
||||
By default the tabbed panel strip takes its background image and color from the
|
||||
tabbed panel's background_image and background_color.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('StripLayout', 'TabbedPanel', 'TabbedPanelContent',
|
||||
'TabbedPanelHeader', 'TabbedPanelItem', 'TabbedPanelStrip',
|
||||
'TabbedPanelException')
|
||||
|
||||
from functools import partial
|
||||
from kivy.clock import Clock
|
||||
from kivy.compat import string_types
|
||||
from kivy.factory import Factory
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.scatter import Scatter
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.logger import Logger
|
||||
from kivy.metrics import dp
|
||||
from kivy.properties import ObjectProperty, StringProperty, OptionProperty, \
|
||||
ListProperty, NumericProperty, AliasProperty, BooleanProperty, \
|
||||
ColorProperty
|
||||
|
||||
|
||||
class TabbedPanelException(Exception):
|
||||
'''The TabbedPanelException class.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class TabbedPanelHeader(ToggleButton):
|
||||
'''A Base for implementing a Tabbed Panel Head. A button intended to be
|
||||
used as a Heading/Tab for a TabbedPanel widget.
|
||||
|
||||
You can use this TabbedPanelHeader widget to add a new tab to a
|
||||
TabbedPanel.
|
||||
'''
|
||||
|
||||
content = ObjectProperty(None, allownone=True)
|
||||
'''Content to be loaded when this tab header is selected.
|
||||
|
||||
:attr:`content` is an :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
# only allow selecting the tab if not already selected
|
||||
def on_touch_down(self, touch):
|
||||
if self.state == 'down':
|
||||
# dispatch to children, not to self
|
||||
for child in self.children:
|
||||
child.dispatch('on_touch_down', touch)
|
||||
return
|
||||
else:
|
||||
super(TabbedPanelHeader, self).on_touch_down(touch)
|
||||
|
||||
def on_release(self, *largs):
|
||||
# Tabbed panel header is a child of tab_strib which has a
|
||||
# `tabbed_panel` property
|
||||
if self.parent:
|
||||
self.parent.tabbed_panel.switch_to(self)
|
||||
else:
|
||||
# tab removed before we could switch to it. Switch back to
|
||||
# previous tab
|
||||
self.panel.switch_to(self.panel.current_tab)
|
||||
|
||||
|
||||
class TabbedPanelItem(TabbedPanelHeader):
|
||||
'''This is a convenience class that provides a header of type
|
||||
TabbedPanelHeader and links it with the content automatically. Thus
|
||||
facilitating you to simply do the following in kv language:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<TabbedPanel>:
|
||||
# ...other settings
|
||||
TabbedPanelItem:
|
||||
BoxLayout:
|
||||
Label:
|
||||
text: 'Second tab content area'
|
||||
Button:
|
||||
text: 'Button that does nothing'
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
'''
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
self.content = widget
|
||||
if not self.parent:
|
||||
return
|
||||
panel = self.parent.tabbed_panel
|
||||
if panel.current_tab == self:
|
||||
panel.switch_to(self)
|
||||
|
||||
def remove_widget(self, *args, **kwargs):
|
||||
self.content = None
|
||||
if not self.parent:
|
||||
return
|
||||
panel = self.parent.tabbed_panel
|
||||
if panel.current_tab == self:
|
||||
panel.remove_widget(*args, **kwargs)
|
||||
|
||||
|
||||
class TabbedPanelStrip(GridLayout):
|
||||
'''A strip intended to be used as background for Heading/Tab.
|
||||
This does not cover the blank areas in case the tabs don't cover
|
||||
the entire width/height of the TabbedPanel(use :class:`StripLayout`
|
||||
for that).
|
||||
'''
|
||||
tabbed_panel = ObjectProperty(None)
|
||||
'''Link to the panel that the tab strip is a part of.
|
||||
|
||||
:attr:`tabbed_panel` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None .
|
||||
'''
|
||||
|
||||
|
||||
class StripLayout(GridLayout):
|
||||
''' The main layout that is used to house the entire tabbedpanel strip
|
||||
including the blank areas in case the tabs don't cover the entire
|
||||
width/height.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
'''
|
||||
|
||||
border = ListProperty([4, 4, 4, 4])
|
||||
'''Border property for the :attr:`background_image`.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults
|
||||
to [4, 4, 4, 4]
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_view')
|
||||
'''Background image to be used for the Strip layout of the TabbedPanel.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to a transparent image.
|
||||
'''
|
||||
|
||||
|
||||
class TabbedPanelContent(FloatLayout):
|
||||
'''The TabbedPanelContent class.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class TabbedPanel(GridLayout):
|
||||
'''The TabbedPanel class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a).
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction, used itself for :attr:`background_image`.
|
||||
Can be changed for a custom background.
|
||||
|
||||
It must be a list of four values: (bottom, right, top, left). Read the
|
||||
BorderImage instructions for more information.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to (16, 16, 16, 16)
|
||||
'''
|
||||
|
||||
background_image = StringProperty('atlas://data/images/defaulttheme/tab')
|
||||
'''Background image of the main shared content object.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/tab'.
|
||||
'''
|
||||
|
||||
background_disabled_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/tab_disabled')
|
||||
'''Background image of the main shared content object when disabled.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled_image` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/tab'.
|
||||
'''
|
||||
|
||||
strip_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_view')
|
||||
'''Background image of the tabbed strip.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`strip_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to a empty image.
|
||||
'''
|
||||
|
||||
strip_border = ListProperty([4, 4, 4, 4])
|
||||
'''Border to be used on :attr:`strip_image`.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`strip_border` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to [4, 4, 4, 4].
|
||||
'''
|
||||
|
||||
_current_tab = ObjectProperty(None)
|
||||
|
||||
def get_current_tab(self):
|
||||
return self._current_tab
|
||||
|
||||
current_tab = AliasProperty(get_current_tab, None, bind=('_current_tab', ))
|
||||
'''Links to the currently selected or active tab.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
:attr:`current_tab` is an :class:`~kivy.AliasProperty`, read-only.
|
||||
'''
|
||||
|
||||
tab_pos = OptionProperty(
|
||||
'top_left',
|
||||
options=('left_top', 'left_mid', 'left_bottom', 'top_left',
|
||||
'top_mid', 'top_right', 'right_top', 'right_mid',
|
||||
'right_bottom', 'bottom_left', 'bottom_mid', 'bottom_right'))
|
||||
'''Specifies the position of the tabs relative to the content.
|
||||
Can be one of: `left_top`, `left_mid`, `left_bottom`, `top_left`,
|
||||
`top_mid`, `top_right`, `right_top`, `right_mid`, `right_bottom`,
|
||||
`bottom_left`, `bottom_mid`, `bottom_right`.
|
||||
|
||||
:attr:`tab_pos` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'top_left'.
|
||||
'''
|
||||
|
||||
tab_height = NumericProperty('40dp')
|
||||
'''Specifies the height of the tab header.
|
||||
|
||||
:attr:`tab_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 40.
|
||||
'''
|
||||
|
||||
tab_width = NumericProperty('100dp', allownone=True)
|
||||
'''Specifies the width of the tab header.
|
||||
|
||||
:attr:`tab_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
bar_width = NumericProperty('2dp')
|
||||
'''Width of the horizontal scroll bar. The width is interpreted
|
||||
as a height for the horizontal bar.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 2.
|
||||
'''
|
||||
|
||||
scroll_type = OptionProperty(['content'], options=(['content'], ['bars'],
|
||||
['bars', 'content'], ['content', 'bars']))
|
||||
|
||||
'''Sets the type of scrolling to use for the content of the scrollview.
|
||||
Available options are: ['content'], ['bars'], ['bars', 'content'].
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`scroll_type` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to ['content'].
|
||||
'''
|
||||
|
||||
do_default_tab = BooleanProperty(True)
|
||||
'''Specifies whether a default_tab head is provided.
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
:attr:`do_default_tab` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to 'True'.
|
||||
'''
|
||||
|
||||
default_tab_text = StringProperty('Default tab')
|
||||
'''Specifies the text displayed on the default tab header.
|
||||
|
||||
:attr:`default_tab_text` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'default tab'.
|
||||
'''
|
||||
|
||||
default_tab_cls = ObjectProperty(TabbedPanelHeader)
|
||||
'''Specifies the class to use for the styling of the default tab.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
.. warning::
|
||||
`default_tab_cls` should be subclassed from `TabbedPanelHeader`
|
||||
|
||||
:attr:`default_tab_cls` is an :class:`~kivy.properties.ObjectProperty`
|
||||
and defaults to `TabbedPanelHeader`. If you set a string, the
|
||||
:class:`~kivy.factory.Factory` will be used to resolve the class.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
The :class:`~kivy.factory.Factory` will resolve the class if a string
|
||||
is set.
|
||||
'''
|
||||
|
||||
def get_tab_list(self):
|
||||
if self._tab_strip:
|
||||
return self._tab_strip.children
|
||||
return 1.
|
||||
|
||||
tab_list = AliasProperty(get_tab_list, None)
|
||||
'''List of all the tab headers.
|
||||
|
||||
:attr:`tab_list` is an :class:`~kivy.properties.AliasProperty` and is
|
||||
read-only.
|
||||
'''
|
||||
|
||||
content = ObjectProperty(None)
|
||||
'''This is the object holding (current_tab's content is added to this)
|
||||
the content of the current tab. To Listen to the changes in the content
|
||||
of the current tab, you should bind to current_tabs `content` property.
|
||||
|
||||
:attr:`content` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to 'None'.
|
||||
'''
|
||||
|
||||
_default_tab = ObjectProperty(None, allow_none=True)
|
||||
|
||||
def get_def_tab(self):
|
||||
return self._default_tab
|
||||
|
||||
def set_def_tab(self, new_tab):
|
||||
if not issubclass(new_tab.__class__, TabbedPanelHeader):
|
||||
raise TabbedPanelException('`default_tab_class` should be\
|
||||
subclassed from `TabbedPanelHeader`')
|
||||
if self._default_tab == new_tab:
|
||||
return
|
||||
oltab = self._default_tab
|
||||
self._default_tab = new_tab
|
||||
self.remove_widget(oltab)
|
||||
self._original_tab = None
|
||||
self.switch_to(new_tab)
|
||||
new_tab.state = 'down'
|
||||
|
||||
default_tab = AliasProperty(get_def_tab, set_def_tab,
|
||||
bind=('_default_tab', ))
|
||||
'''Holds the default tab.
|
||||
|
||||
.. Note:: For convenience, the automatically provided default tab is
|
||||
deleted when you change default_tab to something else.
|
||||
As of 1.5.0, this behavior has been extended to every
|
||||
`default_tab` for consistency and not just the automatically
|
||||
provided one.
|
||||
|
||||
:attr:`default_tab` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
def get_def_tab_content(self):
|
||||
return self.default_tab.content
|
||||
|
||||
def set_def_tab_content(self, *l):
|
||||
self.default_tab.content = l[0]
|
||||
|
||||
default_tab_content = AliasProperty(get_def_tab_content,
|
||||
set_def_tab_content)
|
||||
'''Holds the default tab content.
|
||||
|
||||
:attr:`default_tab_content` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
_update_top_ev = _update_tab_ev = _update_tabs_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# these variables need to be initialized before the kv lang is
|
||||
# processed setup the base layout for the tabbed panel
|
||||
self._childrens = []
|
||||
self._tab_layout = StripLayout(rows=1)
|
||||
self.rows = 1
|
||||
self._tab_strip = TabbedPanelStrip(
|
||||
tabbed_panel=self,
|
||||
rows=1, size_hint=(None, None),
|
||||
height=self.tab_height, width=self.tab_width)
|
||||
|
||||
self._partial_update_scrollview = None
|
||||
self.content = TabbedPanelContent()
|
||||
self._current_tab = self._original_tab \
|
||||
= self._default_tab = TabbedPanelHeader()
|
||||
|
||||
super(TabbedPanel, self).__init__(**kwargs)
|
||||
|
||||
self.fbind('size', self._reposition_tabs)
|
||||
if not self.do_default_tab:
|
||||
Clock.schedule_once(self._switch_to_first_tab)
|
||||
return
|
||||
self._setup_default_tab()
|
||||
self.switch_to(self.default_tab)
|
||||
|
||||
def switch_to(self, header, do_scroll=False):
|
||||
'''Switch to a specific panel header.
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
|
||||
If used with `do_scroll=True`, it scrolls
|
||||
to the header's tab too.
|
||||
|
||||
:meth:`switch_to` cannot be called from within the
|
||||
:class:`TabbedPanel` or its subclass' ``__init__`` method.
|
||||
If that is required, use the ``Clock`` to schedule it. See `discussion
|
||||
<https://github.com/kivy/kivy/issues/3493#issuecomment-121567969>`_
|
||||
for full example.
|
||||
'''
|
||||
header_content = header.content
|
||||
self._current_tab.state = 'normal'
|
||||
header.state = 'down'
|
||||
self._current_tab = header
|
||||
self.clear_widgets()
|
||||
if header_content is None:
|
||||
return
|
||||
# if content has a previous parent remove it from that parent
|
||||
parent = header_content.parent
|
||||
if parent:
|
||||
parent.remove_widget(header_content)
|
||||
self.add_widget(header_content)
|
||||
|
||||
if do_scroll:
|
||||
tabs = self._tab_strip
|
||||
tabs.parent.scroll_to(header)
|
||||
|
||||
def clear_tabs(self, *l):
|
||||
self_tabs = self._tab_strip
|
||||
self_tabs.clear_widgets()
|
||||
if self.do_default_tab:
|
||||
self_default_tab = self._default_tab
|
||||
self_tabs.add_widget(self_default_tab)
|
||||
self_tabs.width = self_default_tab.width
|
||||
self._reposition_tabs()
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
content = self.content
|
||||
if content is None:
|
||||
return
|
||||
parent = widget.parent
|
||||
if parent:
|
||||
parent.remove_widget(widget)
|
||||
if widget in (content, self._tab_layout):
|
||||
super(TabbedPanel, self).add_widget(widget, *args, **kwargs)
|
||||
elif isinstance(widget, TabbedPanelHeader):
|
||||
self_tabs = self._tab_strip
|
||||
self_tabs.add_widget(widget, *args, **kwargs)
|
||||
widget.group = '__tab%r__' % self_tabs.uid
|
||||
self.on_tab_width()
|
||||
else:
|
||||
widget.pos_hint = {'x': 0, 'top': 1}
|
||||
self._childrens.append(widget)
|
||||
content.disabled = self.current_tab.disabled
|
||||
content.add_widget(widget, *args, **kwargs)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
content = self.content
|
||||
if content is None:
|
||||
return
|
||||
if widget in (content, self._tab_layout):
|
||||
super(TabbedPanel, self).remove_widget(widget, *args, **kwargs)
|
||||
elif isinstance(widget, TabbedPanelHeader):
|
||||
if not (self.do_default_tab and widget is self._default_tab):
|
||||
self_tabs = self._tab_strip
|
||||
self_tabs.width -= widget.width
|
||||
self_tabs.remove_widget(widget)
|
||||
if widget.state == 'down' and self.do_default_tab:
|
||||
self._default_tab.on_release()
|
||||
self._reposition_tabs()
|
||||
else:
|
||||
Logger.info('TabbedPanel: default tab! can\'t be removed.\n' +
|
||||
'Change `default_tab` to a different tab.')
|
||||
else:
|
||||
if widget in self._childrens:
|
||||
self._childrens.remove(widget)
|
||||
if widget in content.children:
|
||||
content.remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
if self.content:
|
||||
self.content.clear_widgets(*args, **kwargs)
|
||||
|
||||
def on_strip_image(self, instance, value):
|
||||
if not self._tab_layout:
|
||||
return
|
||||
self._tab_layout.background_image = value
|
||||
|
||||
def on_strip_border(self, instance, value):
|
||||
if not self._tab_layout:
|
||||
return
|
||||
self._tab_layout.border = value
|
||||
|
||||
def on_do_default_tab(self, instance, value):
|
||||
if not value:
|
||||
dft = self.default_tab
|
||||
if dft in self.tab_list:
|
||||
self.remove_widget(dft)
|
||||
self._switch_to_first_tab()
|
||||
self._default_tab = self._current_tab
|
||||
else:
|
||||
self._current_tab.state = 'normal'
|
||||
self._setup_default_tab()
|
||||
|
||||
def on_default_tab_text(self, *args):
|
||||
self._default_tab.text = self.default_tab_text
|
||||
|
||||
def on_tab_width(self, *l):
|
||||
ev = self._update_tab_ev
|
||||
if ev is None:
|
||||
ev = self._update_tab_ev = Clock.create_trigger(
|
||||
self._update_tab_width, 0)
|
||||
ev()
|
||||
|
||||
def on_tab_height(self, *l):
|
||||
self._tab_layout.height = self._tab_strip.height = self.tab_height
|
||||
self._reposition_tabs()
|
||||
|
||||
def on_tab_pos(self, *l):
|
||||
# ensure canvas
|
||||
self._reposition_tabs()
|
||||
|
||||
def _setup_default_tab(self):
|
||||
if self._default_tab in self.tab_list:
|
||||
return
|
||||
content = self._default_tab.content
|
||||
_tabs = self._tab_strip
|
||||
cls = self.default_tab_cls
|
||||
|
||||
if isinstance(cls, string_types):
|
||||
cls = Factory.get(cls)
|
||||
|
||||
if not issubclass(cls, TabbedPanelHeader):
|
||||
raise TabbedPanelException('`default_tab_class` should be\
|
||||
subclassed from `TabbedPanelHeader`')
|
||||
|
||||
# no need to instantiate if class is TabbedPanelHeader
|
||||
if cls != TabbedPanelHeader:
|
||||
self._current_tab = self._original_tab = self._default_tab = cls()
|
||||
|
||||
default_tab = self.default_tab
|
||||
if self._original_tab == self.default_tab:
|
||||
default_tab.text = self.default_tab_text
|
||||
|
||||
default_tab.height = self.tab_height
|
||||
default_tab.group = '__tab%r__' % _tabs.uid
|
||||
default_tab.state = 'down'
|
||||
default_tab.width = self.tab_width if self.tab_width else 100
|
||||
default_tab.content = content
|
||||
|
||||
tl = self.tab_list
|
||||
if default_tab not in tl:
|
||||
_tabs.add_widget(default_tab, len(tl))
|
||||
|
||||
if default_tab.content:
|
||||
self.clear_widgets()
|
||||
self.add_widget(self.default_tab.content)
|
||||
else:
|
||||
Clock.schedule_once(self._load_default_tab_content)
|
||||
self._current_tab = default_tab
|
||||
|
||||
def _switch_to_first_tab(self, *l):
|
||||
ltl = len(self.tab_list) - 1
|
||||
if ltl > -1:
|
||||
self._current_tab = dt = self._original_tab \
|
||||
= self.tab_list[ltl]
|
||||
self.switch_to(dt)
|
||||
|
||||
def _load_default_tab_content(self, dt):
|
||||
if self.default_tab:
|
||||
self.switch_to(self.default_tab)
|
||||
|
||||
def _reposition_tabs(self, *l):
|
||||
ev = self._update_tabs_ev
|
||||
if ev is None:
|
||||
ev = self._update_tabs_ev = Clock.create_trigger(
|
||||
self._update_tabs, 0)
|
||||
ev()
|
||||
|
||||
def _update_tabs(self, *l):
|
||||
self_content = self.content
|
||||
if not self_content:
|
||||
return
|
||||
# cache variables for faster access
|
||||
tab_pos = self.tab_pos
|
||||
tab_layout = self._tab_layout
|
||||
tab_layout.clear_widgets()
|
||||
scrl_v = ScrollView(size_hint=(None, 1), always_overscroll=False,
|
||||
bar_width=self.bar_width,
|
||||
scroll_type=self.scroll_type)
|
||||
tabs = self._tab_strip
|
||||
parent = tabs.parent
|
||||
if parent:
|
||||
parent.remove_widget(tabs)
|
||||
scrl_v.add_widget(tabs)
|
||||
scrl_v.pos = (0, 0)
|
||||
self_update_scrollview = self._update_scrollview
|
||||
|
||||
# update scrlv width when tab width changes depends on tab_pos
|
||||
if self._partial_update_scrollview is not None:
|
||||
tabs.unbind(width=self._partial_update_scrollview)
|
||||
self._partial_update_scrollview = partial(
|
||||
self_update_scrollview, scrl_v)
|
||||
tabs.bind(width=self._partial_update_scrollview)
|
||||
|
||||
# remove all widgets from the tab_strip
|
||||
super(TabbedPanel, self).clear_widgets()
|
||||
tab_height = self.tab_height
|
||||
|
||||
widget_list = []
|
||||
tab_list = []
|
||||
pos_letter = tab_pos[0]
|
||||
if pos_letter == 'b' or pos_letter == 't':
|
||||
# bottom or top positions
|
||||
# one col containing the tab_strip and the content
|
||||
self.cols = 1
|
||||
self.rows = 2
|
||||
# tab_layout contains the scrollview containing tabs and two blank
|
||||
# dummy widgets for spacing
|
||||
tab_layout.rows = 1
|
||||
tab_layout.cols = 3
|
||||
tab_layout.size_hint = (1, None)
|
||||
tab_layout.height = (tab_height + tab_layout.padding[1] +
|
||||
tab_layout.padding[3] + dp(2))
|
||||
self_update_scrollview(scrl_v)
|
||||
|
||||
if pos_letter == 'b':
|
||||
# bottom
|
||||
if tab_pos == 'bottom_mid':
|
||||
tab_list = (Widget(), scrl_v, Widget())
|
||||
widget_list = (self_content, tab_layout)
|
||||
else:
|
||||
if tab_pos == 'bottom_left':
|
||||
tab_list = (scrl_v, Widget(), Widget())
|
||||
elif tab_pos == 'bottom_right':
|
||||
# add two dummy widgets
|
||||
tab_list = (Widget(), Widget(), scrl_v)
|
||||
widget_list = (self_content, tab_layout)
|
||||
else:
|
||||
# top
|
||||
if tab_pos == 'top_mid':
|
||||
tab_list = (Widget(), scrl_v, Widget())
|
||||
elif tab_pos == 'top_left':
|
||||
tab_list = (scrl_v, Widget(), Widget())
|
||||
elif tab_pos == 'top_right':
|
||||
tab_list = (Widget(), Widget(), scrl_v)
|
||||
widget_list = (tab_layout, self_content)
|
||||
elif pos_letter == 'l' or pos_letter == 'r':
|
||||
# left or right positions
|
||||
# one row containing the tab_strip and the content
|
||||
self.cols = 2
|
||||
self.rows = 1
|
||||
# tab_layout contains two blank dummy widgets for spacing
|
||||
# "vertically" and the scatter containing scrollview
|
||||
# containing tabs
|
||||
tab_layout.rows = 3
|
||||
tab_layout.cols = 1
|
||||
tab_layout.size_hint = (None, 1)
|
||||
tab_layout.width = tab_height
|
||||
scrl_v.height = tab_height
|
||||
self_update_scrollview(scrl_v)
|
||||
|
||||
# rotate the scatter for vertical positions
|
||||
rotation = 90 if tab_pos[0] == 'l' else -90
|
||||
sctr = Scatter(do_translation=False,
|
||||
rotation=rotation,
|
||||
do_rotation=False,
|
||||
do_scale=False,
|
||||
size_hint=(None, None),
|
||||
auto_bring_to_front=False,
|
||||
size=scrl_v.size)
|
||||
sctr.add_widget(scrl_v)
|
||||
|
||||
lentab_pos = len(tab_pos)
|
||||
|
||||
# Update scatter's top when its pos changes.
|
||||
# Needed for repositioning scatter to the correct place after its
|
||||
# added to the parent. Use clock_schedule_once to ensure top is
|
||||
# calculated after the parent's pos on canvas has been calculated.
|
||||
# This is needed for when tab_pos changes to correctly position
|
||||
# scatter. Without clock.schedule_once the positions would look
|
||||
# fine but touch won't translate to the correct position
|
||||
|
||||
if tab_pos[lentab_pos - 4:] == '_top':
|
||||
# on positions 'left_top' and 'right_top'
|
||||
sctr.bind(pos=partial(self._update_top, sctr, 'top', None))
|
||||
tab_list = (sctr, )
|
||||
elif tab_pos[lentab_pos - 4:] == '_mid':
|
||||
# calculate top of scatter
|
||||
sctr.bind(pos=partial(self._update_top, sctr, 'mid',
|
||||
scrl_v.width))
|
||||
tab_list = (Widget(), sctr, Widget())
|
||||
elif tab_pos[lentab_pos - 7:] == '_bottom':
|
||||
tab_list = (Widget(), Widget(), sctr)
|
||||
|
||||
if pos_letter == 'l':
|
||||
widget_list = (tab_layout, self_content)
|
||||
else:
|
||||
widget_list = (self_content, tab_layout)
|
||||
|
||||
# add widgets to tab_layout
|
||||
add = tab_layout.add_widget
|
||||
for widg in tab_list:
|
||||
add(widg)
|
||||
|
||||
# add widgets to self
|
||||
add = self.add_widget
|
||||
for widg in widget_list:
|
||||
add(widg)
|
||||
|
||||
def _update_tab_width(self, *l):
|
||||
if self.tab_width:
|
||||
for tab in self.tab_list:
|
||||
tab.size_hint_x = 1
|
||||
tsw = self.tab_width * len(self._tab_strip.children)
|
||||
else:
|
||||
# tab_width = None
|
||||
tsw = 0
|
||||
for tab in self.tab_list:
|
||||
if tab.size_hint_x:
|
||||
# size_hint_x: x/.xyz
|
||||
tab.size_hint_x = 1
|
||||
# drop to default tab_width
|
||||
tsw += 100
|
||||
else:
|
||||
# size_hint_x: None
|
||||
tsw += tab.width
|
||||
self._tab_strip.width = tsw
|
||||
self._reposition_tabs()
|
||||
|
||||
def _update_top(self, *args):
|
||||
sctr, top, scrl_v_width, x, y = args
|
||||
ev = self._update_top_ev
|
||||
if ev is not None:
|
||||
ev.cancel()
|
||||
|
||||
ev = self._update_top_ev = Clock.schedule_once(
|
||||
partial(self._updt_top, sctr, top, scrl_v_width), 0)
|
||||
|
||||
def _updt_top(self, sctr, top, scrl_v_width, *args):
|
||||
if top[0] == 't':
|
||||
sctr.top = self.top
|
||||
else:
|
||||
sctr.top = self.top - (self.height - scrl_v_width) / 2
|
||||
|
||||
def _update_scrollview(self, scrl_v, *l):
|
||||
self_tab_pos = self.tab_pos
|
||||
self_tabs = self._tab_strip
|
||||
if self_tab_pos[0] == 'b' or self_tab_pos[0] == 't':
|
||||
# bottom or top
|
||||
scrl_v.width = min(self.width, self_tabs.width)
|
||||
# required for situations when scrl_v's pos is calculated
|
||||
# when it has no parent
|
||||
scrl_v.top += 1
|
||||
scrl_v.top -= 1
|
||||
else:
|
||||
# left or right
|
||||
scrl_v.width = min(self.height, self_tabs.width)
|
||||
self_tabs.pos = (0, 0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
'''
|
||||
Toggle button
|
||||
=============
|
||||
|
||||
.. image:: images/togglebutton.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`ToggleButton` widget acts like a checkbox. When you touch or click
|
||||
it, the state toggles between 'normal' and 'down' (as opposed to a
|
||||
:class:`Button` that is only 'down' as long as it is pressed).
|
||||
|
||||
Toggle buttons can also be grouped to make radio buttons - only one button in
|
||||
a group can be in a 'down' state. The group name can be a string or any other
|
||||
hashable Python object::
|
||||
|
||||
btn1 = ToggleButton(text='Male', group='sex',)
|
||||
btn2 = ToggleButton(text='Female', group='sex', state='down')
|
||||
btn3 = ToggleButton(text='Mixed', group='sex')
|
||||
|
||||
Only one of the buttons can be 'down'/checked at the same time.
|
||||
|
||||
To configure the ToggleButton, you can use the same properties that you can use
|
||||
for a :class:`~kivy.uix.button.Button` class.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ToggleButton', )
|
||||
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.behaviors import ToggleButtonBehavior
|
||||
|
||||
|
||||
class ToggleButton(ToggleButtonBehavior, Button):
|
||||
'''Toggle button class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,665 @@
|
||||
'''
|
||||
Tree View
|
||||
=========
|
||||
|
||||
.. image:: images/treeview.png
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
|
||||
:class:`TreeView` is a widget used to represent a tree structure. It is
|
||||
currently very basic, supporting a minimal feature set.
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
A :class:`TreeView` is populated with :class:`TreeViewNode` instances, but you
|
||||
cannot use a :class:`TreeViewNode` directly. You must combine it with another
|
||||
widget, such as :class:`~kivy.uix.label.Label`,
|
||||
:class:`~kivy.uix.button.Button` or even your own widget. The TreeView
|
||||
always creates a default root node, based on :class:`TreeViewLabel`.
|
||||
|
||||
:class:`TreeViewNode` is a class object containing needed properties for
|
||||
serving as a tree node. Extend :class:`TreeViewNode` to create custom node
|
||||
types for use with a :class:`TreeView`.
|
||||
|
||||
For constructing your own subclass, follow the pattern of TreeViewLabel which
|
||||
combines a Label and a TreeViewNode, producing a :class:`TreeViewLabel` for
|
||||
direct use in a TreeView instance.
|
||||
|
||||
To use the TreeViewLabel class, you could create two nodes directly attached
|
||||
to root::
|
||||
|
||||
tv = TreeView()
|
||||
tv.add_node(TreeViewLabel(text='My first item'))
|
||||
tv.add_node(TreeViewLabel(text='My second item'))
|
||||
|
||||
Or, create two nodes attached to a first::
|
||||
|
||||
tv = TreeView()
|
||||
n1 = tv.add_node(TreeViewLabel(text='Item 1'))
|
||||
tv.add_node(TreeViewLabel(text='SubItem 1'), n1)
|
||||
tv.add_node(TreeViewLabel(text='SubItem 2'), n1)
|
||||
|
||||
If you have a large tree structure, perhaps you would need a utility function
|
||||
to populate the tree view::
|
||||
|
||||
def populate_tree_view(tree_view, parent, node):
|
||||
if parent is None:
|
||||
tree_node = tree_view.add_node(TreeViewLabel(text=node['node_id'],
|
||||
is_open=True))
|
||||
else:
|
||||
tree_node = tree_view.add_node(TreeViewLabel(text=node['node_id'],
|
||||
is_open=True), parent)
|
||||
|
||||
for child_node in node['children']:
|
||||
populate_tree_view(tree_view, tree_node, child_node)
|
||||
|
||||
|
||||
tree = {'node_id': '1',
|
||||
'children': [{'node_id': '1.1',
|
||||
'children': [{'node_id': '1.1.1',
|
||||
'children': [{'node_id': '1.1.1.1',
|
||||
'children': []}]},
|
||||
{'node_id': '1.1.2',
|
||||
'children': []},
|
||||
{'node_id': '1.1.3',
|
||||
'children': []}]},
|
||||
{'node_id': '1.2',
|
||||
'children': []}]}
|
||||
|
||||
|
||||
class TreeWidget(FloatLayout):
|
||||
def __init__(self, **kwargs):
|
||||
super(TreeWidget, self).__init__(**kwargs)
|
||||
|
||||
tv = TreeView(root_options=dict(text='Tree One'),
|
||||
hide_root=False,
|
||||
indent_level=4)
|
||||
|
||||
populate_tree_view(tv, None, tree)
|
||||
|
||||
self.add_widget(tv)
|
||||
|
||||
The root widget in the tree view is opened by default and has text set as
|
||||
'Root'. If you want to change that, you can use the
|
||||
:attr:`TreeView.root_options`
|
||||
property. This will pass options to the root widget::
|
||||
|
||||
tv = TreeView(root_options=dict(text='My root label'))
|
||||
|
||||
|
||||
Creating Your Own Node Widget
|
||||
-----------------------------
|
||||
|
||||
For a button node type, combine a :class:`~kivy.uix.button.Button` and a
|
||||
:class:`TreeViewNode` as follows::
|
||||
|
||||
class TreeViewButton(Button, TreeViewNode):
|
||||
pass
|
||||
|
||||
You must know that, for a given node, only the
|
||||
:attr:`~kivy.uix.widget.Widget.size_hint_x` will be honored. The allocated
|
||||
width for the node will depend of the current width of the TreeView and the
|
||||
level of the node. For example, if a node is at level 4, the width
|
||||
allocated will be:
|
||||
|
||||
treeview.width - treeview.indent_start - treeview.indent_level * node.level
|
||||
|
||||
You might have some trouble with that. It is the developer's responsibility to
|
||||
correctly handle adapting the graphical representation nodes, if needed.
|
||||
'''
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.properties import BooleanProperty, ListProperty, ObjectProperty, \
|
||||
AliasProperty, NumericProperty, ReferenceListProperty, ColorProperty
|
||||
|
||||
|
||||
class TreeViewException(Exception):
|
||||
'''Exception for errors in the :class:`TreeView`.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class TreeViewNode(object):
|
||||
'''TreeViewNode class, used to build a node class for a TreeView object.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if self.__class__ is TreeViewNode:
|
||||
raise TreeViewException('You cannot use directly TreeViewNode.')
|
||||
super(TreeViewNode, self).__init__(**kwargs)
|
||||
|
||||
is_leaf = BooleanProperty(True)
|
||||
'''Boolean to indicate whether this node is a leaf or not. Used to adjust
|
||||
the graphical representation.
|
||||
|
||||
:attr:`is_leaf` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to True. It is automatically set to False when child is added.
|
||||
'''
|
||||
|
||||
is_open = BooleanProperty(False)
|
||||
'''Boolean to indicate whether this node is opened or not, in case there
|
||||
are child nodes. This is used to adjust the graphical representation.
|
||||
|
||||
.. warning::
|
||||
|
||||
This property is automatically set by the :class:`TreeView`. You can
|
||||
read but not write it.
|
||||
|
||||
:attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
is_loaded = BooleanProperty(False)
|
||||
'''Boolean to indicate whether this node is already loaded or not. This
|
||||
property is used only if the :class:`TreeView` uses asynchronous loading.
|
||||
|
||||
:attr:`is_loaded` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
is_selected = BooleanProperty(False)
|
||||
'''Boolean to indicate whether this node is selected or not. This is used
|
||||
adjust the graphical representation.
|
||||
|
||||
.. warning::
|
||||
|
||||
This property is automatically set by the :class:`TreeView`. You can
|
||||
read but not write it.
|
||||
|
||||
:attr:`is_selected` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
no_selection = BooleanProperty(False)
|
||||
'''Boolean used to indicate whether selection of the node is allowed or
|
||||
not.
|
||||
|
||||
:attr:`no_selection` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
nodes = ListProperty([])
|
||||
'''List of nodes. The nodes list is different than the children list. A
|
||||
node in the nodes list represents a node on the tree. An item in the
|
||||
children list represents the widget associated with the node.
|
||||
|
||||
.. warning::
|
||||
|
||||
This property is automatically set by the :class:`TreeView`. You can
|
||||
read but not write it.
|
||||
|
||||
:attr:`nodes` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
[].
|
||||
'''
|
||||
|
||||
parent_node = ObjectProperty(None, allownone=True)
|
||||
'''Parent node. This attribute is needed because the :attr:`parent` can be
|
||||
None when the node is not displayed.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`parent_node` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
level = NumericProperty(-1)
|
||||
'''Level of the node.
|
||||
|
||||
:attr:`level` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to -1.
|
||||
'''
|
||||
|
||||
color_selected = ColorProperty([.3, .3, .3, 1.])
|
||||
'''Background color of the node when the node is selected.
|
||||
|
||||
:attr:`color_selected` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [.1, .1, .1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
odd = BooleanProperty(False)
|
||||
'''
|
||||
This property is set by the TreeView widget automatically and is read-only.
|
||||
|
||||
:attr:`odd` is a :class:`~kivy.properties.BooleanProperty` and defaults to
|
||||
False.
|
||||
'''
|
||||
|
||||
odd_color = ColorProperty([1., 1., 1., .0])
|
||||
'''Background color of odd nodes when the node is not selected.
|
||||
|
||||
:attr:`odd_color` is a :class:`~kivy.properties.ColorProperty` and defaults
|
||||
to [1., 1., 1., 0.].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
even_color = ColorProperty([0.5, 0.5, 0.5, 0.1])
|
||||
'''Background color of even nodes when the node is not selected.
|
||||
|
||||
:attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` and defaults
|
||||
to [.5, .5, .5, .1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
|
||||
class TreeViewLabel(Label, TreeViewNode):
|
||||
'''Combines a :class:`~kivy.uix.label.Label` and a :class:`TreeViewNode` to
|
||||
create a :class:`TreeViewLabel` that can be used as a text node in the
|
||||
tree.
|
||||
|
||||
See module documentation for more information.
|
||||
'''
|
||||
|
||||
|
||||
class TreeView(Widget):
|
||||
'''TreeView class. See module documentation for more information.
|
||||
|
||||
:Events:
|
||||
`on_node_expand`: (node, )
|
||||
Fired when a node is being expanded
|
||||
`on_node_collapse`: (node, )
|
||||
Fired when a node is being collapsed
|
||||
'''
|
||||
|
||||
__events__ = ('on_node_expand', 'on_node_collapse')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._trigger_layout = Clock.create_trigger(self._do_layout, -1)
|
||||
super(TreeView, self).__init__(**kwargs)
|
||||
tvlabel = TreeViewLabel(text='Root', is_open=True, level=0)
|
||||
for key, value in self.root_options.items():
|
||||
setattr(tvlabel, key, value)
|
||||
self._root = self.add_node(tvlabel, None)
|
||||
|
||||
trigger = self._trigger_layout
|
||||
fbind = self.fbind
|
||||
fbind('pos', trigger)
|
||||
fbind('size', trigger)
|
||||
fbind('indent_level', trigger)
|
||||
fbind('indent_start', trigger)
|
||||
trigger()
|
||||
|
||||
def add_node(self, node, parent=None):
|
||||
'''Add a new node to the tree.
|
||||
|
||||
:Parameters:
|
||||
`node`: instance of a :class:`TreeViewNode`
|
||||
Node to add into the tree
|
||||
`parent`: instance of a :class:`TreeViewNode`, defaults to None
|
||||
Parent node to attach the new node. If `None`, it is added to
|
||||
the :attr:`root` node.
|
||||
|
||||
:returns:
|
||||
the node `node`.
|
||||
'''
|
||||
# check if the widget is "ok" for a node
|
||||
if not isinstance(node, TreeViewNode):
|
||||
raise TreeViewException(
|
||||
'The node must be a subclass of TreeViewNode')
|
||||
# create node
|
||||
if parent is None and self._root:
|
||||
parent = self._root
|
||||
if parent:
|
||||
parent.is_leaf = False
|
||||
parent.nodes.append(node)
|
||||
node.parent_node = parent
|
||||
node.level = parent.level + 1
|
||||
node.fbind('size', self._trigger_layout)
|
||||
self._trigger_layout()
|
||||
return node
|
||||
|
||||
def remove_node(self, node):
|
||||
'''Removes a node from the tree.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:Parameters:
|
||||
`node`: instance of a :class:`TreeViewNode`
|
||||
Node to remove from the tree. If `node` is :attr:`root`, it is
|
||||
not removed.
|
||||
'''
|
||||
# check if the widget is "ok" for a node
|
||||
if not isinstance(node, TreeViewNode):
|
||||
raise TreeViewException(
|
||||
'The node must be a subclass of TreeViewNode')
|
||||
parent = node.parent_node
|
||||
if parent is not None:
|
||||
if node == self._selected_node:
|
||||
node.is_selected = False
|
||||
self._selected_node = None
|
||||
nodes = parent.nodes
|
||||
if node in nodes:
|
||||
nodes.remove(node)
|
||||
parent.is_leaf = not bool(len(nodes))
|
||||
node.parent_node = None
|
||||
node.funbind('size', self._trigger_layout)
|
||||
self._trigger_layout()
|
||||
|
||||
def on_node_expand(self, node):
|
||||
pass
|
||||
|
||||
def on_node_collapse(self, node):
|
||||
pass
|
||||
|
||||
def select_node(self, node):
|
||||
'''Select a node in the tree.
|
||||
'''
|
||||
if node.no_selection:
|
||||
return
|
||||
if self._selected_node:
|
||||
self._selected_node.is_selected = False
|
||||
node.is_selected = True
|
||||
self._selected_node = node
|
||||
|
||||
def deselect_node(self, *args):
|
||||
'''Deselect any selected node.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
if self._selected_node:
|
||||
self._selected_node.is_selected = False
|
||||
self._selected_node = None
|
||||
|
||||
def toggle_node(self, node):
|
||||
'''Toggle the state of the node (open/collapsed).
|
||||
'''
|
||||
node.is_open = not node.is_open
|
||||
if node.is_open:
|
||||
if self.load_func and not node.is_loaded:
|
||||
self._do_node_load(node)
|
||||
self.dispatch('on_node_expand', node)
|
||||
else:
|
||||
self.dispatch('on_node_collapse', node)
|
||||
self._trigger_layout()
|
||||
|
||||
def get_node_at_pos(self, pos):
|
||||
'''Get the node at the position (x, y).
|
||||
'''
|
||||
x, y = pos
|
||||
for node in self.iterate_open_nodes(self.root):
|
||||
if self.x <= x <= self.right and \
|
||||
node.y <= y <= node.top:
|
||||
return node
|
||||
|
||||
def iterate_open_nodes(self, node=None):
|
||||
'''Generator to iterate over all the expended nodes starting from
|
||||
`node` and down. If `node` is `None`, the generator start with
|
||||
:attr:`root`.
|
||||
|
||||
To get all the open nodes::
|
||||
|
||||
treeview = TreeView()
|
||||
# ... add nodes ...
|
||||
for node in treeview.iterate_open_nodes():
|
||||
print(node)
|
||||
|
||||
'''
|
||||
if not node:
|
||||
node = self.root
|
||||
if self.hide_root and node is self.root:
|
||||
pass
|
||||
else:
|
||||
yield node
|
||||
if not node.is_open:
|
||||
return
|
||||
f = self.iterate_open_nodes
|
||||
for cnode in node.nodes:
|
||||
for ynode in f(cnode):
|
||||
yield ynode
|
||||
|
||||
def iterate_all_nodes(self, node=None):
|
||||
'''Generator to iterate over all nodes from `node` and down whether
|
||||
expanded or not. If `node` is `None`, the generator start with
|
||||
:attr:`root`.
|
||||
'''
|
||||
if not node:
|
||||
node = self.root
|
||||
yield node
|
||||
f = self.iterate_all_nodes
|
||||
for cnode in node.nodes:
|
||||
for ynode in f(cnode):
|
||||
yield ynode
|
||||
|
||||
#
|
||||
# Private
|
||||
#
|
||||
def on_load_func(self, instance, value):
|
||||
if value:
|
||||
Clock.schedule_once(self._do_initial_load)
|
||||
|
||||
def _do_initial_load(self, *largs):
|
||||
if not self.load_func:
|
||||
return
|
||||
self._do_node_load(None)
|
||||
|
||||
def _do_node_load(self, node):
|
||||
gen = self.load_func(self, node)
|
||||
if node:
|
||||
node.is_loaded = True
|
||||
if not gen:
|
||||
return
|
||||
for cnode in gen:
|
||||
self.add_node(cnode, node)
|
||||
|
||||
def on_root_options(self, instance, value):
|
||||
if not self.root:
|
||||
return
|
||||
for key, value in value.items():
|
||||
setattr(self.root, key, value)
|
||||
|
||||
def _do_layout(self, *largs):
|
||||
self.clear_widgets()
|
||||
# display only the one who are is_open
|
||||
self._do_open_node(self.root)
|
||||
# now do layout
|
||||
self._do_layout_node(self.root, 0, self.top)
|
||||
# now iterate for calculating minimum size
|
||||
min_width = min_height = 0
|
||||
for count, node in enumerate(self.iterate_open_nodes(self.root)):
|
||||
node.odd = False if count % 2 else True
|
||||
min_width = max(min_width, node.right - self.x)
|
||||
min_height += node.height
|
||||
self.minimum_size = (min_width, min_height)
|
||||
|
||||
def _do_open_node(self, node):
|
||||
if self.hide_root and node is self.root:
|
||||
height = 0
|
||||
else:
|
||||
self.add_widget(node)
|
||||
height = node.height
|
||||
if not node.is_open:
|
||||
return height
|
||||
for cnode in node.nodes:
|
||||
height += self._do_open_node(cnode)
|
||||
return height
|
||||
|
||||
def _do_layout_node(self, node, level, y):
|
||||
if self.hide_root and node is self.root:
|
||||
level -= 1
|
||||
else:
|
||||
node.x = self.x + self.indent_start + level * self.indent_level
|
||||
node.top = y
|
||||
if node.size_hint_x:
|
||||
node.width = (self.width - (node.x - self.x)) \
|
||||
* node.size_hint_x
|
||||
y -= node.height
|
||||
if not node.is_open:
|
||||
return y
|
||||
for cnode in node.nodes:
|
||||
y = self._do_layout_node(cnode, level + 1, y)
|
||||
return y
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
node = self.get_node_at_pos(touch.pos)
|
||||
if not node:
|
||||
return
|
||||
if node.disabled:
|
||||
return
|
||||
# toggle node or selection ?
|
||||
if node.x - self.indent_start <= touch.x < node.x:
|
||||
self.toggle_node(node)
|
||||
elif node.x <= touch.x:
|
||||
self.select_node(node)
|
||||
node.dispatch('on_touch_down', touch)
|
||||
return True
|
||||
|
||||
#
|
||||
# Private properties
|
||||
#
|
||||
_root = ObjectProperty(None)
|
||||
|
||||
_selected_node = ObjectProperty(None, allownone=True)
|
||||
|
||||
#
|
||||
# Properties
|
||||
#
|
||||
|
||||
minimum_width = NumericProperty(0)
|
||||
'''Minimum width needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.0.9
|
||||
|
||||
:attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
minimum_height = NumericProperty(0)
|
||||
'''Minimum height needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.0.9
|
||||
|
||||
:attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
minimum_size = ReferenceListProperty(minimum_width, minimum_height)
|
||||
'''Minimum size needed to contain all children.
|
||||
|
||||
.. versionadded:: 1.0.9
|
||||
|
||||
:attr:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`minimum_width`, :attr:`minimum_height`) properties.
|
||||
'''
|
||||
|
||||
indent_level = NumericProperty('16dp')
|
||||
'''Width used for the indentation of each level except the first level.
|
||||
|
||||
Computation of indent for each level of the tree is::
|
||||
|
||||
indent = indent_start + level * indent_level
|
||||
|
||||
:attr:`indent_level` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 16.
|
||||
'''
|
||||
|
||||
indent_start = NumericProperty('24dp')
|
||||
'''Indentation width of the level 0 / root node. This is mostly the initial
|
||||
size to accommodate a tree icon (collapsed / expanded). See
|
||||
:attr:`indent_level` for more information about the computation of level
|
||||
indentation.
|
||||
|
||||
:attr:`indent_start` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 24.
|
||||
'''
|
||||
|
||||
hide_root = BooleanProperty(False)
|
||||
'''Use this property to show/hide the initial root node. If True, the root
|
||||
node will be appear as a closed node.
|
||||
|
||||
:attr:`hide_root` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def get_selected_node(self):
|
||||
return self._selected_node
|
||||
|
||||
selected_node = AliasProperty(get_selected_node, None,
|
||||
bind=('_selected_node', ))
|
||||
'''Node selected by :meth:`TreeView.select_node` or by touch.
|
||||
|
||||
:attr:`selected_node` is a :class:`~kivy.properties.AliasProperty` and
|
||||
defaults to None. It is read-only.
|
||||
'''
|
||||
|
||||
def get_root(self):
|
||||
return self._root
|
||||
|
||||
root = AliasProperty(get_root, None, bind=('_root', ))
|
||||
'''Root node.
|
||||
|
||||
By default, the root node widget is a :class:`TreeViewLabel` with text
|
||||
'Root'. If you want to change the default options passed to the widget
|
||||
creation, use the :attr:`root_options` property::
|
||||
|
||||
treeview = TreeView(root_options={
|
||||
'text': 'Root directory',
|
||||
'font_size': 15})
|
||||
|
||||
:attr:`root_options` will change the properties of the
|
||||
:class:`TreeViewLabel` instance. However, you cannot change the class used
|
||||
for root node yet.
|
||||
|
||||
:attr:`root` is an :class:`~kivy.properties.AliasProperty` and defaults to
|
||||
None. It is read-only. However, the content of the widget can be changed.
|
||||
'''
|
||||
|
||||
root_options = ObjectProperty({})
|
||||
'''Default root options to pass for root widget. See :attr:`root` property
|
||||
for more information about the usage of root_options.
|
||||
|
||||
:attr:`root_options` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
|
||||
load_func = ObjectProperty(None)
|
||||
'''Callback to use for asynchronous loading. If set, asynchronous loading
|
||||
will be automatically done. The callback must act as a Python generator
|
||||
function, using yield to send data back to the treeview.
|
||||
|
||||
The callback should be in the format::
|
||||
|
||||
def callback(treeview, node):
|
||||
for name in ('Item 1', 'Item 2'):
|
||||
yield TreeViewLabel(text=name)
|
||||
|
||||
:attr:`load_func` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.app import App
|
||||
|
||||
class TestApp(App):
|
||||
|
||||
def build(self):
|
||||
tv = TreeView(hide_root=True)
|
||||
add = tv.add_node
|
||||
root = add(TreeViewLabel(text='Level 1, entry 1', is_open=True))
|
||||
for x in range(5):
|
||||
add(TreeViewLabel(text='Element %d' % x), root)
|
||||
root2 = add(TreeViewLabel(text='Level 1, entry 2', is_open=False))
|
||||
for x in range(24):
|
||||
add(TreeViewLabel(text='Element %d' % x), root2)
|
||||
for x in range(5):
|
||||
add(TreeViewLabel(text='Element %d' % x), root)
|
||||
root2 = add(TreeViewLabel(text='Element childs 2', is_open=False),
|
||||
root)
|
||||
for x in range(24):
|
||||
add(TreeViewLabel(text='Element %d' % x), root2)
|
||||
return tv
|
||||
TestApp().run()
|
||||
@@ -0,0 +1,302 @@
|
||||
'''
|
||||
Video
|
||||
=====
|
||||
|
||||
The :class:`Video` widget is used to display video files and streams.
|
||||
Depending on your Video core provider, platform, and plugins, you will
|
||||
be able to play different formats. For example, the pygame video
|
||||
provider only supports MPEG1 on Linux and OSX. GStreamer is more
|
||||
versatile, and can read many video containers and codecs such as MKV,
|
||||
OGV, AVI, MOV, FLV (if the correct gstreamer plugins are installed). Our
|
||||
:class:`~kivy.core.video.VideoBase` implementation is used under the
|
||||
hood.
|
||||
|
||||
Video loading is asynchronous - many properties are not available until
|
||||
the video is loaded (when the texture is created)::
|
||||
|
||||
def on_position_change(instance, value):
|
||||
print('The position in the video is', value)
|
||||
|
||||
def on_duration_change(instance, value):
|
||||
print('The duration of the video is', value)
|
||||
|
||||
video = Video(source='PandaSneezes.avi')
|
||||
video.bind(
|
||||
position=on_position_change,
|
||||
duration=on_duration_change
|
||||
)
|
||||
|
||||
One can define a preview image which gets displayed until the video is
|
||||
started/loaded by passing ``preview`` to the constructor::
|
||||
|
||||
video = Video(
|
||||
source='PandaSneezes.avi',
|
||||
preview='PandaSneezes_preview.png'
|
||||
)
|
||||
|
||||
One can display the placeholder image when the video stops by reacting on eos::
|
||||
|
||||
def on_eos_change(self, inst, val):
|
||||
if val and self.preview:
|
||||
self.set_texture_from_resource(self.preview)
|
||||
|
||||
video.bind(eos=on_eos_change)
|
||||
'''
|
||||
|
||||
__all__ = ('Video', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.image import Image
|
||||
from kivy.core.video import Video as CoreVideo
|
||||
from kivy.resources import resource_find
|
||||
from kivy.properties import (BooleanProperty, NumericProperty, ObjectProperty,
|
||||
OptionProperty, StringProperty)
|
||||
|
||||
|
||||
class Video(Image):
|
||||
'''Video class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
preview = StringProperty(None, allownone=True)
|
||||
'''Filename / source of a preview image displayed before video starts.
|
||||
|
||||
:attr:`preview` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
|
||||
If set, it gets displayed until the video is loaded/started.
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
'''
|
||||
|
||||
state = OptionProperty('stop', options=('play', 'pause', 'stop'))
|
||||
'''String, indicates whether to play, pause, or stop the video::
|
||||
|
||||
# start playing the video at creation
|
||||
video = Video(source='movie.mkv', state='play')
|
||||
|
||||
# create the video, and start later
|
||||
video = Video(source='movie.mkv')
|
||||
# and later
|
||||
video.state = 'play'
|
||||
|
||||
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'stop'.
|
||||
'''
|
||||
|
||||
play = BooleanProperty(False, deprecated=True)
|
||||
'''
|
||||
.. deprecated:: 1.4.0
|
||||
Use :attr:`state` instead.
|
||||
|
||||
Boolean, indicates whether the video is playing or not.
|
||||
You can start/stop the video by setting this property::
|
||||
|
||||
# start playing the video at creation
|
||||
video = Video(source='movie.mkv', play=True)
|
||||
|
||||
# create the video, and start later
|
||||
video = Video(source='movie.mkv')
|
||||
# and later
|
||||
video.play = True
|
||||
|
||||
:attr:`play` is a :class:`~kivy.properties.BooleanProperty` and defaults to
|
||||
False.
|
||||
|
||||
.. deprecated:: 1.4.0
|
||||
Use :attr:`state` instead.
|
||||
'''
|
||||
|
||||
eos = BooleanProperty(False)
|
||||
'''Boolean, indicates whether the video has finished playing or not
|
||||
(reached the end of the stream).
|
||||
|
||||
:attr:`eos` is a :class:`~kivy.properties.BooleanProperty` and defaults to
|
||||
False.
|
||||
'''
|
||||
|
||||
loaded = BooleanProperty(False)
|
||||
'''Boolean, indicates whether the video is loaded and ready for playback
|
||||
or not.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
:attr:`loaded` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
position = NumericProperty(-1)
|
||||
'''Position of the video between 0 and :attr:`duration`. The position
|
||||
defaults to -1 and is set to a real position when the video is loaded.
|
||||
|
||||
:attr:`position` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to -1.
|
||||
'''
|
||||
|
||||
duration = NumericProperty(-1)
|
||||
'''Duration of the video. The duration defaults to -1, and is set to a real
|
||||
duration when the video is loaded.
|
||||
|
||||
:attr:`duration` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to -1.
|
||||
'''
|
||||
|
||||
volume = NumericProperty(1.)
|
||||
'''Volume of the video, in the range 0-1. 1 means full volume, 0
|
||||
means mute.
|
||||
|
||||
:attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 1.
|
||||
'''
|
||||
|
||||
options = ObjectProperty({})
|
||||
'''Options to pass at Video core object creation.
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
:attr:`options` is an :class:`kivy.properties.ObjectProperty` and defaults
|
||||
to {}.
|
||||
'''
|
||||
|
||||
_video_load_event = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._video = None
|
||||
super(Video, self).__init__(**kwargs)
|
||||
self.fbind('source', self._trigger_video_load)
|
||||
|
||||
if "eos" in kwargs:
|
||||
self.options["eos"] = kwargs["eos"]
|
||||
if self.source:
|
||||
self._trigger_video_load()
|
||||
|
||||
def texture_update(self, *largs):
|
||||
if self.preview:
|
||||
self.set_texture_from_resource(self.preview)
|
||||
else:
|
||||
self.set_texture_from_resource(self.source)
|
||||
|
||||
def seek(self, percent, precise=True):
|
||||
'''Change the position to a percentage (strictly, a proportion)
|
||||
of duration.
|
||||
|
||||
:Parameters:
|
||||
`percent`: float or int
|
||||
Position to seek as a proportion of the total duration,
|
||||
must be between 0-1.
|
||||
`precise`: bool, defaults to True
|
||||
Precise seeking is slower, but seeks to exact requested
|
||||
percent.
|
||||
|
||||
.. warning::
|
||||
Calling seek() before the video is loaded has no effect.
|
||||
|
||||
.. versionadded:: 1.2.0
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
The `precise` keyword argument has been added.
|
||||
'''
|
||||
if self._video is None:
|
||||
raise Exception('Video not loaded.')
|
||||
self._video.seek(percent, precise=precise)
|
||||
|
||||
def _trigger_video_load(self, *largs):
|
||||
ev = self._video_load_event
|
||||
if ev is None:
|
||||
ev = self._video_load_event = Clock.schedule_once(
|
||||
self._do_video_load, -1)
|
||||
ev()
|
||||
|
||||
def _do_video_load(self, *largs):
|
||||
if CoreVideo is None:
|
||||
return
|
||||
self.unload()
|
||||
if not self.source:
|
||||
self._video = None
|
||||
self.texture = None
|
||||
else:
|
||||
filename = self.source
|
||||
# Check if filename is not url
|
||||
if '://' not in filename:
|
||||
filename = resource_find(filename)
|
||||
self._video = CoreVideo(filename=filename, **self.options)
|
||||
self._video.volume = self.volume
|
||||
self._video.bind(on_load=self._on_load,
|
||||
on_frame=self._on_video_frame,
|
||||
on_eos=self._on_eos)
|
||||
if self.state == 'play' or self.play:
|
||||
self._video.play()
|
||||
self.duration = 1.
|
||||
self.position = 0.
|
||||
|
||||
def on_play(self, instance, value):
|
||||
value = 'play' if value else 'stop'
|
||||
return self.on_state(instance, value)
|
||||
|
||||
def on_state(self, instance, value):
|
||||
if not self._video:
|
||||
return
|
||||
if value == 'play':
|
||||
if self.eos:
|
||||
self._video.stop()
|
||||
self._video.position = 0.
|
||||
self.eos = False
|
||||
self._video.play()
|
||||
elif value == 'pause':
|
||||
self._video.pause()
|
||||
else:
|
||||
self._video.stop()
|
||||
self._video.position = 0
|
||||
|
||||
def _on_video_frame(self, *largs):
|
||||
video = self._video
|
||||
if not video:
|
||||
return
|
||||
self.duration = video.duration
|
||||
self.position = video.position
|
||||
self.texture = video.texture
|
||||
self.canvas.ask_update()
|
||||
|
||||
def _on_eos(self, *largs):
|
||||
if not self._video or self._video.eos != 'loop':
|
||||
self.state = 'stop'
|
||||
self.eos = True
|
||||
|
||||
def _on_load(self, *largs):
|
||||
self.loaded = True
|
||||
self._on_video_frame(largs)
|
||||
|
||||
def on_volume(self, instance, value):
|
||||
if self._video:
|
||||
self._video.volume = value
|
||||
|
||||
def unload(self):
|
||||
'''Unload the video. The playback will be stopped.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
if self._video:
|
||||
self._video.stop()
|
||||
self._video.unload()
|
||||
self._video = None
|
||||
self.loaded = False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.app import App
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: %s file" % sys.argv[0])
|
||||
sys.exit(1)
|
||||
|
||||
class VideoApp(App):
|
||||
def build(self):
|
||||
self.v = Video(source=sys.argv[1], state='play')
|
||||
self.v.bind(state=self.replay)
|
||||
return self.v
|
||||
|
||||
def replay(self, *args):
|
||||
if self.v.state == 'stop':
|
||||
self.v.state = 'play'
|
||||
|
||||
VideoApp().run()
|
||||
@@ -0,0 +1,697 @@
|
||||
'''
|
||||
Video player
|
||||
============
|
||||
|
||||
.. versionadded:: 1.2.0
|
||||
|
||||
The video player widget can be used to play video and let the user control the
|
||||
play/pausing, volume and position. The widget cannot be customized much because
|
||||
of the complex assembly of numerous base widgets.
|
||||
|
||||
.. image:: images/videoplayer.jpg
|
||||
:align: center
|
||||
|
||||
Annotations
|
||||
-----------
|
||||
|
||||
If you want to display text at a specific time and for a certain duration,
|
||||
consider annotations. An annotation file has a ".jsa" extension. The player
|
||||
will automatically load the associated annotation file if it exists.
|
||||
|
||||
An annotation file is JSON-based, providing a list of label dictionary items.
|
||||
The key and value must match one of the :class:`VideoPlayerAnnotation` items.
|
||||
For example, here is a short version of a jsa file that you can find in
|
||||
`examples/widgets/cityCC0.jsa`::
|
||||
|
||||
|
||||
[
|
||||
{"start": 0, "duration": 2,
|
||||
"text": "This is an example of annotation"},
|
||||
{"start": 2, "duration": 2,
|
||||
"bgcolor": [0.5, 0.2, 0.4, 0.5],
|
||||
"text": "You can change the background color"}
|
||||
]
|
||||
|
||||
For our cityCC0.mpg example, the result will be:
|
||||
|
||||
.. image:: images/videoplayer-annotation.jpg
|
||||
:align: center
|
||||
|
||||
If you want to experiment with annotation files, test with::
|
||||
|
||||
python -m kivy.uix.videoplayer examples/widgets/cityCC0.mpg
|
||||
|
||||
Fullscreen
|
||||
----------
|
||||
|
||||
The video player can play the video in fullscreen, if
|
||||
:attr:`VideoPlayer.allow_fullscreen` is activated by a double-tap on
|
||||
the video. By default, if the video is smaller than the Window, it will be not
|
||||
stretched.
|
||||
|
||||
You can allow stretching by passing custom options to a
|
||||
:class:`VideoPlayer` instance::
|
||||
|
||||
player = VideoPlayer(source='myvideo.avi', state='play',
|
||||
options={'fit_mode': 'contain'})
|
||||
|
||||
End-of-stream behavior
|
||||
----------------------
|
||||
|
||||
You can specify what happens when the video has finished playing by passing an
|
||||
`eos` (end of stream) directive to the underlying
|
||||
:class:`~kivy.core.video.VideoBase` class. `eos` can be one of 'stop', 'pause'
|
||||
or 'loop' and defaults to 'stop'. For example, in order to loop the video::
|
||||
|
||||
player = VideoPlayer(source='myvideo.avi', state='play',
|
||||
options={'eos': 'loop'})
|
||||
|
||||
.. note::
|
||||
|
||||
The `eos` property of the VideoBase class is a string specifying the
|
||||
end-of-stream behavior. This property differs from the `eos`
|
||||
properties of the :class:`VideoPlayer` and
|
||||
:class:`~kivy.uix.video.Video` classes, whose `eos`
|
||||
property is simply a boolean indicating that the end of the file has
|
||||
been reached.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('VideoPlayer', 'VideoPlayerAnnotation')
|
||||
|
||||
from json import load
|
||||
from os.path import exists
|
||||
from kivy.properties import ObjectProperty, StringProperty, BooleanProperty, \
|
||||
NumericProperty, DictProperty, OptionProperty
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.video import Video
|
||||
from kivy.uix.video import Image
|
||||
from kivy.factory import Factory
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
|
||||
|
||||
class VideoPlayerVolume(Image):
|
||||
video = ObjectProperty(None)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return False
|
||||
touch.grab(self)
|
||||
# save the current volume and delta to it
|
||||
touch.ud[self.uid] = [self.video.volume, 0]
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
# calculate delta
|
||||
dy = abs(touch.y - touch.oy)
|
||||
if dy > 10:
|
||||
dy = min(dy - 10, 100)
|
||||
touch.ud[self.uid][1] = dy
|
||||
self.video.volume = dy / 100.
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
dy = abs(touch.y - touch.oy)
|
||||
if dy < 10:
|
||||
if self.video.volume > 0:
|
||||
self.video.volume = 0
|
||||
else:
|
||||
self.video.volume = 1.
|
||||
|
||||
|
||||
class VideoPlayerPlayPause(Image):
|
||||
video = ObjectProperty(None)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
'''.. versionchanged:: 1.4.0'''
|
||||
if self.collide_point(*touch.pos):
|
||||
if self.video.state == 'play':
|
||||
self.video.state = 'pause'
|
||||
else:
|
||||
self.video.state = 'play'
|
||||
return True
|
||||
|
||||
|
||||
class VideoPlayerStop(Image):
|
||||
video = ObjectProperty(None)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
self.video.state = 'stop'
|
||||
self.video.position = 0
|
||||
return True
|
||||
|
||||
|
||||
class VideoPlayerProgressBar(ProgressBar):
|
||||
video = ObjectProperty(None)
|
||||
seek = NumericProperty(None, allownone=True)
|
||||
alpha = NumericProperty(1.)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(VideoPlayerProgressBar, self).__init__(**kwargs)
|
||||
self.bubble = Factory.Bubble(size=(50, 44))
|
||||
self.bubble_label = Factory.Label(text='0:00')
|
||||
self.bubble.add_widget(self.bubble_label)
|
||||
self.add_widget(self.bubble)
|
||||
|
||||
update = self._update_bubble
|
||||
fbind = self.fbind
|
||||
fbind('pos', update)
|
||||
fbind('size', update)
|
||||
fbind('seek', update)
|
||||
|
||||
def on_video(self, instance, value):
|
||||
self.video.bind(position=self._update_bubble,
|
||||
state=self._showhide_bubble)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
self._show_bubble()
|
||||
touch.grab(self)
|
||||
self._update_seek(touch.x)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
self._update_seek(touch.x)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
if self.seek:
|
||||
self.video.seek(self.seek)
|
||||
self.seek = None
|
||||
self._hide_bubble()
|
||||
return True
|
||||
|
||||
def _update_seek(self, x):
|
||||
if self.width == 0:
|
||||
return
|
||||
x = max(self.x, min(self.right, x)) - self.x
|
||||
self.seek = x / float(self.width)
|
||||
|
||||
def _show_bubble(self):
|
||||
self.alpha = 1
|
||||
Animation.stop_all(self, 'alpha')
|
||||
|
||||
def _hide_bubble(self):
|
||||
self.alpha = 1.
|
||||
Animation(alpha=0, d=4, t='in_out_expo').start(self)
|
||||
|
||||
def on_alpha(self, instance, value):
|
||||
self.bubble.background_color = (1, 1, 1, value)
|
||||
self.bubble_label.color = (1, 1, 1, value)
|
||||
|
||||
def _update_bubble(self, *l):
|
||||
seek = self.seek
|
||||
if self.seek is None:
|
||||
if self.video.duration == 0:
|
||||
seek = 0
|
||||
else:
|
||||
seek = self.video.position / self.video.duration
|
||||
# convert to minutes:seconds
|
||||
d = self.video.duration * seek
|
||||
minutes = int(d / 60)
|
||||
seconds = int(d - (minutes * 60))
|
||||
# fix bubble label & position
|
||||
self.bubble_label.text = '%d:%02d' % (minutes, seconds)
|
||||
self.bubble.center_x = self.x + seek * self.width
|
||||
self.bubble.y = self.top
|
||||
|
||||
def _showhide_bubble(self, instance, value):
|
||||
if value == 'play':
|
||||
self._hide_bubble()
|
||||
else:
|
||||
self._show_bubble()
|
||||
|
||||
|
||||
class VideoPlayerPreview(FloatLayout):
|
||||
source = ObjectProperty(None)
|
||||
video = ObjectProperty(None)
|
||||
click_done = BooleanProperty(False)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos) and not self.click_done:
|
||||
self.click_done = True
|
||||
self.video.state = 'play'
|
||||
return True
|
||||
|
||||
|
||||
class VideoPlayerAnnotation(Label):
|
||||
'''Annotation class used for creating annotation labels.
|
||||
|
||||
Additional keys are available:
|
||||
|
||||
* bgcolor: [r, g, b, a] - background color of the text box
|
||||
* bgsource: 'filename' - background image used for the background text box
|
||||
* border: (n, e, s, w) - border used for the background image
|
||||
|
||||
'''
|
||||
start = NumericProperty(0)
|
||||
'''Start time of the annotation.
|
||||
|
||||
:attr:`start` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 0.
|
||||
'''
|
||||
|
||||
duration = NumericProperty(1)
|
||||
'''Duration of the annotation.
|
||||
|
||||
:attr:`duration` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
annotation = DictProperty({})
|
||||
|
||||
def on_annotation(self, instance, ann):
|
||||
for key, value in ann.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class VideoPlayer(GridLayout):
|
||||
'''VideoPlayer class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
source = StringProperty('')
|
||||
'''Source of the video to read.
|
||||
|
||||
:attr:`source` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to ''.
|
||||
|
||||
.. versionchanged:: 1.4.0
|
||||
'''
|
||||
|
||||
thumbnail = StringProperty('')
|
||||
'''Thumbnail of the video to show. If None, VideoPlayer will try to find
|
||||
the thumbnail from the :attr:`source` + '.png'.
|
||||
|
||||
:attr:`thumbnail` a :class:`~kivy.properties.StringProperty` and defaults
|
||||
to ''.
|
||||
|
||||
.. versionchanged:: 1.4.0
|
||||
'''
|
||||
|
||||
duration = NumericProperty(-1)
|
||||
'''Duration of the video. The duration defaults to -1 and is set to the
|
||||
real duration when the video is loaded.
|
||||
|
||||
:attr:`duration` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to -1.
|
||||
'''
|
||||
|
||||
position = NumericProperty(0)
|
||||
'''Position of the video between 0 and :attr:`duration`. The position
|
||||
defaults to -1 and is set to the real position when the video is loaded.
|
||||
|
||||
:attr:`position` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to -1.
|
||||
'''
|
||||
|
||||
volume = NumericProperty(1.0)
|
||||
'''Volume of the video in the range 0-1. 1 means full volume and 0 means
|
||||
mute.
|
||||
|
||||
:attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 1.
|
||||
'''
|
||||
|
||||
state = OptionProperty('stop', options=('play', 'pause', 'stop'))
|
||||
'''String, indicates whether to play, pause, or stop the video::
|
||||
|
||||
# start playing the video at creation
|
||||
video = VideoPlayer(source='movie.mkv', state='play')
|
||||
|
||||
# create the video, and start later
|
||||
video = VideoPlayer(source='movie.mkv')
|
||||
# and later
|
||||
video.state = 'play'
|
||||
|
||||
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'stop'.
|
||||
'''
|
||||
|
||||
play = BooleanProperty(False, deprecated=True)
|
||||
'''
|
||||
.. deprecated:: 1.4.0
|
||||
Use :attr:`state` instead.
|
||||
|
||||
Boolean, indicates whether the video is playing or not. You can start/stop
|
||||
the video by setting this property::
|
||||
|
||||
# start playing the video at creation
|
||||
video = VideoPlayer(source='movie.mkv', play=True)
|
||||
|
||||
# create the video, and start later
|
||||
video = VideoPlayer(source='movie.mkv')
|
||||
# and later
|
||||
video.play = True
|
||||
|
||||
:attr:`play` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
image_overlay_play = StringProperty(
|
||||
'atlas://data/images/defaulttheme/player-play-overlay')
|
||||
'''Image filename used to show a "play" overlay when the video has not yet
|
||||
started.
|
||||
|
||||
:attr:`image_overlay_play` is a
|
||||
:class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/player-play-overlay'.
|
||||
|
||||
'''
|
||||
|
||||
image_loading = StringProperty('data/images/image-loading.zip')
|
||||
'''Image filename used when the video is loading.
|
||||
|
||||
:attr:`image_loading` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'data/images/image-loading.zip'.
|
||||
'''
|
||||
|
||||
image_play = StringProperty(
|
||||
'atlas://data/images/defaulttheme/media-playback-start')
|
||||
'''Image filename used for the "Play" button.
|
||||
|
||||
:attr:`image_play` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/media-playback-start'.
|
||||
'''
|
||||
|
||||
image_stop = StringProperty(
|
||||
'atlas://data/images/defaulttheme/media-playback-stop')
|
||||
'''Image filename used for the "Stop" button.
|
||||
|
||||
:attr:`image_stop` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/media-playback-stop'.
|
||||
'''
|
||||
|
||||
image_pause = StringProperty(
|
||||
'atlas://data/images/defaulttheme/media-playback-pause')
|
||||
'''Image filename used for the "Pause" button.
|
||||
|
||||
:attr:`image_pause` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/media-playback-pause'.
|
||||
'''
|
||||
|
||||
image_volumehigh = StringProperty(
|
||||
'atlas://data/images/defaulttheme/audio-volume-high')
|
||||
'''Image filename used for the volume icon when the volume is high.
|
||||
|
||||
:attr:`image_volumehigh` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/audio-volume-high'.
|
||||
'''
|
||||
|
||||
image_volumemedium = StringProperty(
|
||||
'atlas://data/images/defaulttheme/audio-volume-medium')
|
||||
'''Image filename used for the volume icon when the volume is medium.
|
||||
|
||||
:attr:`image_volumemedium` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/audio-volume-medium'.
|
||||
'''
|
||||
|
||||
image_volumelow = StringProperty(
|
||||
'atlas://data/images/defaulttheme/audio-volume-low')
|
||||
'''Image filename used for the volume icon when the volume is low.
|
||||
|
||||
:attr:`image_volumelow` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/audio-volume-low'.
|
||||
'''
|
||||
|
||||
image_volumemuted = StringProperty(
|
||||
'atlas://data/images/defaulttheme/audio-volume-muted')
|
||||
'''Image filename used for the volume icon when the volume is muted.
|
||||
|
||||
:attr:`image_volumemuted` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/audio-volume-muted'.
|
||||
'''
|
||||
|
||||
annotations = StringProperty('')
|
||||
'''If set, it will be used for reading annotations box.
|
||||
|
||||
:attr:`annotations` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to ''.
|
||||
'''
|
||||
|
||||
fullscreen = BooleanProperty(False)
|
||||
'''Switch to fullscreen view. This should be used with care. When
|
||||
activated, the widget will remove itself from its parent, remove all
|
||||
children from the window and will add itself to it. When fullscreen is
|
||||
unset, all the previous children are restored and the widget is restored to
|
||||
its previous parent.
|
||||
|
||||
.. warning::
|
||||
|
||||
The re-add operation doesn't care about the index position of its
|
||||
children within the parent.
|
||||
|
||||
:attr:`fullscreen` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
allow_fullscreen = BooleanProperty(True)
|
||||
'''By default, you can double-tap on the video to make it fullscreen. Set
|
||||
this property to False to prevent this behavior.
|
||||
|
||||
:attr:`allow_fullscreen` is a :class:`~kivy.properties.BooleanProperty`
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
options = DictProperty({})
|
||||
'''Optional parameters can be passed to a :class:`~kivy.uix.video.Video`
|
||||
instance with this property.
|
||||
|
||||
:attr:`options` a :class:`~kivy.properties.DictProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
|
||||
# internals
|
||||
container = ObjectProperty(None)
|
||||
|
||||
_video_load_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._video = None
|
||||
self._image = None
|
||||
self._annotations = ''
|
||||
self._annotations_labels = []
|
||||
super(VideoPlayer, self).__init__(**kwargs)
|
||||
update_thumbnail = self._update_thumbnail
|
||||
update_annotations = self._update_annotations
|
||||
fbind = self.fbind
|
||||
fbind('thumbnail', update_thumbnail)
|
||||
fbind('annotations', update_annotations)
|
||||
|
||||
if self.source:
|
||||
self._trigger_video_load()
|
||||
|
||||
def _trigger_video_load(self, *largs):
|
||||
ev = self._video_load_ev
|
||||
if ev is None:
|
||||
ev = self._video_load_ev = Clock.schedule_once(self._do_video_load,
|
||||
-1)
|
||||
ev()
|
||||
|
||||
def _try_load_default_thumbnail(self, *largs):
|
||||
if not self.thumbnail:
|
||||
filename = self.source.rsplit('.', 1)
|
||||
thumbnail = filename[0] + '.png'
|
||||
if exists(thumbnail):
|
||||
self._load_thumbnail(thumbnail)
|
||||
|
||||
def _try_load_default_annotations(self, *largs):
|
||||
if not self.annotations:
|
||||
filename = self.source.rsplit('.', 1)
|
||||
annotations = filename[0] + '.jsa'
|
||||
if exists(annotations):
|
||||
self._load_annotations(annotations)
|
||||
|
||||
def on_source(self, instance, value):
|
||||
# By default, VideoPlayer should look for thumbnail and annotations
|
||||
# with the same filename (except extension) of the source (video) file.
|
||||
Clock.schedule_once(self._try_load_default_thumbnail, -1)
|
||||
Clock.schedule_once(self._try_load_default_annotations, -1)
|
||||
if self._video is not None:
|
||||
self._video.unload()
|
||||
self._video = None
|
||||
if value:
|
||||
self._trigger_video_load()
|
||||
|
||||
def _update_thumbnail(self, *largs):
|
||||
self._load_thumbnail(self.thumbnail)
|
||||
|
||||
def _update_annotations(self, *largs):
|
||||
self._load_annotations(self.annotations)
|
||||
|
||||
def on_image_overlay_play(self, instance, value):
|
||||
self._image.image_overlay_play = value
|
||||
|
||||
def on_image_loading(self, instance, value):
|
||||
self._image.image_loading = value
|
||||
|
||||
def _load_thumbnail(self, thumbnail):
|
||||
if not self.container:
|
||||
return
|
||||
self.container.clear_widgets()
|
||||
if thumbnail:
|
||||
self._image = VideoPlayerPreview(source=thumbnail, video=self)
|
||||
self.container.add_widget(self._image)
|
||||
|
||||
def _load_annotations(self, annotations):
|
||||
if not self.container:
|
||||
return
|
||||
self._annotations_labels = []
|
||||
if annotations:
|
||||
with open(annotations, 'r') as fd:
|
||||
self._annotations = load(fd)
|
||||
if self._annotations:
|
||||
for ann in self._annotations:
|
||||
self._annotations_labels.append(
|
||||
VideoPlayerAnnotation(annotation=ann))
|
||||
|
||||
def on_state(self, instance, value):
|
||||
if self._video is not None:
|
||||
self._video.state = value
|
||||
|
||||
def _set_state(self, instance, value):
|
||||
self.state = value
|
||||
|
||||
def _do_video_load(self, *largs):
|
||||
self._video = Video(source=self.source, state=self.state,
|
||||
volume=self.volume, pos_hint={'x': 0, 'y': 0},
|
||||
**self.options)
|
||||
self._video.bind(texture=self._play_started,
|
||||
duration=self.setter('duration'),
|
||||
position=self.setter('position'),
|
||||
volume=self.setter('volume'),
|
||||
state=self._set_state)
|
||||
|
||||
def on_play(self, instance, value):
|
||||
value = 'play' if value else 'stop'
|
||||
return self.on_state(instance, value)
|
||||
|
||||
def on_volume(self, instance, value):
|
||||
if not self._video:
|
||||
return
|
||||
self._video.volume = value
|
||||
|
||||
def on_position(self, instance, value):
|
||||
labels = self._annotations_labels
|
||||
if not labels:
|
||||
return
|
||||
for label in labels:
|
||||
start = label.start
|
||||
duration = label.duration
|
||||
if start > value or (start + duration) < value:
|
||||
if label.parent:
|
||||
label.parent.remove_widget(label)
|
||||
elif label.parent is None:
|
||||
self.container.add_widget(label)
|
||||
|
||||
def seek(self, percent, precise=True):
|
||||
'''Change the position to a percentage (strictly, a proportion)
|
||||
of duration.
|
||||
|
||||
:Parameters:
|
||||
`percent`: float or int
|
||||
Position to seek as a proportion of total duration, must
|
||||
be between 0-1.
|
||||
`precise`: bool, defaults to True
|
||||
Precise seeking is slower, but seeks to exact requested
|
||||
percent.
|
||||
|
||||
.. warning::
|
||||
Calling seek() before the video is loaded has no effect.
|
||||
|
||||
.. versionadded:: 1.2.0
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
The `precise` keyword argument has been added.
|
||||
'''
|
||||
if not self._video:
|
||||
return
|
||||
self._video.seek(percent, precise=precise)
|
||||
|
||||
def _play_started(self, instance, value):
|
||||
self.container.clear_widgets()
|
||||
self.container.add_widget(self._video)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return False
|
||||
if touch.is_double_tap and self.allow_fullscreen:
|
||||
self.fullscreen = not self.fullscreen
|
||||
return True
|
||||
return super(VideoPlayer, self).on_touch_down(touch)
|
||||
|
||||
def on_fullscreen(self, instance, value):
|
||||
window = self.get_parent_window()
|
||||
if not window:
|
||||
Logger.warning('VideoPlayer: Cannot switch to fullscreen, '
|
||||
'window not found.')
|
||||
if value:
|
||||
self.fullscreen = False
|
||||
return
|
||||
if not self.parent:
|
||||
Logger.warning('VideoPlayer: Cannot switch to fullscreen, '
|
||||
'no parent.')
|
||||
if value:
|
||||
self.fullscreen = False
|
||||
return
|
||||
|
||||
if value:
|
||||
self._fullscreen_state = state = {
|
||||
'parent': self.parent,
|
||||
'pos': self.pos,
|
||||
'size': self.size,
|
||||
'pos_hint': self.pos_hint,
|
||||
'size_hint': self.size_hint,
|
||||
'window_children': window.children[:]}
|
||||
|
||||
# remove all window children
|
||||
for child in window.children[:]:
|
||||
window.remove_widget(child)
|
||||
|
||||
# put the video in fullscreen
|
||||
if state['parent'] is not window:
|
||||
state['parent'].remove_widget(self)
|
||||
window.add_widget(self)
|
||||
|
||||
# ensure the video widget is in 0, 0, and the size will be
|
||||
# readjusted
|
||||
self.pos = (0, 0)
|
||||
self.size = (100, 100)
|
||||
self.pos_hint = {}
|
||||
self.size_hint = (1, 1)
|
||||
else:
|
||||
state = self._fullscreen_state
|
||||
window.remove_widget(self)
|
||||
for child in state['window_children']:
|
||||
window.add_widget(child)
|
||||
self.pos_hint = state['pos_hint']
|
||||
self.size_hint = state['size_hint']
|
||||
self.pos = state['pos']
|
||||
self.size = state['size']
|
||||
if state['parent'] is not window:
|
||||
state['parent'].add_widget(self)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from kivy.base import runTouchApp
|
||||
player = VideoPlayer(source=sys.argv[1])
|
||||
runTouchApp(player)
|
||||
if player:
|
||||
player.state = 'stop'
|
||||
@@ -0,0 +1,879 @@
|
||||
'''
|
||||
VKeyboard
|
||||
=========
|
||||
|
||||
.. image:: images/vkeyboard.jpg
|
||||
:align: right
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
|
||||
VKeyboard is an onscreen keyboard for Kivy. Its operation is intended to be
|
||||
transparent to the user. Using the widget directly is NOT recommended. Read the
|
||||
section `Request keyboard`_ first.
|
||||
|
||||
Modes
|
||||
-----
|
||||
|
||||
This virtual keyboard has a docked and free mode:
|
||||
|
||||
* docked mode (:attr:`VKeyboard.docked` = True)
|
||||
Generally used when only one person is using the computer, like a tablet or
|
||||
personal computer etc.
|
||||
* free mode: (:attr:`VKeyboard.docked` = False)
|
||||
Mostly for multitouch surfaces. This mode allows multiple virtual
|
||||
keyboards to be used on the screen.
|
||||
|
||||
If the docked mode changes, you need to manually call
|
||||
:meth:`VKeyboard.setup_mode` otherwise the change will have no impact.
|
||||
During that call, the VKeyboard, implemented on top of a
|
||||
:class:`~kivy.uix.scatter.Scatter`, will change the
|
||||
behavior of the scatter and position the keyboard near the target (if target
|
||||
and docked mode is set).
|
||||
|
||||
|
||||
Layouts
|
||||
-------
|
||||
|
||||
The virtual keyboard is able to load a custom layout. If you create a new
|
||||
layout and put the JSON in :file:`<kivy_data_dir>/keyboards/<layoutid>.json`,
|
||||
you can load it by setting :attr:`VKeyboard.layout` to your layoutid.
|
||||
|
||||
The JSON must be structured like this::
|
||||
|
||||
{
|
||||
"title": "Title of your layout",
|
||||
"description": "Description of your layout",
|
||||
"cols": 15,
|
||||
"rows": 5,
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
Then, you need to describe the keys in each row, for either a "normal",
|
||||
"shift" or a "special" (added in version 1.9.0) mode. Keys for this row
|
||||
data must be named `normal_<row>`, `shift_<row>` and `special_<row>`.
|
||||
Replace `row` with the row number.
|
||||
Inside each row, you will describe the key. A key is a 4 element list in
|
||||
the format::
|
||||
|
||||
[ <text displayed on the keyboard>, <text to put when the key is pressed>,
|
||||
<text that represents the keycode>, <size of cols> ]
|
||||
|
||||
Here are example keys::
|
||||
|
||||
# f key
|
||||
["f", "f", "f", 1]
|
||||
# capslock
|
||||
["\u21B9", "\t", "tab", 1.5]
|
||||
|
||||
Finally, complete the JSON::
|
||||
|
||||
{
|
||||
...
|
||||
"normal_1": [
|
||||
["`", "`", "`", 1], ["1", "1", "1", 1], ["2", "2", "2", 1],
|
||||
["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1],
|
||||
["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1],
|
||||
["9", "9", "9", 1], ["0", "0", "0", 1], ["+", "+", "+", 1],
|
||||
["=", "=", "=", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
|
||||
"shift_1": [ ... ],
|
||||
"normal_2": [ ... ],
|
||||
"special_2": [ ... ],
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
Request Keyboard
|
||||
----------------
|
||||
|
||||
The instantiation of the virtual keyboard is controlled by the configuration.
|
||||
Check `keyboard_mode` and `keyboard_layout` in the :doc:`api-kivy.config`.
|
||||
|
||||
If you intend to create a widget that requires a keyboard, do not use the
|
||||
virtual keyboard directly, but prefer to use the best method available on
|
||||
the platform. Check the :meth:`~kivy.core.window.WindowBase.request_keyboard`
|
||||
method in the :doc:`api-kivy.core.window`.
|
||||
|
||||
If you want a specific layout when you request the keyboard, you should write
|
||||
something like this (from 1.8.0, numeric.json can be in the same directory as
|
||||
your main.py)::
|
||||
|
||||
keyboard = Window.request_keyboard(
|
||||
self._keyboard_close, self)
|
||||
if keyboard.widget:
|
||||
vkeyboard = self._keyboard.widget
|
||||
vkeyboard.layout = 'numeric.json'
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('VKeyboard', )
|
||||
|
||||
from kivy import kivy_data_dir
|
||||
from kivy.vector import Vector
|
||||
from kivy.config import Config
|
||||
from kivy.uix.scatter import Scatter
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import ObjectProperty, NumericProperty, StringProperty, \
|
||||
BooleanProperty, DictProperty, OptionProperty, ListProperty, ColorProperty
|
||||
from kivy.logger import Logger
|
||||
from kivy.graphics import Color, BorderImage, Canvas
|
||||
from kivy.core.image import Image
|
||||
from kivy.resources import resource_find
|
||||
from kivy.clock import Clock
|
||||
|
||||
from io import open
|
||||
from os.path import join, splitext, basename
|
||||
from os import listdir
|
||||
from json import loads
|
||||
|
||||
|
||||
default_layout_path = join(kivy_data_dir, 'keyboards')
|
||||
|
||||
|
||||
class VKeyboard(Scatter):
|
||||
'''
|
||||
VKeyboard is an onscreen keyboard with multitouch support.
|
||||
Its layout is entirely customizable and you can switch between available
|
||||
layouts using a button in the bottom right of the widget.
|
||||
|
||||
:Events:
|
||||
`on_key_down`: keycode, internal, modifiers
|
||||
Fired when the keyboard received a key down event (key press).
|
||||
`on_key_up`: keycode, internal, modifiers
|
||||
Fired when the keyboard received a key up event (key release).
|
||||
'''
|
||||
|
||||
target = ObjectProperty(None, allownone=True)
|
||||
'''Target widget associated with the VKeyboard. If set, it will be used to
|
||||
send keyboard events. If the VKeyboard mode is "free", it will also be used
|
||||
to set the initial position.
|
||||
|
||||
:attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
callback = ObjectProperty(None, allownone=True)
|
||||
'''Callback can be set to a function that will be called if the
|
||||
VKeyboard is closed by the user.
|
||||
|
||||
:attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
layout = StringProperty(None)
|
||||
'''Layout to use for the VKeyboard. By default, it will be the
|
||||
layout set in the configuration, according to the `keyboard_layout`
|
||||
in `[kivy]` section.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
If layout is a .json filename, it will loaded and added to the
|
||||
available_layouts.
|
||||
|
||||
:attr:`layout` is a :class:`~kivy.properties.StringProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
layout_path = StringProperty(default_layout_path)
|
||||
'''Path from which layouts are read.
|
||||
|
||||
:attr:`layout` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to :file:`<kivy_data_dir>/keyboards/`
|
||||
'''
|
||||
|
||||
available_layouts = DictProperty({})
|
||||
'''Dictionary of all available layouts. Keys are the layout ID, and the
|
||||
value is the JSON (translated into a Python object).
|
||||
|
||||
:attr:`available_layouts` is a :class:`~kivy.properties.DictProperty` and
|
||||
defaults to {}.
|
||||
'''
|
||||
|
||||
docked = BooleanProperty(False)
|
||||
'''Indicate whether the VKeyboard is docked on the screen or not. If you
|
||||
change it, you must manually call :meth:`setup_mode` otherwise it will have
|
||||
no impact. If the VKeyboard is created by the Window, the docked mode will
|
||||
be automatically set by the configuration, using the `keyboard_mode` token
|
||||
in `[kivy]` section.
|
||||
|
||||
:attr:`docked` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
margin_hint = ListProperty([.05, .06, .05, .06])
|
||||
'''Margin hint, used as spacing between keyboard background and keys
|
||||
content. The margin is composed of four values, between 0 and 1::
|
||||
|
||||
margin_hint = [top, right, bottom, left]
|
||||
|
||||
The margin hints will be multiplied by width and height, according to their
|
||||
position.
|
||||
|
||||
:attr:`margin_hint` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to [.05, .06, .05, .06]
|
||||
'''
|
||||
|
||||
key_margin = ListProperty([2, 2, 2, 2])
|
||||
'''Key margin, used to create space between keys. The margin is composed of
|
||||
four values, in pixels::
|
||||
|
||||
key_margin = [top, right, bottom, left]
|
||||
|
||||
:attr:`key_margin` is a :class:`~kivy.properties.ListProperty` and defaults
|
||||
to [2, 2, 2, 2]
|
||||
'''
|
||||
|
||||
font_size = NumericProperty(20.)
|
||||
'''font_size, specifies the size of the text on the virtual keyboard keys.
|
||||
It should be kept within limits to ensure the text does not extend beyond
|
||||
the bounds of the key or become too small to read.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 20.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a). If a background is
|
||||
set, the color will be combined with the background texture.
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background = StringProperty(
|
||||
'atlas://data/images/defaulttheme/vkeyboard_background')
|
||||
'''Filename of the background image.
|
||||
|
||||
:attr:`background` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_background`.
|
||||
'''
|
||||
|
||||
background_disabled = StringProperty(
|
||||
'atlas://data/images/defaulttheme/vkeyboard_disabled_background')
|
||||
'''Filename of the background image when the vkeyboard is disabled.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
:file:`atlas://data/images/defaulttheme/vkeyboard__disabled_background`.
|
||||
|
||||
'''
|
||||
|
||||
key_background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Key background color, in the format (r, g, b, a). If a key background is
|
||||
set, the color will be combined with the key background texture.
|
||||
|
||||
:attr:`key_background_color` is a :class:`~kivy.properties.ColorProperty`
|
||||
and defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
key_background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/vkeyboard_key_normal')
|
||||
'''Filename of the key background image for use when no touches are active
|
||||
on the widget.
|
||||
|
||||
:attr:`key_background_normal` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to
|
||||
:file:`atlas://data/images/defaulttheme/vkeyboard_key_normal`.
|
||||
'''
|
||||
|
||||
key_disabled_background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/vkeyboard_key_normal')
|
||||
'''Filename of the key background image for use when no touches are active
|
||||
on the widget and vkeyboard is disabled.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`key_disabled_background_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
:file:`atlas://data/images/defaulttheme/vkeyboard_disabled_key_normal`.
|
||||
|
||||
'''
|
||||
|
||||
key_background_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/vkeyboard_key_down')
|
||||
'''Filename of the key background image for use when a touch is active
|
||||
on the widget.
|
||||
|
||||
:attr:`key_background_down` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to
|
||||
:file:`atlas://data/images/defaulttheme/vkeyboard_key_down`.
|
||||
'''
|
||||
|
||||
background_border = ListProperty([16, 16, 16, 16])
|
||||
'''Background image border. Used for controlling the
|
||||
:attr:`~kivy.graphics.vertex_instructions.BorderImage.border` property of
|
||||
the background.
|
||||
|
||||
:attr:`background_border` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to [16, 16, 16, 16]
|
||||
'''
|
||||
|
||||
key_border = ListProperty([8, 8, 8, 8])
|
||||
'''Key image border. Used for controlling the
|
||||
:attr:`~kivy.graphics.vertex_instructions.BorderImage.border` property of
|
||||
the key.
|
||||
|
||||
:attr:`key_border` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to [16, 16, 16, 16]
|
||||
'''
|
||||
|
||||
# XXX internal variables
|
||||
layout_mode = OptionProperty('normal',
|
||||
options=('normal', 'shift', 'special'))
|
||||
layout_geometry = DictProperty({})
|
||||
have_capslock = BooleanProperty(False)
|
||||
have_shift = BooleanProperty(False)
|
||||
have_special = BooleanProperty(False)
|
||||
active_keys = DictProperty({})
|
||||
font_name = StringProperty('data/fonts/DejaVuSans.ttf')
|
||||
repeat_touch = ObjectProperty(allownone=True)
|
||||
|
||||
_start_repeat_key_ev = None
|
||||
_repeat_key_ev = None
|
||||
|
||||
__events__ = ('on_key_down', 'on_key_up', 'on_textinput')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# XXX move to style.kv
|
||||
if 'size_hint' not in kwargs:
|
||||
if 'size_hint_x' not in kwargs:
|
||||
self.size_hint_x = None
|
||||
if 'size_hint_y' not in kwargs:
|
||||
self.size_hint_y = None
|
||||
if 'size' not in kwargs:
|
||||
if 'width' not in kwargs:
|
||||
self.width = 700
|
||||
if 'height' not in kwargs:
|
||||
self.height = 200
|
||||
if 'scale_min' not in kwargs:
|
||||
self.scale_min = .4
|
||||
if 'scale_max' not in kwargs:
|
||||
self.scale_max = 1.6
|
||||
if 'docked' not in kwargs:
|
||||
self.docked = False
|
||||
|
||||
layout_mode = self._trigger_update_layout_mode = Clock.create_trigger(
|
||||
self._update_layout_mode)
|
||||
layouts = self._trigger_load_layouts = Clock.create_trigger(
|
||||
self._load_layouts)
|
||||
layout = self._trigger_load_layout = Clock.create_trigger(
|
||||
self._load_layout)
|
||||
fbind = self.fbind
|
||||
|
||||
fbind('docked', self.setup_mode)
|
||||
fbind('have_shift', layout_mode)
|
||||
fbind('have_capslock', layout_mode)
|
||||
fbind('have_special', layout_mode)
|
||||
fbind('layout_path', layouts)
|
||||
fbind('layout', layout)
|
||||
super(VKeyboard, self).__init__(**kwargs)
|
||||
|
||||
# load all the layouts found in the layout_path directory
|
||||
self._load_layouts()
|
||||
|
||||
# ensure we have default layouts
|
||||
available_layouts = self.available_layouts
|
||||
if not available_layouts:
|
||||
Logger.critical('VKeyboard: unable to load default layouts')
|
||||
|
||||
# load the default layout from configuration
|
||||
if self.layout is None:
|
||||
self.layout = Config.get('kivy', 'keyboard_layout')
|
||||
else:
|
||||
# ensure the current layout is found on the available layout
|
||||
self._trigger_load_layout()
|
||||
|
||||
# update layout mode (shift or normal)
|
||||
self._trigger_update_layout_mode()
|
||||
|
||||
# create a top layer to draw active keys on
|
||||
with self.canvas:
|
||||
self.background_key_layer = Canvas()
|
||||
self.active_keys_layer = Canvas()
|
||||
|
||||
def on_disabled(self, instance, value):
|
||||
self.refresh_keys()
|
||||
|
||||
def _update_layout_mode(self, *l):
|
||||
# update mode according to capslock and shift key
|
||||
mode = self.have_capslock != self.have_shift
|
||||
mode = 'shift' if mode else 'normal'
|
||||
if self.have_special:
|
||||
mode = "special"
|
||||
if mode != self.layout_mode:
|
||||
self.layout_mode = mode
|
||||
self.refresh(False)
|
||||
|
||||
def _load_layout(self, *largs):
|
||||
# ensure new layouts are loaded first
|
||||
if self._trigger_load_layouts.is_triggered:
|
||||
self._load_layouts()
|
||||
self._trigger_load_layouts.cancel()
|
||||
|
||||
value = self.layout
|
||||
available_layouts = self.available_layouts
|
||||
|
||||
# it's a filename, try to load it directly
|
||||
if self.layout[-5:] == '.json':
|
||||
if value not in available_layouts:
|
||||
fn = resource_find(self.layout)
|
||||
self._load_layout_fn(fn, self.layout)
|
||||
|
||||
if not available_layouts:
|
||||
return
|
||||
if value not in available_layouts and value != 'qwerty':
|
||||
Logger.error(
|
||||
'Vkeyboard: <%s> keyboard layout mentioned in '
|
||||
'conf file was not found, fallback on qwerty' %
|
||||
value)
|
||||
self.layout = 'qwerty'
|
||||
self.refresh(True)
|
||||
|
||||
def _load_layouts(self, *largs):
|
||||
# first load available layouts from json files
|
||||
# XXX fix to be able to reload layout when path is changing
|
||||
value = self.layout_path
|
||||
for fn in listdir(value):
|
||||
self._load_layout_fn(join(value, fn),
|
||||
basename(splitext(fn)[0]))
|
||||
|
||||
def _load_layout_fn(self, fn, name):
|
||||
available_layouts = self.available_layouts
|
||||
if fn[-5:] != '.json':
|
||||
return
|
||||
with open(fn, 'r', encoding='utf-8') as fd:
|
||||
json_content = fd.read()
|
||||
layout = loads(json_content)
|
||||
available_layouts[name] = layout
|
||||
|
||||
def setup_mode(self, *largs):
|
||||
'''Call this method when you want to readjust the keyboard according to
|
||||
options: :attr:`docked` or not, with attached :attr:`target` or not:
|
||||
|
||||
* If :attr:`docked` is True, it will call :meth:`setup_mode_dock`
|
||||
* If :attr:`docked` is False, it will call :meth:`setup_mode_free`
|
||||
|
||||
Feel free to overload these methods to create new
|
||||
positioning behavior.
|
||||
'''
|
||||
if self.docked:
|
||||
self.setup_mode_dock()
|
||||
else:
|
||||
self.setup_mode_free()
|
||||
|
||||
def setup_mode_dock(self, *largs):
|
||||
'''Setup the keyboard in docked mode.
|
||||
|
||||
Dock mode will reset the rotation, disable translation, rotation and
|
||||
scale. Scale and position will be automatically adjusted to attach the
|
||||
keyboard to the bottom of the screen.
|
||||
|
||||
.. note::
|
||||
Don't call this method directly, use :meth:`setup_mode` instead.
|
||||
'''
|
||||
self.do_translation = False
|
||||
self.do_rotation = False
|
||||
self.do_scale = False
|
||||
self.rotation = 0
|
||||
win = self.get_parent_window()
|
||||
scale = win.width / float(self.width)
|
||||
self.scale = scale
|
||||
self.pos = 0, 0
|
||||
win.bind(on_resize=self._update_dock_mode)
|
||||
|
||||
def _update_dock_mode(self, win, *largs):
|
||||
scale = win.width / float(self.width)
|
||||
self.scale = scale
|
||||
self.pos = 0, 0
|
||||
|
||||
def setup_mode_free(self):
|
||||
'''Setup the keyboard in free mode.
|
||||
|
||||
Free mode is designed to let the user control the position and
|
||||
orientation of the keyboard. The only real usage is for a multiuser
|
||||
environment, but you might found other ways to use it.
|
||||
If a :attr:`target` is set, it will place the vkeyboard under the
|
||||
target.
|
||||
|
||||
.. note::
|
||||
Don't call this method directly, use :meth:`setup_mode` instead.
|
||||
'''
|
||||
self.do_translation = True
|
||||
self.do_rotation = True
|
||||
self.do_scale = True
|
||||
target = self.target
|
||||
if not target:
|
||||
return
|
||||
|
||||
# NOTE all math will be done in window point of view
|
||||
# determine rotation of the target
|
||||
a = Vector(1, 0)
|
||||
b = Vector(target.to_window(0, 0))
|
||||
c = Vector(target.to_window(1, 0)) - b
|
||||
self.rotation = -a.angle(c)
|
||||
|
||||
# determine the position of center/top of the keyboard
|
||||
dpos = Vector(self.to_window(self.width / 2., self.height))
|
||||
|
||||
# determine the position of center/bottom of the target
|
||||
cpos = Vector(target.to_window(target.center_x, target.y))
|
||||
|
||||
# the goal now is to map both point, calculate the diff between them
|
||||
diff = dpos - cpos
|
||||
|
||||
# we still have an issue, self.pos represent the bounding box,
|
||||
# not the 0,0 coordinate of the scatter. we need to apply also
|
||||
# the diff between them (inside and outside coordinate matrix).
|
||||
# It's hard to explain, but do a scheme on a paper, write all
|
||||
# the vector i'm calculating, and you'll understand. :)
|
||||
diff2 = Vector(self.x + self.width / 2., self.y + self.height) - \
|
||||
Vector(self.to_parent(self.width / 2., self.height))
|
||||
diff -= diff2
|
||||
|
||||
# now we have a good "diff", set it as a pos.
|
||||
self.pos = -diff
|
||||
|
||||
def change_layout(self):
|
||||
# XXX implement popup with all available layouts
|
||||
pass
|
||||
|
||||
def refresh(self, force=False):
|
||||
'''(internal) Recreate the entire widget and graphics according to the
|
||||
selected layout.
|
||||
'''
|
||||
self.clear_widgets()
|
||||
if force:
|
||||
self.refresh_keys_hint()
|
||||
self.refresh_keys()
|
||||
self.refresh_active_keys_layer()
|
||||
|
||||
def refresh_active_keys_layer(self):
|
||||
self.active_keys_layer.clear()
|
||||
|
||||
active_keys = self.active_keys
|
||||
layout_geometry = self.layout_geometry
|
||||
background = resource_find(self.key_background_down)
|
||||
texture = Image(background, mipmap=True).texture
|
||||
|
||||
with self.active_keys_layer:
|
||||
Color(*self.key_background_color)
|
||||
for line_nb, index in active_keys.values():
|
||||
pos, size = layout_geometry['LINE_%d' % line_nb][index]
|
||||
BorderImage(texture=texture, pos=pos, size=size,
|
||||
border=self.key_border)
|
||||
|
||||
def refresh_keys_hint(self):
|
||||
layout = self.available_layouts[self.layout]
|
||||
layout_cols = layout['cols']
|
||||
layout_rows = layout['rows']
|
||||
layout_geometry = self.layout_geometry
|
||||
mtop, mright, mbottom, mleft = self.margin_hint
|
||||
|
||||
# get relative EFFICIENT surface of the layout without external margins
|
||||
el_hint = 1. - mleft - mright
|
||||
eh_hint = 1. - mtop - mbottom
|
||||
ex_hint = 0 + mleft
|
||||
ey_hint = 0 + mbottom
|
||||
|
||||
# get relative unit surface
|
||||
uw_hint = (1. / layout_cols) * el_hint
|
||||
uh_hint = (1. / layout_rows) * eh_hint
|
||||
layout_geometry['U_HINT'] = (uw_hint, uh_hint)
|
||||
|
||||
# calculate individual key RELATIVE surface and pos (without key
|
||||
# margin)
|
||||
current_y_hint = ey_hint + eh_hint
|
||||
for line_nb in range(1, layout_rows + 1):
|
||||
current_y_hint -= uh_hint
|
||||
# get line_name
|
||||
line_name = '%s_%d' % (self.layout_mode, line_nb)
|
||||
line_hint = 'LINE_HINT_%d' % line_nb
|
||||
layout_geometry[line_hint] = []
|
||||
current_x_hint = ex_hint
|
||||
# go through the list of keys (tuples of 4)
|
||||
for key in layout[line_name]:
|
||||
# calculate relative pos, size
|
||||
layout_geometry[line_hint].append([
|
||||
(current_x_hint, current_y_hint),
|
||||
(key[3] * uw_hint, uh_hint)])
|
||||
current_x_hint += key[3] * uw_hint
|
||||
|
||||
self.layout_geometry = layout_geometry
|
||||
|
||||
def refresh_keys(self):
|
||||
layout = self.available_layouts[self.layout]
|
||||
layout_rows = layout['rows']
|
||||
layout_geometry = self.layout_geometry
|
||||
w, h = self.size
|
||||
kmtop, kmright, kmbottom, kmleft = self.key_margin
|
||||
uw_hint, uh_hint = layout_geometry['U_HINT']
|
||||
|
||||
for line_nb in range(1, layout_rows + 1):
|
||||
llg = layout_geometry['LINE_%d' % line_nb] = []
|
||||
llg_append = llg.append
|
||||
for key in layout_geometry['LINE_HINT_%d' % line_nb]:
|
||||
x_hint, y_hint = key[0]
|
||||
w_hint, h_hint = key[1]
|
||||
kx = x_hint * w
|
||||
ky = y_hint * h
|
||||
kw = w_hint * w
|
||||
kh = h_hint * h
|
||||
|
||||
# now adjust, considering the key margin
|
||||
kx = int(kx + kmleft)
|
||||
ky = int(ky + kmbottom)
|
||||
kw = int(kw - kmleft - kmright)
|
||||
kh = int(kh - kmbottom - kmtop)
|
||||
|
||||
pos = (kx, ky)
|
||||
size = (kw, kh)
|
||||
llg_append((pos, size))
|
||||
|
||||
self.layout_geometry = layout_geometry
|
||||
self.draw_keys()
|
||||
|
||||
def draw_keys(self):
|
||||
layout = self.available_layouts[self.layout]
|
||||
layout_rows = layout['rows']
|
||||
layout_geometry = self.layout_geometry
|
||||
layout_mode = self.layout_mode
|
||||
|
||||
# draw background
|
||||
background = resource_find(self.background_disabled
|
||||
if self.disabled else
|
||||
self.background)
|
||||
texture = Image(background, mipmap=True).texture
|
||||
self.background_key_layer.clear()
|
||||
with self.background_key_layer:
|
||||
Color(*self.background_color)
|
||||
BorderImage(texture=texture, size=self.size,
|
||||
border=self.background_border)
|
||||
|
||||
# XXX separate drawing the keys and the fonts to avoid
|
||||
# XXX reloading the texture each time
|
||||
|
||||
# first draw keys without the font
|
||||
key_normal = resource_find(self.key_background_disabled_normal
|
||||
if self.disabled else
|
||||
self.key_background_normal)
|
||||
texture = Image(key_normal, mipmap=True).texture
|
||||
with self.background_key_layer:
|
||||
Color(*self.key_background_color)
|
||||
for line_nb in range(1, layout_rows + 1):
|
||||
for pos, size in layout_geometry['LINE_%d' % line_nb]:
|
||||
BorderImage(texture=texture, pos=pos, size=size,
|
||||
border=self.key_border)
|
||||
|
||||
# then draw the text
|
||||
for line_nb in range(1, layout_rows + 1):
|
||||
key_nb = 0
|
||||
for pos, size in layout_geometry['LINE_%d' % line_nb]:
|
||||
# retrieve the relative text
|
||||
text = layout[layout_mode + '_' + str(line_nb)][key_nb][0]
|
||||
z = Label(text=text, font_size=self.font_size, pos=pos,
|
||||
size=size, font_name=self.font_name)
|
||||
self.add_widget(z)
|
||||
key_nb += 1
|
||||
|
||||
def on_key_down(self, *largs):
|
||||
pass
|
||||
|
||||
def on_key_up(self, *largs):
|
||||
pass
|
||||
|
||||
def on_textinput(self, *largs):
|
||||
pass
|
||||
|
||||
def get_key_at_pos(self, x, y):
|
||||
w, h = self.size
|
||||
x_hint = x / w
|
||||
# focus on the surface without margins
|
||||
layout_geometry = self.layout_geometry
|
||||
layout = self.available_layouts[self.layout]
|
||||
layout_rows = layout['rows']
|
||||
mtop, mright, mbottom, mleft = self.margin_hint
|
||||
|
||||
# get the line of the layout
|
||||
e_height = h - (mbottom + mtop) * h # efficient height in pixels
|
||||
line_height = e_height / layout_rows # line height in px
|
||||
y = y - mbottom * h
|
||||
line_nb = layout_rows - int(y / line_height)
|
||||
|
||||
if line_nb > layout_rows:
|
||||
line_nb = layout_rows
|
||||
if line_nb < 1:
|
||||
line_nb = 1
|
||||
|
||||
# get the key within the line
|
||||
key_index = ''
|
||||
current_key_index = 0
|
||||
for key in layout_geometry['LINE_HINT_%d' % line_nb]:
|
||||
if x_hint >= key[0][0] and x_hint < key[0][0] + key[1][0]:
|
||||
key_index = current_key_index
|
||||
break
|
||||
else:
|
||||
current_key_index += 1
|
||||
if key_index == '':
|
||||
return None
|
||||
|
||||
# get the full character
|
||||
key = layout['%s_%d' % (self.layout_mode, line_nb)][key_index]
|
||||
|
||||
return [key, (line_nb, key_index)]
|
||||
|
||||
def collide_margin(self, x, y):
|
||||
'''Do a collision test, and return True if the (x, y) is inside the
|
||||
vkeyboard margin.
|
||||
'''
|
||||
mtop, mright, mbottom, mleft = self.margin_hint
|
||||
x_hint = x / self.width
|
||||
y_hint = y / self.height
|
||||
if x_hint > mleft and x_hint < 1. - mright \
|
||||
and y_hint > mbottom and y_hint < 1. - mtop:
|
||||
return False
|
||||
return True
|
||||
|
||||
def process_key_on(self, touch):
|
||||
if not touch:
|
||||
return
|
||||
x, y = self.to_local(*touch.pos)
|
||||
key = self.get_key_at_pos(x, y)
|
||||
if not key:
|
||||
return
|
||||
|
||||
key_data = key[0]
|
||||
displayed_char, internal, special_char, size = key_data
|
||||
line_nb, key_index = key[1]
|
||||
|
||||
# save pressed key on the touch
|
||||
ud = touch.ud[self.uid] = {}
|
||||
ud['key'] = key
|
||||
|
||||
# for caps lock or shift only:
|
||||
uid = touch.uid
|
||||
if special_char is not None:
|
||||
# Do not repeat special keys
|
||||
if special_char in ('capslock', 'shift', 'layout', 'special'):
|
||||
if self._start_repeat_key_ev is not None:
|
||||
self._start_repeat_key_ev.cancel()
|
||||
self._start_repeat_key_ev = None
|
||||
self.repeat_touch = None
|
||||
if special_char == 'capslock':
|
||||
self.have_capslock = not self.have_capslock
|
||||
uid = -1
|
||||
elif special_char == 'shift':
|
||||
self.have_shift = True
|
||||
elif special_char == 'special':
|
||||
self.have_special = True
|
||||
elif special_char == 'layout':
|
||||
self.change_layout()
|
||||
|
||||
# send info to the bus
|
||||
b_keycode = special_char
|
||||
b_modifiers = self._get_modifiers()
|
||||
if self.get_parent_window().__class__.__module__ == \
|
||||
'kivy.core.window.window_sdl2' and internal:
|
||||
self.dispatch('on_textinput', internal)
|
||||
else:
|
||||
self.dispatch('on_key_down', b_keycode, internal, b_modifiers)
|
||||
|
||||
# save key as an active key for drawing
|
||||
self.active_keys[uid] = key[1]
|
||||
self.refresh_active_keys_layer()
|
||||
|
||||
def process_key_up(self, touch):
|
||||
uid = touch.uid
|
||||
if self.uid not in touch.ud:
|
||||
return
|
||||
|
||||
# save pressed key on the touch
|
||||
key_data, key = touch.ud[self.uid]['key']
|
||||
displayed_char, internal, special_char, size = key_data
|
||||
|
||||
# send info to the bus
|
||||
b_keycode = special_char
|
||||
b_modifiers = self._get_modifiers()
|
||||
self.dispatch('on_key_up', b_keycode, internal, b_modifiers)
|
||||
|
||||
if special_char == 'capslock':
|
||||
uid = -1
|
||||
|
||||
if uid in self.active_keys:
|
||||
self.active_keys.pop(uid, None)
|
||||
if special_char == 'shift':
|
||||
self.have_shift = False
|
||||
elif special_char == 'special':
|
||||
self.have_special = False
|
||||
if special_char == 'capslock' and self.have_capslock:
|
||||
self.active_keys[-1] = key
|
||||
self.refresh_active_keys_layer()
|
||||
|
||||
def _get_modifiers(self):
|
||||
ret = []
|
||||
if self.have_shift:
|
||||
ret.append('shift')
|
||||
if self.have_capslock:
|
||||
ret.append('capslock')
|
||||
return ret
|
||||
|
||||
def _start_repeat_key(self, *kwargs):
|
||||
self._repeat_key_ev = Clock.schedule_interval(self._repeat_key, 0.05)
|
||||
|
||||
def _repeat_key(self, *kwargs):
|
||||
self.process_key_on(self.repeat_touch)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
x, y = touch.pos
|
||||
if not self.collide_point(x, y):
|
||||
return
|
||||
if self.disabled:
|
||||
return True
|
||||
|
||||
x, y = self.to_local(x, y)
|
||||
if not self.collide_margin(x, y):
|
||||
if self.repeat_touch is None:
|
||||
self._start_repeat_key_ev = Clock.schedule_once(
|
||||
self._start_repeat_key, 0.5)
|
||||
self.repeat_touch = touch
|
||||
|
||||
self.process_key_on(touch)
|
||||
touch.grab(self, exclusive=True)
|
||||
|
||||
else:
|
||||
super(VKeyboard, self).on_touch_down(touch)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is self:
|
||||
self.process_key_up(touch)
|
||||
|
||||
if self._start_repeat_key_ev is not None:
|
||||
self._start_repeat_key_ev.cancel()
|
||||
self._start_repeat_key_ev = None
|
||||
if touch == self.repeat_touch:
|
||||
if self._repeat_key_ev is not None:
|
||||
self._repeat_key_ev.cancel()
|
||||
self._repeat_key_ev = None
|
||||
self.repeat_touch = None
|
||||
|
||||
return super(VKeyboard, self).on_touch_up(touch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
vk = VKeyboard(layout='azerty')
|
||||
runTouchApp(vk)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user