first commit

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

View file

@ -0,0 +1,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`
----
'''

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,595 @@
'''
Focus Behavior
==============
The :class:`~kivy.uix.behaviors.FocusBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
keyboard focus behavior. When combined with other
FocusBehavior widgets it allows one to cycle focus among them by pressing
tab. In addition, upon gaining focus, the instance will automatically
receive keyboard input.
Focus, very different from selection, is intimately tied with the keyboard;
each keyboard can focus on zero or one widgets, and each widget can only
have the focus of one keyboard. However, multiple keyboards can focus
simultaneously on different widgets. When escape is hit, the widget having
the focus of that keyboard will de-focus.
Managing focus
--------------
In essence, focus is implemented as a doubly linked list, where each
node holds a (weak) reference to the instance before it and after it,
as visualized when cycling through the nodes using tab (forward) or
shift+tab (backward). If a previous or next widget is not specified,
:attr:`focus_next` and :attr:`focus_previous` defaults to `None`. This
means that the :attr:`~kivy.uix.widget.Widget.children` list and
:attr:`parents <kivy.uix.widget.Widget.parent>` are
walked to find the next focusable widget, unless :attr:`focus_next` or
:attr:`focus_previous` is set to the `StopIteration` class, in which case
focus stops there.
For example, to cycle focus between :class:`~kivy.uix.button.Button`
elements of a :class:`~kivy.uix.gridlayout.GridLayout`::
class FocusButton(FocusBehavior, Button):
pass
grid = GridLayout(cols=4)
for i in range(40):
grid.add_widget(FocusButton(text=str(i)))
# clicking on a widget will activate focus, and tab can now be used
# to cycle through
When using a software keyboard, typical on mobile and touch devices, the
keyboard display behavior is determined by the
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
this property to ensure the focused widget is not covered or obscured by the
keyboard.
Initializing focus
------------------
Widgets needs to be visible before they can receive the focus. This means that
setting their *focus* property to True before they are visible will have no
effect. To initialize focus, you can use the 'on_parent' event::
from kivy.app import App
from kivy.uix.textinput import TextInput
class MyTextInput(TextInput):
def on_parent(self, widget, parent):
self.focus = True
class SampleApp(App):
def build(self):
return MyTextInput()
SampleApp().run()
If you are using a :class:`~kivy.uix.popup`, you can use the 'on_open' event.
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.
.. warning::
This code is still experimental, and its API is subject to change in a
future version.
'''
__all__ = ('FocusBehavior', )
from kivy.properties import OptionProperty, ObjectProperty, BooleanProperty, \
AliasProperty
from kivy.config import Config
from kivy.base import EventLoop
# When we are generating documentation, Config doesn't exist
_is_desktop = False
_keyboard_mode = 'system'
if Config:
_is_desktop = Config.getboolean('kivy', 'desktop')
_keyboard_mode = Config.get('kivy', 'keyboard_mode')
class FocusBehavior(object):
'''Provides keyboard focus behavior. When combined with other
FocusBehavior widgets it allows one to cycle focus among them by pressing
tab. Please see the
:mod:`focus behavior module documentation <kivy.uix.behaviors.focus>`
for more information.
.. versionadded:: 1.9.0
'''
_requested_keyboard = False
_keyboard = ObjectProperty(None, allownone=True)
_keyboards = {}
ignored_touch = []
'''A list of touches that should not be used to defocus. After on_touch_up,
every touch that is not in :attr:`ignored_touch` will defocus all the
focused widgets if the config keyboard mode is not multi. Touches on
focusable widgets that were used to focus are automatically added here.
Example usage::
class Unfocusable(Widget):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
FocusBehavior.ignored_touch.append(touch)
Notice that you need to access this as a class, not an instance variable.
'''
def _set_keyboard(self, value):
focus = self.focus
keyboard = self._keyboard
keyboards = FocusBehavior._keyboards
if keyboard:
self.focus = False # this'll unbind
if self._keyboard: # remove assigned keyboard from dict
del keyboards[keyboard]
if value and value not in keyboards:
keyboards[value] = None
self._keyboard = value
self.focus = focus
def _get_keyboard(self):
return self._keyboard
keyboard = AliasProperty(_get_keyboard, _set_keyboard,
bind=('_keyboard', ))
'''The keyboard to bind to (or bound to the widget) when focused.
When None, a keyboard is requested and released whenever the widget comes
into and out of focus. If not None, it must be a keyboard, which gets
bound and unbound from the widget whenever it's in or out of focus. It is
useful only when more than one keyboard is available, so it is recommended
to be set to None when only one keyboard is available.
If more than one keyboard is available, whenever an instance gets focused
a new keyboard will be requested if None. Unless the other instances lose
focus (e.g. if tab was used), a new keyboard will appear. When this is
undesired, the keyboard property can be used. For example, if there are
two users with two keyboards, then each keyboard can be assigned to
different groups of instances of FocusBehavior, ensuring that within
each group, only one FocusBehavior will have focus, and will receive input
from the correct keyboard. See `keyboard_mode` in :mod:`~kivy.config` for
more information on the keyboard modes.
**Keyboard and focus behavior**
When using the keyboard, there are some important default behaviors you
should keep in mind.
* When Config's `keyboard_mode` is multi, each new touch is considered
a touch by a different user and will set the focus (if clicked on a
focusable) with a new keyboard. Already focused elements will not lose
their focus (even if an unfocusable widget is touched).
* If the keyboard property is set, that keyboard will be used when the
instance gets focused. If widgets with different keyboards are linked
through :attr:`focus_next` and :attr:`focus_previous`, then as they are
tabbed through, different keyboards will become active. Therefore,
typically it's undesirable to link instances which are assigned
different keyboards.
* When a widget has focus, setting its keyboard to None will remove its
keyboard, but the widget will then immediately try to get
another keyboard. In order to remove its keyboard, rather set its
:attr:`focus` to False.
* When using a software keyboard, typical on mobile and touch devices, the
keyboard display behavior is determined by the
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
this property to ensure the focused widget is not covered or obscured.
:attr:`keyboard` is an :class:`~kivy.properties.AliasProperty` and defaults
to None.
.. warning:
When assigning a keyboard, the keyboard must not be released while
it is still assigned to an instance. Similarly, the keyboard created
by the instance on focus and assigned to :attr:`keyboard` if None,
will be released by the instance when the instance loses focus.
Therefore, it is not safe to assign this keyboard to another instance's
:attr:`keyboard`.
'''
is_focusable = BooleanProperty(_is_desktop)
'''Whether the instance can become focused. If focused, it'll lose focus
when set to False.
:attr:`is_focusable` is a :class:`~kivy.properties.BooleanProperty` and
defaults to True on a desktop (i.e. `desktop` is True in
:mod:`~kivy.config`), False otherwise.
'''
focus = BooleanProperty(False)
'''Whether the instance currently has focus.
Setting it to True will bind to and/or request the keyboard, and input
will be forwarded to the instance. Setting it to False will unbind
and/or release the keyboard. For a given keyboard, only one widget can
have its focus, so focusing one will automatically unfocus the other
instance holding its focus.
When using a software keyboard, please refer to the
:attr:`~kivy.core.window.WindowBase.softinput_mode` property to determine
how the keyboard display is handled.
:attr:`focus` is a :class:`~kivy.properties.BooleanProperty` and defaults
to False.
'''
focused = focus
'''An alias of :attr:`focus`.
:attr:`focused` is a :class:`~kivy.properties.BooleanProperty` and defaults
to False.
.. warning::
:attr:`focused` is an alias of :attr:`focus` and will be removed in
2.0.0.
'''
keyboard_suggestions = BooleanProperty(True)
'''If True provides auto suggestions on top of keyboard.
This will only work if :attr:`input_type` is set to `text`, `url`, `mail` or
`address`.
.. warning::
On Android, `keyboard_suggestions` relies on
`InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS` to work, but some keyboards
just ignore this flag. If you want to disable suggestions at all on
Android, you can set `input_type` to `null`, which will request the
input method to run in a limited "generate key events" mode.
.. versionadded:: 2.1.0
:attr:`keyboard_suggestions` is a :class:`~kivy.properties.BooleanProperty`
and defaults to True
'''
def _set_on_focus_next(self, instance, value):
'''If changing focus, ensure your code does not create an infinite loop.
eg:
```python
widget.focus_next = widget
widget.focus_previous = widget
```
'''
next_ = self._old_focus_next
if next_ is value: # prevent infinite loop
return
if isinstance(next_, FocusBehavior):
next_.focus_previous = None
self._old_focus_next = value
if value is None or value is StopIteration:
return
if not isinstance(value, FocusBehavior):
raise ValueError('focus_next accepts only objects based on'
' FocusBehavior, or the `StopIteration` class.')
value.focus_previous = self
focus_next = ObjectProperty(None, allownone=True)
'''The :class:`FocusBehavior` instance to acquire focus when
tab is pressed and this instance has focus, if not `None` or
`StopIteration`.
When tab is pressed, focus cycles through all the :class:`FocusBehavior`
widgets that are linked through :attr:`focus_next` and are focusable. If
:attr:`focus_next` is `None`, it instead walks the children lists to find
the next focusable widget. Finally, if :attr:`focus_next` is
the `StopIteration` class, focus won't move forward, but end here.
.. note:
Setting :attr:`focus_next` automatically sets :attr:`focus_previous`
of the other instance to point to this instance, if not None or
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
also sets the :attr:`focus_previous` property of the instance
previously in :attr:`focus_next` to `None`. Therefore, it is only
required to set one of the :attr:`focus_previous` or
:attr:`focus_next` links since the other side will be set
automatically.
:attr:`focus_next` is an :class:`~kivy.properties.ObjectProperty` and
defaults to `None`.
'''
def _set_on_focus_previous(self, instance, value):
prev = self._old_focus_previous
if prev is value:
return
if isinstance(prev, FocusBehavior):
prev.focus_next = None
self._old_focus_previous = value
if value is None or value is StopIteration:
return
if not isinstance(value, FocusBehavior):
raise ValueError('focus_previous accepts only objects based'
'on FocusBehavior, or the `StopIteration` class.')
value.focus_next = self
focus_previous = ObjectProperty(None, allownone=True)
'''The :class:`FocusBehavior` instance to acquire focus when
shift+tab is pressed on this instance, if not None or `StopIteration`.
When shift+tab is pressed, focus cycles through all the
:class:`FocusBehavior` widgets that are linked through
:attr:`focus_previous` and are focusable. If :attr:`focus_previous` is
`None`, it instead walks the children tree to find the
previous focusable widget. Finally, if :attr:`focus_previous` is the
`StopIteration` class, focus won't move backward, but end here.
.. note:
Setting :attr:`focus_previous` automatically sets :attr:`focus_next`
of the other instance to point to this instance, if not None or
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
also sets the :attr:`focus_next` property of the instance previously in
:attr:`focus_previous` to `None`. Therefore, it is only required
to set one of the :attr:`focus_previous` or :attr:`focus_next`
links since the other side will be set automatically.
:attr:`focus_previous` is an :class:`~kivy.properties.ObjectProperty` and
defaults to `None`.
'''
keyboard_mode = OptionProperty('auto', options=('auto', 'managed'))
'''Determines how the keyboard visibility should be managed. 'auto' will
result in the standard behavior of showing/hiding on focus. 'managed'
requires setting the keyboard visibility manually, or calling the helper
functions :meth:`show_keyboard` and :meth:`hide_keyboard`.
:attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and
defaults to 'auto'. Can be one of 'auto' or 'managed'.
'''
input_type = OptionProperty('null', options=('null', 'text', 'number',
'url', 'mail', 'datetime',
'tel', 'address'))
'''The kind of input keyboard to request.
.. versionadded:: 1.8.0
.. versionchanged:: 2.1.0
Changed default value from `text` to `null`. Added `null` to options.
.. warning::
As the default value has been changed, you may need to adjust
`input_type` in your code.
:attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and
defaults to 'null'. Can be one of 'null', 'text', 'number', 'url', 'mail',
'datetime', 'tel' or 'address'.
'''
unfocus_on_touch = BooleanProperty(_keyboard_mode not in
('multi', 'systemandmulti'))
'''Whether a instance should lose focus when clicked outside the instance.
When a user clicks on a widget that is focus aware and shares the same
keyboard as this widget (which in the case with only one keyboard),
then as the other widgets gain focus, this widget loses focus. In addition
to that, if this property is `True`, clicking on any widget other than this
widget, will remove focus from this widget.
:attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` and
defaults to `False` if the `keyboard_mode` in :attr:`~kivy.config.Config`
is `'multi'` or `'systemandmulti'`, otherwise it defaults to `True`.
'''
def __init__(self, **kwargs):
self._old_focus_next = None
self._old_focus_previous = None
super(FocusBehavior, self).__init__(**kwargs)
self._keyboard_mode = _keyboard_mode
fbind = self.fbind
fbind('focus', self._on_focus)
fbind('disabled', self._on_focusable)
fbind('is_focusable', self._on_focusable)
fbind('focus_next', self._set_on_focus_next)
fbind('focus_previous', self._set_on_focus_previous)
def _on_focusable(self, instance, value):
if self.disabled or not self.is_focusable:
self.focus = False
def _on_focus(self, instance, value, *largs):
if self.keyboard_mode == 'auto':
if value:
self._bind_keyboard()
else:
self._unbind_keyboard()
def _ensure_keyboard(self):
if self._keyboard is None:
self._requested_keyboard = True
keyboard = self._keyboard = EventLoop.window.request_keyboard(
self._keyboard_released,
self,
input_type=self.input_type,
keyboard_suggestions=self.keyboard_suggestions,
)
keyboards = FocusBehavior._keyboards
if keyboard not in keyboards:
keyboards[keyboard] = None
def _bind_keyboard(self):
self._ensure_keyboard()
keyboard = self._keyboard
if not keyboard or self.disabled or not self.is_focusable:
self.focus = False
return
keyboards = FocusBehavior._keyboards
old_focus = keyboards[keyboard] # keyboard should be in dict
if old_focus:
old_focus.focus = False
# keyboard shouldn't have been released here, see keyboard warning
keyboards[keyboard] = self
keyboard.bind(on_key_down=self.keyboard_on_key_down,
on_key_up=self.keyboard_on_key_up,
on_textinput=self.keyboard_on_textinput)
def _unbind_keyboard(self):
keyboard = self._keyboard
if keyboard:
keyboard.unbind(on_key_down=self.keyboard_on_key_down,
on_key_up=self.keyboard_on_key_up,
on_textinput=self.keyboard_on_textinput)
if self._requested_keyboard:
keyboard.release()
self._keyboard = None
self._requested_keyboard = False
del FocusBehavior._keyboards[keyboard]
else:
FocusBehavior._keyboards[keyboard] = None
def keyboard_on_textinput(self, window, text):
pass
def _keyboard_released(self):
self.focus = False
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
return
if (not self.disabled and self.is_focusable and
('button' not in touch.profile or
not touch.button.startswith('scroll'))):
self.focus = True
FocusBehavior.ignored_touch.append(touch)
return super(FocusBehavior, self).on_touch_down(touch)
@staticmethod
def _handle_post_on_touch_up(touch):
''' Called by window after each touch has finished.
'''
touches = FocusBehavior.ignored_touch
if touch in touches:
touches.remove(touch)
return
if 'button' in touch.profile and touch.button in\
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
return
for focusable in list(FocusBehavior._keyboards.values()):
if focusable is None or not focusable.unfocus_on_touch:
continue
focusable.focus = False
def _get_focus_next(self, focus_dir):
current = self
walk_tree = 'walk' if focus_dir == 'focus_next' else 'walk_reverse'
while 1:
# if we hit a focusable, walk through focus_xxx
while getattr(current, focus_dir) is not None:
current = getattr(current, focus_dir)
if current is self or current is StopIteration:
return None # make sure we don't loop forever
if current.is_focusable and not current.disabled:
return current
# hit unfocusable, walk widget tree
itr = getattr(current, walk_tree)(loopback=True)
if focus_dir == 'focus_next':
next(itr) # current is returned first when walking forward
for current in itr:
if isinstance(current, FocusBehavior):
break
# why did we stop
if isinstance(current, FocusBehavior):
if current is self:
return None
if current.is_focusable and not current.disabled:
return current
else:
return None
def get_focus_next(self):
'''Returns the next focusable widget using either :attr:`focus_next`
or the :attr:`children` similar to the order when tabbing forwards
with the ``tab`` key.
'''
return self._get_focus_next('focus_next')
def get_focus_previous(self):
'''Returns the previous focusable widget using either
:attr:`focus_previous` or the :attr:`children` similar to the
order when the ``tab`` + ``shift`` keys are triggered together.
'''
return self._get_focus_next('focus_previous')
def keyboard_on_key_down(self, window, keycode, text, modifiers):
'''The method bound to the keyboard when the instance has focus.
When the instance becomes focused, this method is bound to the
keyboard and will be called for every input press. The parameters are
the same as :meth:`kivy.core.window.WindowBase.on_key_down`.
When overwriting the method in the derived widget, super should be
called to enable tab cycling. If the derived widget wishes to use tab
for its own purposes, it can call super after it has processed the
character (if it does not wish to consume the tab).
Similar to other keyboard functions, it should return True if the
key was consumed.
'''
if keycode[1] == 'tab': # deal with cycle
modifiers = set(modifiers)
if {'ctrl', 'alt', 'meta', 'super', 'compose'} & modifiers:
return False
if 'shift' in modifiers:
next = self.get_focus_previous()
else:
next = self.get_focus_next()
if next:
self.focus = False
next.focus = True
return True
return False
def keyboard_on_key_up(self, window, keycode):
'''The method bound to the keyboard when the instance has focus.
When the instance becomes focused, this method is bound to the
keyboard and will be called for every input release. The parameters are
the same as :meth:`kivy.core.window.WindowBase.on_key_up`.
When overwriting the method in the derived widget, super should be
called to enable de-focusing on escape. If the derived widget wishes
to use escape for its own purposes, it can call super after it has
processed the character (if it does not wish to consume the escape).
See :meth:`keyboard_on_key_down`
'''
if keycode[1] == 'escape':
self.focus = False
return True
return False
def show_keyboard(self):
'''
Convenience function to show the keyboard in managed mode.
'''
if self.keyboard_mode == 'managed':
self._bind_keyboard()
def hide_keyboard(self):
'''
Convenience function to hide the keyboard in managed mode.
'''
if self.keyboard_mode == 'managed':
self._unbind_keyboard()

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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)
'''

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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])

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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))

View file

@ -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))

View file

@ -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, )

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more