Ajout du GUI
This commit is contained in:
56
kivy/uix/__init__.py
Normal file
56
kivy/uix/__init__.py
Normal 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`
|
||||
|
||||
----
|
||||
'''
|
||||
BIN
kivy/uix/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/accordion.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/accordion.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/actionbar.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/actionbar.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/anchorlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/anchorlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/boxlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/boxlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/bubble.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/bubble.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/button.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/button.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/camera.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/camera.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/carousel.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/carousel.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/checkbox.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/checkbox.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/codeinput.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/codeinput.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/colorpicker.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/colorpicker.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/dropdown.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/dropdown.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/effectwidget.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/effectwidget.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/filechooser.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/filechooser.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/floatlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/floatlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/gesturesurface.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/gesturesurface.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/gridlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/gridlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/image.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/image.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/label.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/label.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/layout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/layout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/modalview.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/modalview.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/pagelayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/pagelayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/popup.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/popup.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/progressbar.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/progressbar.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/recycleboxlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/recycleboxlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/recyclegridlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/recyclegridlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/recyclelayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/recyclelayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/relativelayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/relativelayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/rst.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/rst.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/sandbox.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/sandbox.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/scatter.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/scatter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/scatterlayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/scatterlayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/screenmanager.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/screenmanager.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/scrollview.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/scrollview.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/settings.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/slider.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/slider.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/spinner.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/spinner.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/splitter.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/splitter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/stacklayout.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/stacklayout.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/stencilview.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/stencilview.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/switch.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/switch.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/tabbedpanel.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/tabbedpanel.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/textinput.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/textinput.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/togglebutton.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/togglebutton.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/treeview.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/treeview.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/video.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/video.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/videoplayer.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/videoplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/vkeyboard.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/vkeyboard.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/__pycache__/widget.cpython-310.pyc
Normal file
BIN
kivy/uix/__pycache__/widget.cpython-310.pyc
Normal file
Binary file not shown.
484
kivy/uix/accordion.py
Normal file
484
kivy/uix/accordion.py
Normal 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)
|
||||
933
kivy/uix/actionbar.py
Normal file
933
kivy/uix/actionbar.py
Normal file
@@ -0,0 +1,933 @@
|
||||
'''
|
||||
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` contains an :class:`ActionView` with various
|
||||
:class:`ContextualActionViews <kivy.uix.actionbar.ContextualActionView>`.
|
||||
An :class:`ActionView` will contain an :class:`ActionPrevious` having title,
|
||||
app_icon and previous_icon properties. An :class:`ActionView` will contain
|
||||
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)
|
||||
122
kivy/uix/anchorlayout.py
Normal file
122
kivy/uix/anchorlayout.py
Normal 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
|
||||
95
kivy/uix/behaviors/__init__.py
Normal file
95
kivy/uix/behaviors/__init__.py
Normal file
@@ -0,0 +1,95 @@
|
||||
'''
|
||||
Behaviors
|
||||
=========
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Behavior mixin classes
|
||||
----------------------
|
||||
|
||||
This module implements behaviors that can be
|
||||
`mixed in <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
with existing base widgets. The idea behind these classes is to encapsulate
|
||||
properties and events associated with certain types of widgets.
|
||||
|
||||
Isolating these properties and events in a mixin class allows you to define
|
||||
your own implementation for standard kivy widgets that can act as drop-in
|
||||
replacements. This means you can re-style and re-define widgets as desired
|
||||
without breaking compatibility: as long as they implement the behaviors
|
||||
correctly, they can simply replace the standard widgets.
|
||||
|
||||
Adding behaviors
|
||||
----------------
|
||||
|
||||
Say you want to add :class:`~kivy.uix.button.Button` capabilities to an
|
||||
:class:`~kivy.uix.image.Image`, you could do::
|
||||
|
||||
class IconButton(ButtonBehavior, Image):
|
||||
pass
|
||||
|
||||
This would give you an :class:`~kivy.uix.image.Image` with the events and
|
||||
properties inherited from :class:`ButtonBehavior`. For example, the *on_press*
|
||||
and *on_release* events would be fired when appropriate::
|
||||
|
||||
class IconButton(ButtonBehavior, Image):
|
||||
def on_press(self):
|
||||
print("on_press")
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
IconButton:
|
||||
on_press: print('on_press')
|
||||
|
||||
Naturally, you could also bind to any property changes the behavior class
|
||||
offers:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def state_changed(*args):
|
||||
print('state changed')
|
||||
|
||||
button = IconButton()
|
||||
button.bind(state=state_changed)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The behavior class must always be _before_ the widget class. If you don't
|
||||
specify the inheritance in this order, the behavior will not work because
|
||||
the behavior methods are overwritten by the class method listed first.
|
||||
|
||||
Similarly, if you combine a behavior class with a class which
|
||||
requires the use of the methods also defined by the behavior class, the
|
||||
resulting class may not function properly. For example, when combining the
|
||||
:class:`ButtonBehavior` with a :class:`~kivy.uix.slider.Slider`, both of
|
||||
which use the :meth:`~kivy.uix.widget.Widget.on_touch_up` method,
|
||||
the resulting class may not work properly.
|
||||
|
||||
.. versionchanged:: 1.9.1
|
||||
|
||||
The individual behavior classes, previously in one big `behaviors.py`
|
||||
file, has been split into a single file for each class under the
|
||||
:mod:`~kivy.uix.behaviors` module. All the behaviors are still imported
|
||||
in the :mod:`~kivy.uix.behaviors` module so they are accessible as before
|
||||
(e.g. both `from kivy.uix.behaviors import ButtonBehavior` and
|
||||
`from kivy.uix.behaviors.button import ButtonBehavior` work).
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', 'ToggleButtonBehavior', 'DragBehavior',
|
||||
'FocusBehavior', 'CompoundSelectionBehavior',
|
||||
'CodeNavigationBehavior', 'EmacsBehavior', 'CoverBehavior',
|
||||
'TouchRippleBehavior', 'TouchRippleButtonBehavior')
|
||||
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from kivy.uix.behaviors.togglebutton import ToggleButtonBehavior
|
||||
from kivy.uix.behaviors.drag import DragBehavior
|
||||
from kivy.uix.behaviors.focus import FocusBehavior
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.behaviors.codenavigation import CodeNavigationBehavior
|
||||
from kivy.uix.behaviors.emacs import EmacsBehavior
|
||||
from kivy.uix.behaviors.cover import CoverBehavior
|
||||
from kivy.uix.behaviors.touchripple import TouchRippleBehavior
|
||||
from kivy.uix.behaviors.touchripple import TouchRippleButtonBehavior
|
||||
BIN
kivy/uix/behaviors/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/button.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/button.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/codenavigation.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/codenavigation.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/compoundselection.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/compoundselection.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/cover.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/cover.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/drag.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/drag.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/emacs.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/emacs.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/focus.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/focus.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/knspace.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/knspace.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/togglebutton.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/togglebutton.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/uix/behaviors/__pycache__/touchripple.cpython-310.pyc
Normal file
BIN
kivy/uix/behaviors/__pycache__/touchripple.cpython-310.pyc
Normal file
Binary file not shown.
212
kivy/uix/behaviors/button.py
Normal file
212
kivy/uix/behaviors/button.py
Normal file
@@ -0,0 +1,212 @@
|
||||
'''
|
||||
Button Behavior
|
||||
===============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. You can combine this class with
|
||||
other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative buttons that preserve Kivy button behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds button behavior to an image to make a checkbox that
|
||||
behaves like a button::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_press(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
|
||||
def on_release(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
See :class:`~kivy.uix.behaviors.ButtonBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
from kivy.properties import OptionProperty, ObjectProperty, \
|
||||
BooleanProperty, NumericProperty
|
||||
from time import time
|
||||
|
||||
|
||||
class ButtonBehavior(object):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. Please see the
|
||||
:mod:`button behaviors module <kivy.uix.behaviors.button>` documentation
|
||||
for more information.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
|
||||
'''
|
||||
|
||||
state = OptionProperty('normal', options=('normal', 'down'))
|
||||
'''The state of the button, must be one of 'normal' or 'down'.
|
||||
The state is 'down' only when the button is currently touched/clicked,
|
||||
otherwise its 'normal'.
|
||||
|
||||
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'normal'.
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
min_state_time = NumericProperty(0)
|
||||
'''The minimum period of time which the widget must remain in the
|
||||
`'down'` state.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
:attr:`min_state_time` is a float and defaults to 0.035. This value is
|
||||
taken from :class:`~kivy.config.Config`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The default value is now False.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
if 'min_state_time' not in kwargs:
|
||||
self.min_state_time = float(Config.get('graphics',
|
||||
'min_state_time'))
|
||||
super(ButtonBehavior, self).__init__(**kwargs)
|
||||
self.__state_event = None
|
||||
self.__touch_time = None
|
||||
self.fbind('state', self.cancel_event)
|
||||
|
||||
def _do_press(self):
|
||||
self.state = 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
self.state = 'normal'
|
||||
|
||||
def cancel_event(self, *args):
|
||||
if self.__state_event:
|
||||
self.__state_event.cancel()
|
||||
self.__state_event = None
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(ButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.__touch_time = time()
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(ButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(ButtonBehavior, self).on_touch_up(touch)
|
||||
assert(self in touch.ud)
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
|
||||
if (not self.always_release and
|
||||
not self.collide_point(*touch.pos)):
|
||||
self._do_release()
|
||||
return
|
||||
|
||||
touchtime = time() - self.__touch_time
|
||||
if touchtime < self.min_state_time:
|
||||
self.__state_event = Clock.schedule_once(
|
||||
self._do_release, self.min_state_time - touchtime)
|
||||
else:
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
return True
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
def trigger_action(self, duration=0.1):
|
||||
'''Trigger whatever action(s) have been bound to the button by calling
|
||||
both the on_press and on_release callbacks.
|
||||
|
||||
This is similar to a quick button press without using any touch events,
|
||||
but note that like most kivy code, this is not guaranteed to be safe to
|
||||
call from external threads. If needed use
|
||||
:class:`Clock <kivy.clock.Clock>` to safely schedule this function and
|
||||
the resulting callbacks to be called from the main thread.
|
||||
|
||||
Duration is the length of the press in seconds. Pass 0 if you want
|
||||
the action to happen instantly.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
|
||||
def trigger_release(dt):
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
if not duration:
|
||||
trigger_release(0)
|
||||
else:
|
||||
Clock.schedule_once(trigger_release, duration)
|
||||
167
kivy/uix/behaviors/codenavigation.py
Normal file
167
kivy/uix/behaviors/codenavigation.py
Normal file
@@ -0,0 +1,167 @@
|
||||
'''
|
||||
Code Navigation Behavior
|
||||
========================
|
||||
|
||||
The :class:`~kivy.uix.bahviors.CodeNavigationBehavior` modifies navigation
|
||||
behavior in the :class:`~kivy.uix.textinput.TextInput`, making it work like an
|
||||
IDE instead of a word processor.
|
||||
|
||||
Using this mixin gives the TextInput the ability to recognize whitespace,
|
||||
punctuation and case variations (e.g. CamelCase) when moving over text. It
|
||||
is currently used by the :class:`~kivy.uix.codeinput.CodeInput` widget.
|
||||
'''
|
||||
|
||||
__all__ = ('CodeNavigationBehavior', )
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
import string
|
||||
|
||||
|
||||
class CodeNavigationBehavior(EventDispatcher):
|
||||
'''Code navigation behavior. Modifies the navigation behavior in TextInput
|
||||
to work like an IDE instead of a word processor. Please see the
|
||||
:mod:`code navigation behaviors module <kivy.uix.behaviors.codenavigation>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
def _move_cursor_word_left(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
pos -= 1
|
||||
|
||||
if pos == 0:
|
||||
return 0, 0
|
||||
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if col == -1:
|
||||
if row == 0:
|
||||
return 0, 0
|
||||
row -= 1
|
||||
rline = lines[row]
|
||||
col = len(rline)
|
||||
lc = c
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
if lc not in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in punct:
|
||||
col += 1
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
col += 1
|
||||
break
|
||||
if mode == 'us' and c != '_' and (c in punct or c in ws):
|
||||
col += 1
|
||||
break
|
||||
|
||||
if mode == 'us' and c != '_':
|
||||
mode = ('normal' if c in ucase
|
||||
else 'ws' if c in ws
|
||||
else 'camel')
|
||||
elif mode == 'ws' and c not in ws:
|
||||
mode = ('normal' if c in ucase
|
||||
else 'us' if c == '_'
|
||||
else 'punct' if c in punct
|
||||
else 'camel')
|
||||
|
||||
col -= 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == len(lines) - 1:
|
||||
return row, len(lines[row])
|
||||
row += 1
|
||||
col = 0
|
||||
|
||||
return col, row
|
||||
|
||||
def _move_cursor_word_right(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
mrow = len(lines) - 1
|
||||
|
||||
if row == mrow and col == len(lines[row]):
|
||||
return col, row
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c in lcase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if mode in ('normal', 'camel', 'punct') and c in ws:
|
||||
mode = 'ws'
|
||||
elif mode in ('normal', 'camel') and c == '_':
|
||||
mode = 'us'
|
||||
elif mode == 'normal' and c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
if mode == 'us':
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c != '_':
|
||||
break
|
||||
if mode == 'ws' and c not in ws:
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
break
|
||||
if mode != 'punct' and c != '_' and c in punct:
|
||||
break
|
||||
|
||||
col += 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == mrow:
|
||||
return len(rline), mrow
|
||||
row += 1
|
||||
rline = lines[row]
|
||||
col = 0
|
||||
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
break
|
||||
|
||||
return col, row
|
||||
689
kivy/uix/behaviors/compoundselection.py
Normal file
689
kivy/uix/behaviors/compoundselection.py
Normal file
@@ -0,0 +1,689 @@
|
||||
'''
|
||||
Compound Selection Behavior
|
||||
===========================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class implements the logic
|
||||
behind keyboard and touch selection of selectable widgets managed by the
|
||||
derived widget. For example, it can be combined with a
|
||||
:class:`~kivy.uix.gridlayout.GridLayout` to add selection to the layout.
|
||||
|
||||
Compound selection concepts
|
||||
---------------------------
|
||||
|
||||
At its core, it keeps a dynamic list of widgets that can be selected.
|
||||
Then, as the touches and keyboard input are passed in, it selects one or
|
||||
more of the widgets based on these inputs. For example, it uses the mouse
|
||||
scroll and keyboard up/down buttons to scroll through the list of widgets.
|
||||
Multiselection can also be achieved using the keyboard shift and ctrl keys.
|
||||
|
||||
Finally, in addition to the up/down type keyboard inputs, compound selection
|
||||
can also accept letters from the keyboard to be used to select nodes with
|
||||
associated strings that start with those letters, similar to how files
|
||||
are selected by a file browser.
|
||||
|
||||
Selection mechanics
|
||||
-------------------
|
||||
|
||||
When the controller needs to select a node, it calls :meth:`select_node` and
|
||||
:meth:`deselect_node`. Therefore, they must be overwritten in order alter
|
||||
node selection. By default, the class doesn't listen for keyboard or
|
||||
touch events, so the derived widget must call
|
||||
:meth:`select_with_touch`, :meth:`select_with_key_down`, and
|
||||
:meth:`select_with_key_up` on events that it wants to pass on for selection
|
||||
purposes.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
To add selection to a grid layout which will contain
|
||||
:class:`~kivy.uix.Button` widgets. For each button added to the layout, you
|
||||
need to bind the :attr:`~kivy.uix.widget.Widget.on_touch_down` of the button
|
||||
to :meth:`select_with_touch` to pass on the touch events::
|
||||
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.core.window import Window
|
||||
from kivy.app import App
|
||||
|
||||
|
||||
class SelectableGrid(FocusBehavior, CompoundSelectionBehavior, GridLayout):
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key presses will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_down(
|
||||
window, keycode, text, modifiers):
|
||||
return True
|
||||
if self.select_with_key_down(window, keycode, text, modifiers):
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key release will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_up(window, keycode):
|
||||
return True
|
||||
if self.select_with_key_up(window, keycode):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
""" Override the adding of widgets so we can bind and catch their
|
||||
*on_touch_down* events. """
|
||||
widget.bind(on_touch_down=self.button_touch_down,
|
||||
on_touch_up=self.button_touch_up)
|
||||
return super(SelectableGrid, self)\
|
||||
.add_widget(widget, *args, **kwargs)
|
||||
|
||||
def button_touch_down(self, button, touch):
|
||||
""" Use collision detection to select buttons when the touch occurs
|
||||
within their area. """
|
||||
if button.collide_point(*touch.pos):
|
||||
self.select_with_touch(button, touch)
|
||||
|
||||
def button_touch_up(self, button, touch):
|
||||
""" Use collision detection to de-select buttons when the touch
|
||||
occurs outside their area and *touch_multiselect* is not True. """
|
||||
if not (button.collide_point(*touch.pos) or
|
||||
self.touch_multiselect):
|
||||
self.deselect_node(button)
|
||||
|
||||
def select_node(self, node):
|
||||
node.background_color = (1, 0, 0, 1)
|
||||
return super(SelectableGrid, self).select_node(node)
|
||||
|
||||
def deselect_node(self, node):
|
||||
node.background_color = (1, 1, 1, 1)
|
||||
super(SelectableGrid, self).deselect_node(node)
|
||||
|
||||
def on_selected_nodes(self, grid, nodes):
|
||||
print("Selected nodes = {0}".format(nodes))
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
grid = SelectableGrid(cols=3, rows=2, touch_multiselect=True,
|
||||
multiselect=True)
|
||||
for i in range(0, 6):
|
||||
grid.add_widget(Button(text="Button {0}".format(i)))
|
||||
return grid
|
||||
|
||||
|
||||
TestApp().run()
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('CompoundSelectionBehavior', )
|
||||
|
||||
from time import time
|
||||
from os import environ
|
||||
|
||||
from kivy.properties import NumericProperty, BooleanProperty, ListProperty
|
||||
|
||||
|
||||
if 'KIVY_DOC' not in environ:
|
||||
from kivy.config import Config
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
else:
|
||||
_is_desktop = False
|
||||
|
||||
|
||||
class CompoundSelectionBehavior(object):
|
||||
'''The Selection behavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
implements the logic behind keyboard and touch
|
||||
selection of selectable widgets managed by the derived widget. Please see
|
||||
the :mod:`compound selection behaviors module
|
||||
<kivy.uix.behaviors.compoundselection>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
selected_nodes = ListProperty([])
|
||||
'''The list of selected nodes.
|
||||
|
||||
.. note::
|
||||
|
||||
Multiple nodes can be selected right after one another e.g. using the
|
||||
keyboard. When listening to :attr:`selected_nodes`, one should be
|
||||
aware of this.
|
||||
|
||||
:attr:`selected_nodes` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to the empty list, []. It is read-only and should not be modified.
|
||||
'''
|
||||
|
||||
touch_multiselect = BooleanProperty(False)
|
||||
'''A special touch mode which determines whether touch events, as
|
||||
processed by :meth:`select_with_touch`, will add the currently touched
|
||||
node to the selection, or if it will clear the selection before adding the
|
||||
node. This allows the selection of multiple nodes by simply touching them.
|
||||
|
||||
This is different from :attr:`multiselect` because when it is True,
|
||||
simply touching an unselected node will select it, even if ctrl is not
|
||||
pressed. If it is False, however, ctrl must be pressed in order to
|
||||
add to the selection when :attr:`multiselect` is True.
|
||||
|
||||
.. note::
|
||||
|
||||
:attr:`multiselect`, when False, will disable
|
||||
:attr:`touch_multiselect`.
|
||||
|
||||
:attr:`touch_multiselect` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
multiselect = BooleanProperty(False)
|
||||
'''Determines whether multiple nodes can be selected. If enabled, keyboard
|
||||
shift and ctrl selection, optionally combined with touch, for example, will
|
||||
be able to select multiple widgets in the normally expected manner.
|
||||
This dominates :attr:`touch_multiselect` when False.
|
||||
|
||||
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
touch_deselect_last = BooleanProperty(not _is_desktop)
|
||||
'''Determines whether the last selected node can be deselected when
|
||||
:attr:`multiselect` or :attr:`touch_multiselect` is False.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`touch_deselect_last` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True on mobile, False on desktop platforms.
|
||||
'''
|
||||
|
||||
keyboard_select = BooleanProperty(True)
|
||||
'''Determines whether the keyboard can be used for selection. If False,
|
||||
keyboard inputs will be ignored.
|
||||
|
||||
:attr:`keyboard_select` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
page_count = NumericProperty(10)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when pageup (or pagedown) is
|
||||
pressed.
|
||||
|
||||
:attr:`page_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 10.
|
||||
'''
|
||||
|
||||
up_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the up (or down) arrow on
|
||||
the keyboard is pressed.
|
||||
|
||||
:attr:`up_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
right_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the right (or left) arrow
|
||||
on the keyboard is pressed.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
scroll_count = NumericProperty(0)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the mouse scroll wheel is
|
||||
scrolled.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
nodes_order_reversed = BooleanProperty(True)
|
||||
''' (Internal) Indicates whether the order of the nodes as displayed top-
|
||||
down is reversed compared to their order in :meth:`get_selectable_nodes`
|
||||
(e.g. how the children property is reversed compared to how
|
||||
it's displayed).
|
||||
'''
|
||||
|
||||
text_entry_timeout = NumericProperty(1.)
|
||||
'''When typing characters in rapid succession (i.e. the time difference since
|
||||
the last character is less than :attr:`text_entry_timeout`), the keys get
|
||||
concatenated and the combined text is passed as the key argument of
|
||||
:meth:`goto_node`.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
_anchor = None # the last anchor node selected (e.g. shift relative node)
|
||||
# the idx may be out of sync
|
||||
_anchor_idx = 0 # cache indexs in case list hasn't changed
|
||||
_last_selected_node = None # the absolute last node selected
|
||||
_last_node_idx = 0
|
||||
_ctrl_down = False # if it's pressed - for e.g. shift selection
|
||||
_shift_down = False
|
||||
# holds str used to find node, e.g. if word is typed. passed to goto_node
|
||||
_word_filter = ''
|
||||
_last_key_time = 0 # time since last press, for finding whole strs in node
|
||||
_key_list = [] # keys that are already pressed, to not press continuously
|
||||
_offset_counts = {} # cache of counts for faster access
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CompoundSelectionBehavior, self).__init__(**kwargs)
|
||||
self._key_list = []
|
||||
|
||||
def ensure_single_select(*l):
|
||||
if (not self.multiselect) and len(self.selected_nodes) > 1:
|
||||
self.clear_selection()
|
||||
update_counts = self._update_counts
|
||||
update_counts()
|
||||
fbind = self.fbind
|
||||
fbind('multiselect', ensure_single_select)
|
||||
fbind('page_count', update_counts)
|
||||
fbind('up_count', update_counts)
|
||||
fbind('right_count', update_counts)
|
||||
fbind('scroll_count', update_counts)
|
||||
|
||||
def select_with_touch(self, node, touch=None):
|
||||
'''(internal) Processes a touch on the node. This should be called by
|
||||
the derived widget when a node is touched and is to be used for
|
||||
selection. Depending on the keyboard keys pressed and the
|
||||
configuration, it could select or deslect this and other nodes in the
|
||||
selectable nodes list, :meth:`get_selectable_nodes`.
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node that received the touch. Can be None for a scroll
|
||||
type touch.
|
||||
`touch`
|
||||
Optionally, the touch. Defaults to None.
|
||||
|
||||
:Returns:
|
||||
bool, True if the touch was used, False otherwise.
|
||||
'''
|
||||
multi = self.multiselect
|
||||
multiselect = multi and (self._ctrl_down or self.touch_multiselect)
|
||||
range_select = multi and self._shift_down
|
||||
|
||||
if touch and 'button' in touch.profile and touch.button in\
|
||||
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
|
||||
node_src, idx_src = self._resolve_last_node()
|
||||
node, idx = self.goto_node(touch.button, node_src, idx_src)
|
||||
if node == node_src:
|
||||
return False
|
||||
if range_select:
|
||||
self._select_range(multiselect, True, node, idx)
|
||||
else:
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
if node is None:
|
||||
return False
|
||||
|
||||
if (node in self.selected_nodes and (not range_select)): # selected
|
||||
if multiselect:
|
||||
self.deselect_node(node)
|
||||
else:
|
||||
selected_node_count = len(self.selected_nodes)
|
||||
self.clear_selection()
|
||||
if not self.touch_deselect_last or selected_node_count > 1:
|
||||
self.select_node(node)
|
||||
elif range_select:
|
||||
# keep anchor only if not multiselect (ctrl-type selection)
|
||||
self._select_range(multiselect, not multiselect, node, 0)
|
||||
else: # it's not selected at this point
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
|
||||
def select_with_key_down(self, keyboard, scancode, codepoint, modifiers,
|
||||
**kwargs):
|
||||
'''Processes a key press. This is called when a key press is to be used
|
||||
for selection. Depending on the keyboard keys pressed and the
|
||||
configuration, it could select or deselect nodes or node ranges
|
||||
from the selectable nodes list, :meth:`get_selectable_nodes`.
|
||||
|
||||
The parameters are such that it could be bound directly to the
|
||||
on_key_down event of a keyboard. Therefore, it is safe to be called
|
||||
repeatedly when the key is held down as is done by the keyboard.
|
||||
|
||||
:Returns:
|
||||
bool, True if the keypress was used, False otherwise.
|
||||
'''
|
||||
if not self.keyboard_select:
|
||||
return False
|
||||
keys = self._key_list
|
||||
multi = self.multiselect
|
||||
node_src, idx_src = self._resolve_last_node()
|
||||
text = scancode[1]
|
||||
|
||||
if text == 'shift':
|
||||
self._shift_down = True
|
||||
elif text in ('ctrl', 'lctrl', 'rctrl'):
|
||||
self._ctrl_down = True
|
||||
elif (multi and 'ctrl' in modifiers and text in ('a', 'A') and
|
||||
text not in keys):
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
select = self.select_node
|
||||
for node in sister_nodes:
|
||||
select(node)
|
||||
keys.append(text)
|
||||
else:
|
||||
s = text
|
||||
if len(text) > 1:
|
||||
d = {'divide': '/', 'mul': '*', 'substract': '-', 'add': '+',
|
||||
'decimal': '.'}
|
||||
if text.startswith('numpad'):
|
||||
s = text[6:]
|
||||
if len(s) > 1:
|
||||
if s in d:
|
||||
s = d[s]
|
||||
else:
|
||||
s = None
|
||||
else:
|
||||
s = None
|
||||
|
||||
if s is not None:
|
||||
if s not in keys: # don't keep adding while holding down
|
||||
if time() - self._last_key_time <= self.text_entry_timeout:
|
||||
self._word_filter += s
|
||||
else:
|
||||
self._word_filter = s
|
||||
keys.append(s)
|
||||
|
||||
self._last_key_time = time()
|
||||
node, idx = self.goto_node(self._word_filter, node_src,
|
||||
idx_src)
|
||||
else:
|
||||
self._word_filter = ''
|
||||
node, idx = self.goto_node(text, node_src, idx_src)
|
||||
|
||||
if node == node_src:
|
||||
return False
|
||||
|
||||
multiselect = multi and 'ctrl' in modifiers
|
||||
if multi and 'shift' in modifiers:
|
||||
self._select_range(multiselect, True, node, idx)
|
||||
else:
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
self._word_filter = ''
|
||||
return False
|
||||
|
||||
def select_with_key_up(self, keyboard, scancode, **kwargs):
|
||||
'''(internal) Processes a key release. This must be called by the
|
||||
derived widget when a key that :meth:`select_with_key_down` returned
|
||||
True is released.
|
||||
|
||||
The parameters are such that it could be bound directly to the
|
||||
on_key_up event of a keyboard.
|
||||
|
||||
:Returns:
|
||||
bool, True if the key release was used, False otherwise.
|
||||
'''
|
||||
if scancode[1] == 'shift':
|
||||
self._shift_down = False
|
||||
elif scancode[1] in ('ctrl', 'lctrl', 'rctrl'):
|
||||
self._ctrl_down = False
|
||||
else:
|
||||
try:
|
||||
self._key_list.remove(scancode[1])
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _update_counts(self, *largs):
|
||||
# doesn't invert indices here
|
||||
pc = self.page_count
|
||||
uc = self.up_count
|
||||
rc = self.right_count
|
||||
sc = self.scroll_count
|
||||
self._offset_counts = {'pageup': -pc, 'pagedown': pc, 'up': -uc,
|
||||
'down': uc, 'right': rc, 'left': -rc, 'scrollup': sc,
|
||||
'scrolldown': -sc, 'scrollright': -sc, 'scrollleft': sc}
|
||||
|
||||
def _resolve_last_node(self):
|
||||
# for offset selection, we have a anchor, and we select everything
|
||||
# between anchor and added offset relative to last node
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
if not len(sister_nodes):
|
||||
return None, 0
|
||||
last_node = self._last_selected_node
|
||||
last_idx = self._last_node_idx
|
||||
end = len(sister_nodes) - 1
|
||||
|
||||
if last_node is None:
|
||||
last_node = self._anchor
|
||||
last_idx = self._anchor_idx
|
||||
if last_node is None:
|
||||
return sister_nodes[end], end
|
||||
if last_idx > end or sister_nodes[last_idx] != last_node:
|
||||
try:
|
||||
return last_node, self.get_index_of_node(last_node,
|
||||
sister_nodes)
|
||||
except ValueError:
|
||||
return sister_nodes[end], end
|
||||
return last_node, last_idx
|
||||
|
||||
def _select_range(self, multiselect, keep_anchor, node, idx):
|
||||
'''Selects a range between self._anchor and node or idx.
|
||||
If multiselect is True, it will be added to the selection, otherwise
|
||||
it will unselect everything before selecting the range. This is only
|
||||
called if self.multiselect is True.
|
||||
If keep anchor is False, the anchor is moved to node. This should
|
||||
always be True for keyboard selection.
|
||||
'''
|
||||
select = self.select_node
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
end = len(sister_nodes) - 1
|
||||
last_node = self._anchor
|
||||
last_idx = self._anchor_idx
|
||||
|
||||
if last_node is None:
|
||||
last_idx = end
|
||||
last_node = sister_nodes[end]
|
||||
else:
|
||||
if last_idx > end or sister_nodes[last_idx] != last_node:
|
||||
try:
|
||||
last_idx = self.get_index_of_node(last_node, sister_nodes)
|
||||
except ValueError:
|
||||
# list changed - cannot do select across them
|
||||
return
|
||||
if idx > end or sister_nodes[idx] != node:
|
||||
try: # just in case
|
||||
idx = self.get_index_of_node(node, sister_nodes)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if last_idx > idx:
|
||||
last_idx, idx = idx, last_idx
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
for item in sister_nodes[last_idx:idx + 1]:
|
||||
select(item)
|
||||
|
||||
if keep_anchor:
|
||||
self._anchor = last_node
|
||||
self._anchor_idx = last_idx
|
||||
else:
|
||||
self._anchor = node # in case idx was reversed, reset
|
||||
self._anchor_idx = idx
|
||||
self._last_selected_node = node
|
||||
self._last_node_idx = idx
|
||||
|
||||
def clear_selection(self):
|
||||
''' Deselects all the currently selected nodes.
|
||||
'''
|
||||
# keep the anchor and last selected node
|
||||
deselect = self.deselect_node
|
||||
nodes = self.selected_nodes
|
||||
# empty beforehand so lookup in deselect will be fast
|
||||
for node in nodes[:]:
|
||||
deselect(node)
|
||||
|
||||
def get_selectable_nodes(self):
|
||||
'''(internal) Returns a list of the nodes that can be selected. It can
|
||||
be overwritten by the derived widget to return the correct list.
|
||||
|
||||
This list is used to determine which nodes to select with group
|
||||
selection. E.g. the last element in the list will be selected when
|
||||
home is pressed, pagedown will move (or add to, if shift is held) the
|
||||
selection from the current position by negative :attr:`page_count`
|
||||
nodes starting from the position of the currently selected node in
|
||||
this list and so on. Still, nodes can be selected even if they are not
|
||||
in this list.
|
||||
|
||||
.. note::
|
||||
|
||||
It is safe to dynamically change this list including removing,
|
||||
adding, or re-arranging its elements. Nodes can be selected even
|
||||
if they are not on this list. And selected nodes removed from the
|
||||
list will remain selected until :meth:`deselect_node` is called.
|
||||
|
||||
.. warning::
|
||||
|
||||
Layouts display their children in the reverse order. That is, the
|
||||
contents of :attr:`~kivy.uix.widget.Widget.children` is displayed
|
||||
form right to left, bottom to top. Therefore, internally, the
|
||||
indices of the elements returned by this function are reversed to
|
||||
make it work by default for most layouts so that the final result
|
||||
is consistent e.g. home, although it will select the last element
|
||||
in this list visually, will select the first element when
|
||||
counting from top to bottom and left to right. If this behavior is
|
||||
not desired, a reversed list should be returned instead.
|
||||
|
||||
Defaults to returning :attr:`~kivy.uix.widget.Widget.children`.
|
||||
'''
|
||||
return self.children
|
||||
|
||||
def get_index_of_node(self, node, selectable_nodes):
|
||||
'''(internal) Returns the index of the `node` within the
|
||||
`selectable_nodes` returned by :meth:`get_selectable_nodes`.
|
||||
'''
|
||||
return selectable_nodes.index(node)
|
||||
|
||||
def goto_node(self, key, last_node, last_node_idx):
|
||||
'''(internal) Used by the controller to get the node at the position
|
||||
indicated by key. The key can be keyboard inputs, e.g. pageup,
|
||||
or scroll inputs from the mouse scroll wheel, e.g. scrollup.
|
||||
'last_node' is the last node selected and is used to find the resulting
|
||||
node. For example, if the key is up, the returned node is one node
|
||||
up from the last node.
|
||||
|
||||
It can be overwritten by the derived widget.
|
||||
|
||||
:Parameters:
|
||||
`key`
|
||||
str, the string used to find the desired node. It can be any
|
||||
of the keyboard keys, as well as the mouse scrollup,
|
||||
scrolldown, scrollright, and scrollleft strings. If letters
|
||||
are typed in quick succession, the letters will be combined
|
||||
before it's passed in as key and can be used to find nodes that
|
||||
have an associated string that starts with those letters.
|
||||
`last_node`
|
||||
The last node that was selected.
|
||||
`last_node_idx`
|
||||
The cached index of the last node selected in the
|
||||
:meth:`get_selectable_nodes` list. If the list hasn't changed
|
||||
it saves having to look up the index of `last_node` in that
|
||||
list.
|
||||
|
||||
:Returns:
|
||||
tuple, the node targeted by key and its index in the
|
||||
:meth:`get_selectable_nodes` list. Returning
|
||||
`(last_node, last_node_idx)` indicates a node wasn't found.
|
||||
'''
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
end = len(sister_nodes) - 1
|
||||
counts = self._offset_counts
|
||||
if end == -1:
|
||||
return last_node, last_node_idx
|
||||
if last_node_idx > end or sister_nodes[last_node_idx] != last_node:
|
||||
try: # just in case
|
||||
last_node_idx = self.get_index_of_node(last_node, sister_nodes)
|
||||
except ValueError:
|
||||
return last_node, last_node_idx
|
||||
|
||||
is_reversed = self.nodes_order_reversed
|
||||
if key in counts:
|
||||
count = -counts[key] if is_reversed else counts[key]
|
||||
idx = max(min(count + last_node_idx, end), 0)
|
||||
return sister_nodes[idx], idx
|
||||
elif key == 'home':
|
||||
if is_reversed:
|
||||
return sister_nodes[end], end
|
||||
return sister_nodes[0], 0
|
||||
elif key == 'end':
|
||||
if is_reversed:
|
||||
return sister_nodes[0], 0
|
||||
return sister_nodes[end], end
|
||||
else:
|
||||
return last_node, last_node_idx
|
||||
|
||||
def select_node(self, node):
|
||||
''' Selects a node.
|
||||
|
||||
It is called by the controller when it selects a node and can be
|
||||
called from the outside to select a node directly. The derived widget
|
||||
should overwrite this method and change the node state to selected
|
||||
when called.
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node to be selected.
|
||||
|
||||
:Returns:
|
||||
bool, True if the node was selected, False otherwise.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method must be called by the derived widget using super if it
|
||||
is overwritten.
|
||||
'''
|
||||
nodes = self.selected_nodes
|
||||
if node in nodes:
|
||||
return False
|
||||
|
||||
if (not self.multiselect) and len(nodes):
|
||||
self.clear_selection()
|
||||
if node not in nodes:
|
||||
nodes.append(node)
|
||||
self._anchor = node
|
||||
self._last_selected_node = node
|
||||
return True
|
||||
|
||||
def deselect_node(self, node):
|
||||
''' Deselects a possibly selected node.
|
||||
|
||||
It is called by the controller when it deselects a node and can also
|
||||
be called from the outside to deselect a node directly. The derived
|
||||
widget should overwrite this method and change the node to its
|
||||
unselected state when this is called
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node to be deselected.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method must be called by the derived widget using super if it
|
||||
is overwritten.
|
||||
'''
|
||||
try:
|
||||
self.selected_nodes.remove(node)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
160
kivy/uix/behaviors/cover.py
Normal file
160
kivy/uix/behaviors/cover.py
Normal file
@@ -0,0 +1,160 @@
|
||||
'''
|
||||
Cover Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.cover.CoverBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ is intended for rendering
|
||||
textures to full widget size keeping the aspect ratio of the original texture.
|
||||
|
||||
Use cases are i.e. rendering full size background images or video content in
|
||||
a dynamic layout.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following examples add cover behavior to an image:
|
||||
|
||||
In python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import CoverBehavior
|
||||
from kivy.uix.image import Image
|
||||
|
||||
|
||||
class CoverImage(CoverBehavior, Image):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverImage, self).__init__(**kwargs)
|
||||
texture = self._coreimage.texture
|
||||
self.reference_size = texture.size
|
||||
self.texture = texture
|
||||
|
||||
|
||||
class MainApp(App):
|
||||
|
||||
def build(self):
|
||||
return CoverImage(source='image.jpg')
|
||||
|
||||
MainApp().run()
|
||||
|
||||
In Kivy Language:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
CoverImage:
|
||||
source: 'image.png'
|
||||
|
||||
<CoverImage@CoverBehavior+Image>:
|
||||
reference_size: self.texture_size
|
||||
|
||||
See :class:`~kivy.uix.behaviors.cover.CoverBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('CoverBehavior', )
|
||||
|
||||
from decimal import Decimal
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ListProperty
|
||||
|
||||
|
||||
Builder.load_string("""
|
||||
<-CoverBehavior>:
|
||||
canvas.before:
|
||||
StencilPush
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilUse
|
||||
canvas:
|
||||
Rectangle:
|
||||
texture: self.texture
|
||||
size: self.cover_size
|
||||
pos: self.cover_pos
|
||||
canvas.after:
|
||||
StencilUnUse
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilPop
|
||||
""")
|
||||
|
||||
|
||||
class CoverBehavior(object):
|
||||
'''The CoverBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
provides rendering a texture covering full widget size keeping aspect ratio
|
||||
of the original texture.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
reference_size = ListProperty([])
|
||||
'''Reference size used for aspect ratio approximation calculation.
|
||||
|
||||
:attr:`reference_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[]`.
|
||||
'''
|
||||
|
||||
cover_size = ListProperty([0, 0])
|
||||
'''Size of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
cover_pos = ListProperty([0, 0])
|
||||
'''Position of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_pos` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverBehavior, self).__init__(**kwargs)
|
||||
# bind covering
|
||||
self.bind(
|
||||
size=self.calculate_cover,
|
||||
pos=self.calculate_cover
|
||||
)
|
||||
|
||||
def _aspect_ratio_approximate(self, size):
|
||||
# return a decimal approximation of an aspect ratio.
|
||||
return Decimal('%.2f' % (float(size[0]) / size[1]))
|
||||
|
||||
def _scale_size(self, size, sizer):
|
||||
# return scaled size based on sizer, where sizer (n, None) scales x
|
||||
# to n and (None, n) scales y to n
|
||||
size_new = list(sizer)
|
||||
i = size_new.index(None)
|
||||
j = i * -1 + 1
|
||||
size_new[i] = (size_new[j] * size[i]) / size[j]
|
||||
return tuple(size_new)
|
||||
|
||||
def calculate_cover(self, *args):
|
||||
# return if no reference size yet
|
||||
if not self.reference_size:
|
||||
return
|
||||
size = self.size
|
||||
origin_appr = self._aspect_ratio_approximate(self.reference_size)
|
||||
crop_appr = self._aspect_ratio_approximate(size)
|
||||
# same aspect ratio
|
||||
if origin_appr == crop_appr:
|
||||
crop_size = self.size
|
||||
offset = (0, 0)
|
||||
# scale x
|
||||
elif origin_appr < crop_appr:
|
||||
crop_size = self._scale_size(self.reference_size, (size[0], None))
|
||||
offset = (0, ((crop_size[1] - size[1]) / 2) * -1)
|
||||
# scale y
|
||||
else:
|
||||
crop_size = self._scale_size(self.reference_size, (None, size[1]))
|
||||
offset = (((crop_size[0] - size[0]) / 2) * -1, 0)
|
||||
# set background size and position
|
||||
self.cover_size = crop_size
|
||||
self.cover_pos = offset
|
||||
234
kivy/uix/behaviors/drag.py
Normal file
234
kivy/uix/behaviors/drag.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Drag Behavior
|
||||
=============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.drag.DragBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides Drag behavior.
|
||||
When combined with a widget, dragging in the rectangle defined by the
|
||||
:attr:`~kivy.uix.behaviors.drag.DragBehavior.drag_rectangle` will drag the
|
||||
widget.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example creates a draggable label::
|
||||
|
||||
from kivy.uix.label import Label
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import DragBehavior
|
||||
from kivy.lang import Builder
|
||||
|
||||
# You could also put the following in your kv file...
|
||||
kv = '''
|
||||
<DragLabel>:
|
||||
# Define the properties for the DragLabel
|
||||
drag_rectangle: self.x, self.y, self.width, self.height
|
||||
drag_timeout: 10000000
|
||||
drag_distance: 0
|
||||
|
||||
FloatLayout:
|
||||
# Define the root widget
|
||||
DragLabel:
|
||||
size_hint: 0.25, 0.2
|
||||
text: 'Drag me'
|
||||
'''
|
||||
|
||||
|
||||
class DragLabel(DragBehavior, Label):
|
||||
pass
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return Builder.load_string(kv)
|
||||
|
||||
TestApp().run()
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ('DragBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import NumericProperty, ReferenceListProperty
|
||||
from kivy.config import Config
|
||||
from kivy.metrics import sp
|
||||
from functools import partial
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_scroll_timeout = _scroll_distance = 0
|
||||
if Config:
|
||||
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
|
||||
_scroll_distance = Config.getint('widgets', 'scroll_distance')
|
||||
|
||||
|
||||
class DragBehavior(object):
|
||||
'''
|
||||
The DragBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_ provides
|
||||
Drag behavior. When combined with a widget, dragging in the rectangle
|
||||
defined by :attr:`drag_rectangle` will drag the widget. Please see
|
||||
the :mod:`drag behaviors module <kivy.uix.behaviors.drag>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
drag_distance = NumericProperty(_scroll_distance)
|
||||
'''Distance to move before dragging the :class:`DragBehavior`, in pixels.
|
||||
As soon as the distance has been traveled, the :class:`DragBehavior` will
|
||||
start to drag, and no touch event will be dispatched to the children.
|
||||
It is advisable that you base this value on the dpi of your target device's
|
||||
screen.
|
||||
|
||||
:attr:`drag_distance` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_distance` as defined in the user
|
||||
:class:`~kivy.config.Config` (20 pixels by default).
|
||||
'''
|
||||
|
||||
drag_timeout = NumericProperty(_scroll_timeout)
|
||||
'''Timeout allowed to trigger the :attr:`drag_distance`, in milliseconds.
|
||||
If the user has not moved :attr:`drag_distance` within the timeout,
|
||||
dragging will be disabled, and the touch event will be dispatched to the
|
||||
children.
|
||||
|
||||
:attr:`drag_timeout` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_timeout` as defined in the user
|
||||
:class:`~kivy.config.Config` (55 milliseconds by default).
|
||||
'''
|
||||
|
||||
drag_rect_x = NumericProperty(0)
|
||||
'''X position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_x` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_y = NumericProperty(0)
|
||||
'''Y position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_Y` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_width = NumericProperty(100)
|
||||
'''Width of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rect_height = NumericProperty(100)
|
||||
'''Height of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rectangle = ReferenceListProperty(drag_rect_x, drag_rect_y,
|
||||
drag_rect_width, drag_rect_height)
|
||||
'''Position and size of the axis aligned bounding rectangle where dragging
|
||||
is allowed.
|
||||
|
||||
:attr:`drag_rectangle` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`drag_rect_x`, :attr:`drag_rect_y`, :attr:`drag_rect_width`,
|
||||
:attr:`drag_rect_height`) properties.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._drag_touch = None
|
||||
super(DragBehavior, self).__init__(**kwargs)
|
||||
|
||||
def _get_uid(self, prefix='sv'):
|
||||
return '{0}.{1}'.format(prefix, self.uid)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
xx, yy, w, h = self.drag_rectangle
|
||||
x, y = touch.pos
|
||||
if not self.collide_point(x, y):
|
||||
touch.ud[self._get_uid('svavoid')] = True
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
if self._drag_touch or ('button' in touch.profile and
|
||||
touch.button.startswith('scroll')) or\
|
||||
not ((xx < x <= xx + w) and (yy < y <= yy + h)):
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
|
||||
# no mouse scrolling, so the user is going to drag with this touch.
|
||||
self._drag_touch = touch
|
||||
uid = self._get_uid()
|
||||
touch.grab(self)
|
||||
touch.ud[uid] = {
|
||||
'mode': 'unknown',
|
||||
'dx': 0,
|
||||
'dy': 0}
|
||||
Clock.schedule_once(self._change_touch_mode,
|
||||
self.drag_timeout / 1000.)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud or\
|
||||
self._drag_touch is not touch:
|
||||
return super(DragBehavior, self).on_touch_move(touch) or\
|
||||
self._get_uid() in touch.ud
|
||||
if touch.grab_current is not self:
|
||||
return True
|
||||
|
||||
uid = self._get_uid()
|
||||
ud = touch.ud[uid]
|
||||
mode = ud['mode']
|
||||
if mode == 'unknown':
|
||||
ud['dx'] += abs(touch.dx)
|
||||
ud['dy'] += abs(touch.dy)
|
||||
if ud['dx'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
if ud['dy'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
ud['mode'] = mode
|
||||
if mode == 'drag':
|
||||
self.x += touch.dx
|
||||
self.y += touch.dy
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud:
|
||||
return super(DragBehavior, self).on_touch_up(touch)
|
||||
|
||||
if self._drag_touch and self in [x() for x in touch.grab_list]:
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
ud = touch.ud[self._get_uid()]
|
||||
if ud['mode'] == 'unknown':
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
|
||||
else:
|
||||
if self._drag_touch is not touch:
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
|
||||
def _do_touch_up(self, touch, *largs):
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
# don't forget about grab event!
|
||||
for x in touch.grab_list[:]:
|
||||
touch.grab_list.remove(x)
|
||||
x = x()
|
||||
if not x:
|
||||
continue
|
||||
touch.grab_current = x
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
touch.grab_current = None
|
||||
|
||||
def _change_touch_mode(self, *largs):
|
||||
if not self._drag_touch:
|
||||
return
|
||||
uid = self._get_uid()
|
||||
touch = self._drag_touch
|
||||
ud = touch.ud[uid]
|
||||
if ud['mode'] != 'unknown':
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.parent.to_widget)
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
touch.pop()
|
||||
return
|
||||
140
kivy/uix/behaviors/emacs.py
Normal file
140
kivy/uix/behaviors/emacs.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
Emacs Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.emacs.EmacsBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ allows you to add
|
||||
`Emacs <https://www.gnu.org/software/emacs/>`_ keyboard shortcuts for basic
|
||||
movement and editing to the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
The shortcuts currently available are listed below:
|
||||
|
||||
Emacs shortcuts
|
||||
---------------
|
||||
=============== ========================================================
|
||||
Shortcut Description
|
||||
--------------- --------------------------------------------------------
|
||||
Control + a Move cursor to the beginning of the line
|
||||
Control + e Move cursor to the end of the line
|
||||
Control + f Move cursor one character to the right
|
||||
Control + b Move cursor one character to the left
|
||||
Alt + f Move cursor to the end of the word to the right
|
||||
Alt + b Move cursor to the start of the word to the left
|
||||
Alt + Backspace Delete text left of the cursor to the beginning of word
|
||||
Alt + d Delete text right of the cursor to the end of the word
|
||||
Alt + w Copy selection
|
||||
Control + w Cut selection
|
||||
Control + y Paste selection
|
||||
=============== ========================================================
|
||||
|
||||
.. warning::
|
||||
If you have the :mod:`~kivy.modules.inspector` module enabled, the
|
||||
shortcut for opening the inspector (Control + e) conflicts with the
|
||||
Emacs shortcut to move to the end of the line (it will still move the
|
||||
cursor to the end of the line, but the inspector will open as well).
|
||||
'''
|
||||
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
|
||||
__all__ = ('EmacsBehavior', )
|
||||
|
||||
|
||||
class EmacsBehavior(object):
|
||||
'''
|
||||
A `mixin <https://en.wikipedia.org/wiki/Mixin>`_ that enables Emacs-style
|
||||
keyboard shortcuts for the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
Please see the :mod:`Emacs behaviors module <kivy.uix.behaviors.emacs>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
key_bindings = StringProperty('emacs')
|
||||
'''String name which determines the type of key bindings to use with the
|
||||
:class:`~kivy.uix.textinput.TextInput`. This allows Emacs key bindings to
|
||||
be enabled/disabled programmatically for widgets that inherit from
|
||||
:class:`EmacsBehavior`. If the value is not ``'emacs'``, Emacs bindings
|
||||
will be disabled. Use ``'default'`` for switching to the default key
|
||||
bindings of TextInput.
|
||||
|
||||
:attr:`key_bindings` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to ``'emacs'``.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EmacsBehavior, self).__init__(**kwargs)
|
||||
|
||||
self.bindings = {
|
||||
'ctrl': {
|
||||
'a': lambda: self.do_cursor_movement('cursor_home'),
|
||||
'e': lambda: self.do_cursor_movement('cursor_end'),
|
||||
'f': lambda: self.do_cursor_movement('cursor_right'),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left'),
|
||||
'w': lambda: self._cut(self.selection_text),
|
||||
'y': self.paste,
|
||||
},
|
||||
'alt': {
|
||||
'w': self.copy,
|
||||
'f': lambda: self.do_cursor_movement('cursor_right',
|
||||
control=True),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left',
|
||||
control=True),
|
||||
'd': self.delete_word_right,
|
||||
'\x08': self.delete_word_left, # alt + backspace
|
||||
},
|
||||
}
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
|
||||
key, key_str = keycode
|
||||
|
||||
# join the modifiers e.g. ['alt', 'ctrl']
|
||||
mod = '+'.join(modifiers) if modifiers else None
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if key in range(256) and self.key_bindings == 'emacs':
|
||||
if mod == 'ctrl' and chr(key) in self.bindings['ctrl'].keys():
|
||||
is_emacs_shortcut = True
|
||||
elif mod == 'alt' and chr(key) in self.bindings['alt'].keys():
|
||||
is_emacs_shortcut = True
|
||||
else: # e.g. ctrl+alt or alt+ctrl (alt-gr key)
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if is_emacs_shortcut:
|
||||
# Look up mod and key
|
||||
emacs_shortcut = self.bindings[mod][chr(key)]
|
||||
emacs_shortcut()
|
||||
else:
|
||||
super(EmacsBehavior, self).keyboard_on_key_down(window, keycode,
|
||||
text, modifiers)
|
||||
|
||||
def delete_word_right(self):
|
||||
'''Delete text right of the cursor to the end of the word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
start_cursor = self.cursor
|
||||
self.do_cursor_movement('cursor_right', control=True)
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[start_index:end_index]
|
||||
self._set_unredo_delsel(start_index, end_index, s, from_undo=False)
|
||||
self.text = self.text[:start_index] + self.text[end_index:]
|
||||
self._set_cursor(pos=start_cursor)
|
||||
|
||||
def delete_word_left(self):
|
||||
'''Delete text left of the cursor to the beginning of word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
self.do_cursor_movement('cursor_left', control=True)
|
||||
end_cursor = self.cursor
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[end_index:start_index]
|
||||
self._set_unredo_delsel(end_index, start_index, s, from_undo=False)
|
||||
self.text = self.text[:end_index] + self.text[start_index:]
|
||||
self._set_cursor(pos=end_cursor)
|
||||
587
kivy/uix/behaviors/focus.py
Normal file
587
kivy/uix/behaviors/focus.py
Normal file
@@ -0,0 +1,587 @@
|
||||
'''
|
||||
Focus Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.FocusBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
keyboard focus behavior. When combined with other
|
||||
FocusBehavior widgets it allows one to cycle focus among them by pressing
|
||||
tab. In addition, upon gaining focus, the instance will automatically
|
||||
receive keyboard input.
|
||||
|
||||
Focus, very different from selection, is intimately tied with the keyboard;
|
||||
each keyboard can focus on zero or one widgets, and each widget can only
|
||||
have the focus of one keyboard. However, multiple keyboards can focus
|
||||
simultaneously on different widgets. When escape is hit, the widget having
|
||||
the focus of that keyboard will de-focus.
|
||||
|
||||
Managing focus
|
||||
--------------
|
||||
|
||||
In essence, focus is implemented as a doubly linked list, where each
|
||||
node holds a (weak) reference to the instance before it and after it,
|
||||
as visualized when cycling through the nodes using tab (forward) or
|
||||
shift+tab (backward). If a previous or next widget is not specified,
|
||||
:attr:`focus_next` and :attr:`focus_previous` defaults to `None`. This
|
||||
means that the :attr:`~kivy.uix.widget.Widget.children` list and
|
||||
:attr:`parents <kivy.uix.widget.Widget.parent>` are
|
||||
walked to find the next focusable widget, unless :attr:`focus_next` or
|
||||
:attr:`focus_previous` is set to the `StopIteration` class, in which case
|
||||
focus stops there.
|
||||
|
||||
For example, to cycle focus between :class:`~kivy.uix.button.Button`
|
||||
elements of a :class:`~kivy.uix.gridlayout.GridLayout`::
|
||||
|
||||
class FocusButton(FocusBehavior, Button):
|
||||
pass
|
||||
|
||||
grid = GridLayout(cols=4)
|
||||
for i in range(40):
|
||||
grid.add_widget(FocusButton(text=str(i)))
|
||||
# clicking on a widget will activate focus, and tab can now be used
|
||||
# to cycle through
|
||||
|
||||
When using a software keyboard, typical on mobile and touch devices, the
|
||||
keyboard display behavior is determined by the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
|
||||
this property to ensure the focused widget is not covered or obscured by the
|
||||
keyboard.
|
||||
|
||||
Initializing focus
|
||||
------------------
|
||||
|
||||
Widgets needs to be visible before they can receive the focus. This means that
|
||||
setting their *focus* property to True before they are visible will have no
|
||||
effect. To initialize focus, you can use the 'on_parent' event::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.textinput import TextInput
|
||||
|
||||
class MyTextInput(TextInput):
|
||||
def on_parent(self, widget, parent):
|
||||
self.focus = True
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyTextInput()
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
If you are using a :class:`~kivy.uix.popup`, you can use the 'on_open' event.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
'''
|
||||
|
||||
__all__ = ('FocusBehavior', )
|
||||
|
||||
from kivy.properties import OptionProperty, ObjectProperty, BooleanProperty, \
|
||||
AliasProperty
|
||||
from kivy.config import Config
|
||||
from kivy.base import EventLoop
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_is_desktop = False
|
||||
_keyboard_mode = 'system'
|
||||
if Config:
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
_keyboard_mode = Config.get('kivy', 'keyboard_mode')
|
||||
|
||||
|
||||
class FocusBehavior(object):
|
||||
'''Provides keyboard focus behavior. When combined with other
|
||||
FocusBehavior widgets it allows one to cycle focus among them by pressing
|
||||
tab. Please see the
|
||||
:mod:`focus behavior module documentation <kivy.uix.behaviors.focus>`
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
'''
|
||||
|
||||
_requested_keyboard = False
|
||||
_keyboard = ObjectProperty(None, allownone=True)
|
||||
_keyboards = {}
|
||||
|
||||
ignored_touch = []
|
||||
'''A list of touches that should not be used to defocus. After on_touch_up,
|
||||
every touch that is not in :attr:`ignored_touch` will defocus all the
|
||||
focused widgets if the config keyboard mode is not multi. Touches on
|
||||
focusable widgets that were used to focus are automatically added here.
|
||||
|
||||
Example usage::
|
||||
|
||||
class Unfocusable(Widget):
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
FocusBehavior.ignored_touch.append(touch)
|
||||
|
||||
Notice that you need to access this as a class, not an instance variable.
|
||||
'''
|
||||
|
||||
def _set_keyboard(self, value):
|
||||
focus = self.focus
|
||||
keyboard = self._keyboard
|
||||
keyboards = FocusBehavior._keyboards
|
||||
if keyboard:
|
||||
self.focus = False # this'll unbind
|
||||
if self._keyboard: # remove assigned keyboard from dict
|
||||
del keyboards[keyboard]
|
||||
if value and value not in keyboards:
|
||||
keyboards[value] = None
|
||||
self._keyboard = value
|
||||
self.focus = focus
|
||||
|
||||
def _get_keyboard(self):
|
||||
return self._keyboard
|
||||
keyboard = AliasProperty(_get_keyboard, _set_keyboard,
|
||||
bind=('_keyboard', ))
|
||||
'''The keyboard to bind to (or bound to the widget) when focused.
|
||||
|
||||
When None, a keyboard is requested and released whenever the widget comes
|
||||
into and out of focus. If not None, it must be a keyboard, which gets
|
||||
bound and unbound from the widget whenever it's in or out of focus. It is
|
||||
useful only when more than one keyboard is available, so it is recommended
|
||||
to be set to None when only one keyboard is available.
|
||||
|
||||
If more than one keyboard is available, whenever an instance gets focused
|
||||
a new keyboard will be requested if None. Unless the other instances lose
|
||||
focus (e.g. if tab was used), a new keyboard will appear. When this is
|
||||
undesired, the keyboard property can be used. For example, if there are
|
||||
two users with two keyboards, then each keyboard can be assigned to
|
||||
different groups of instances of FocusBehavior, ensuring that within
|
||||
each group, only one FocusBehavior will have focus, and will receive input
|
||||
from the correct keyboard. See `keyboard_mode` in :mod:`~kivy.config` for
|
||||
more information on the keyboard modes.
|
||||
|
||||
**Keyboard and focus behavior**
|
||||
|
||||
When using the keyboard, there are some important default behaviors you
|
||||
should keep in mind.
|
||||
|
||||
* When Config's `keyboard_mode` is multi, each new touch is considered
|
||||
a touch by a different user and will set the focus (if clicked on a
|
||||
focusable) with a new keyboard. Already focused elements will not lose
|
||||
their focus (even if an unfocusable widget is touched).
|
||||
|
||||
* If the keyboard property is set, that keyboard will be used when the
|
||||
instance gets focused. If widgets with different keyboards are linked
|
||||
through :attr:`focus_next` and :attr:`focus_previous`, then as they are
|
||||
tabbed through, different keyboards will become active. Therefore,
|
||||
typically it's undesirable to link instances which are assigned
|
||||
different keyboards.
|
||||
|
||||
* When a widget has focus, setting its keyboard to None will remove its
|
||||
keyboard, but the widget will then immediately try to get
|
||||
another keyboard. In order to remove its keyboard, rather set its
|
||||
:attr:`focus` to False.
|
||||
|
||||
* When using a software keyboard, typical on mobile and touch devices, the
|
||||
keyboard display behavior is determined by the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
|
||||
this property to ensure the focused widget is not covered or obscured.
|
||||
|
||||
:attr:`keyboard` is an :class:`~kivy.properties.AliasProperty` and defaults
|
||||
to None.
|
||||
|
||||
.. warning:
|
||||
|
||||
When assigning a keyboard, the keyboard must not be released while
|
||||
it is still assigned to an instance. Similarly, the keyboard created
|
||||
by the instance on focus and assigned to :attr:`keyboard` if None,
|
||||
will be released by the instance when the instance loses focus.
|
||||
Therefore, it is not safe to assign this keyboard to another instance's
|
||||
:attr:`keyboard`.
|
||||
'''
|
||||
|
||||
is_focusable = BooleanProperty(_is_desktop)
|
||||
'''Whether the instance can become focused. If focused, it'll lose focus
|
||||
when set to False.
|
||||
|
||||
:attr:`is_focusable` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True on a desktop (i.e. `desktop` is True in
|
||||
:mod:`~kivy.config`), False otherwise.
|
||||
'''
|
||||
|
||||
focus = BooleanProperty(False)
|
||||
'''Whether the instance currently has focus.
|
||||
|
||||
Setting it to True will bind to and/or request the keyboard, and input
|
||||
will be forwarded to the instance. Setting it to False will unbind
|
||||
and/or release the keyboard. For a given keyboard, only one widget can
|
||||
have its focus, so focusing one will automatically unfocus the other
|
||||
instance holding its focus.
|
||||
|
||||
When using a software keyboard, please refer to the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property to determine
|
||||
how the keyboard display is handled.
|
||||
|
||||
:attr:`focus` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
focused = focus
|
||||
'''An alias of :attr:`focus`.
|
||||
|
||||
:attr:`focused` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
|
||||
.. warning::
|
||||
:attr:`focused` is an alias of :attr:`focus` and will be removed in
|
||||
2.0.0.
|
||||
'''
|
||||
|
||||
keyboard_suggestions = BooleanProperty(True)
|
||||
'''If True provides auto suggestions on top of keyboard.
|
||||
This will only work if :attr:`input_type` is set to `text`, `url`, `mail` or
|
||||
`address`.
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
:attr:`keyboard_suggestions` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True
|
||||
'''
|
||||
|
||||
def _set_on_focus_next(self, instance, value):
|
||||
''' If changing code, ensure following code is not infinite loop:
|
||||
widget.focus_next = widget
|
||||
widget.focus_previous = widget
|
||||
widget.focus_previous = widget2
|
||||
'''
|
||||
next = self._old_focus_next
|
||||
if next is value: # prevent infinite loop
|
||||
return
|
||||
|
||||
if isinstance(next, FocusBehavior):
|
||||
next.focus_previous = None
|
||||
self._old_focus_next = value
|
||||
if value is None or value is StopIteration:
|
||||
return
|
||||
if not isinstance(value, FocusBehavior):
|
||||
raise ValueError('focus_next accepts only objects based on'
|
||||
' FocusBehavior, or the `StopIteration` class.')
|
||||
value.focus_previous = self
|
||||
|
||||
focus_next = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`FocusBehavior` instance to acquire focus when
|
||||
tab is pressed and this instance has focus, if not `None` or
|
||||
`StopIteration`.
|
||||
|
||||
When tab is pressed, focus cycles through all the :class:`FocusBehavior`
|
||||
widgets that are linked through :attr:`focus_next` and are focusable. If
|
||||
:attr:`focus_next` is `None`, it instead walks the children lists to find
|
||||
the next focusable widget. Finally, if :attr:`focus_next` is
|
||||
the `StopIteration` class, focus won't move forward, but end here.
|
||||
|
||||
.. note:
|
||||
|
||||
Setting :attr:`focus_next` automatically sets :attr:`focus_previous`
|
||||
of the other instance to point to this instance, if not None or
|
||||
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
|
||||
also sets the :attr:`focus_previous` property of the instance
|
||||
previously in :attr:`focus_next` to `None`. Therefore, it is only
|
||||
required to set one of the :attr:`focus_previous` or
|
||||
:attr:`focus_next` links since the other side will be set
|
||||
automatically.
|
||||
|
||||
:attr:`focus_next` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
def _set_on_focus_previous(self, instance, value):
|
||||
prev = self._old_focus_previous
|
||||
if prev is value:
|
||||
return
|
||||
|
||||
if isinstance(prev, FocusBehavior):
|
||||
prev.focus_next = None
|
||||
self._old_focus_previous = value
|
||||
if value is None or value is StopIteration:
|
||||
return
|
||||
if not isinstance(value, FocusBehavior):
|
||||
raise ValueError('focus_previous accepts only objects based'
|
||||
'on FocusBehavior, or the `StopIteration` class.')
|
||||
value.focus_next = self
|
||||
|
||||
focus_previous = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`FocusBehavior` instance to acquire focus when
|
||||
shift+tab is pressed on this instance, if not None or `StopIteration`.
|
||||
|
||||
When shift+tab is pressed, focus cycles through all the
|
||||
:class:`FocusBehavior` widgets that are linked through
|
||||
:attr:`focus_previous` and are focusable. If :attr:`focus_previous` is
|
||||
`None`, it instead walks the children tree to find the
|
||||
previous focusable widget. Finally, if :attr:`focus_previous` is the
|
||||
`StopIteration` class, focus won't move backward, but end here.
|
||||
|
||||
.. note:
|
||||
|
||||
Setting :attr:`focus_previous` automatically sets :attr:`focus_next`
|
||||
of the other instance to point to this instance, if not None or
|
||||
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
|
||||
also sets the :attr:`focus_next` property of the instance previously in
|
||||
:attr:`focus_previous` to `None`. Therefore, it is only required
|
||||
to set one of the :attr:`focus_previous` or :attr:`focus_next`
|
||||
links since the other side will be set automatically.
|
||||
|
||||
:attr:`focus_previous` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
keyboard_mode = OptionProperty('auto', options=('auto', 'managed'))
|
||||
'''Determines how the keyboard visibility should be managed. 'auto' will
|
||||
result in the standard behaviour of showing/hiding on focus. 'managed'
|
||||
requires setting the keyboard visibility manually, or calling the helper
|
||||
functions :meth:`show_keyboard` and :meth:`hide_keyboard`.
|
||||
|
||||
:attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and
|
||||
defaults to 'auto'. Can be one of 'auto' or 'managed'.
|
||||
'''
|
||||
|
||||
input_type = OptionProperty('null', options=('null', 'text', 'number',
|
||||
'url', 'mail', 'datetime',
|
||||
'tel', 'address'))
|
||||
'''The kind of input keyboard to request.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. versionchanged:: 2.1.0
|
||||
Changed default value from `text` to `null`. Added `null` to options.
|
||||
|
||||
.. warning::
|
||||
As the default value has been changed, you may need to adjust
|
||||
`input_type` in your code.
|
||||
|
||||
:attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and
|
||||
defaults to 'null'. Can be one of 'null', 'text', 'number', 'url', 'mail',
|
||||
'datetime', 'tel' or 'address'.
|
||||
'''
|
||||
|
||||
unfocus_on_touch = BooleanProperty(_keyboard_mode not in
|
||||
('multi', 'systemandmulti'))
|
||||
'''Whether a instance should lose focus when clicked outside the instance.
|
||||
|
||||
When a user clicks on a widget that is focus aware and shares the same
|
||||
keyboard as this widget (which in the case of only one keyboard, are
|
||||
all focus aware widgets), then as the other widgets gains focus, this
|
||||
widget loses focus. In addition to that, if this property is `True`,
|
||||
clicking on any widget other than this widget, will remove focus form this
|
||||
widget.
|
||||
|
||||
:attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False` if the `keyboard_mode` in :attr:`~kivy.config.Config`
|
||||
is `'multi'` or `'systemandmulti'`, otherwise it defaults to `True`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._old_focus_next = None
|
||||
self._old_focus_previous = None
|
||||
super(FocusBehavior, self).__init__(**kwargs)
|
||||
|
||||
self._keyboard_mode = _keyboard_mode
|
||||
fbind = self.fbind
|
||||
fbind('focus', self._on_focus)
|
||||
fbind('disabled', self._on_focusable)
|
||||
fbind('is_focusable', self._on_focusable)
|
||||
fbind('focus_next', self._set_on_focus_next)
|
||||
fbind('focus_previous', self._set_on_focus_previous)
|
||||
|
||||
def _on_focusable(self, instance, value):
|
||||
if self.disabled or not self.is_focusable:
|
||||
self.focus = False
|
||||
|
||||
def _on_focus(self, instance, value, *largs):
|
||||
if self.keyboard_mode == 'auto':
|
||||
if value:
|
||||
self._bind_keyboard()
|
||||
else:
|
||||
self._unbind_keyboard()
|
||||
|
||||
def _ensure_keyboard(self):
|
||||
if self._keyboard is None:
|
||||
self._requested_keyboard = True
|
||||
keyboard = self._keyboard = EventLoop.window.request_keyboard(
|
||||
self._keyboard_released,
|
||||
self,
|
||||
input_type=self.input_type,
|
||||
keyboard_suggestions=self.keyboard_suggestions,
|
||||
)
|
||||
keyboards = FocusBehavior._keyboards
|
||||
if keyboard not in keyboards:
|
||||
keyboards[keyboard] = None
|
||||
|
||||
def _bind_keyboard(self):
|
||||
self._ensure_keyboard()
|
||||
keyboard = self._keyboard
|
||||
|
||||
if not keyboard or self.disabled or not self.is_focusable:
|
||||
self.focus = False
|
||||
return
|
||||
keyboards = FocusBehavior._keyboards
|
||||
old_focus = keyboards[keyboard] # keyboard should be in dict
|
||||
if old_focus:
|
||||
old_focus.focus = False
|
||||
# keyboard shouldn't have been released here, see keyboard warning
|
||||
keyboards[keyboard] = self
|
||||
keyboard.bind(on_key_down=self.keyboard_on_key_down,
|
||||
on_key_up=self.keyboard_on_key_up,
|
||||
on_textinput=self.keyboard_on_textinput)
|
||||
|
||||
def _unbind_keyboard(self):
|
||||
keyboard = self._keyboard
|
||||
if keyboard:
|
||||
keyboard.unbind(on_key_down=self.keyboard_on_key_down,
|
||||
on_key_up=self.keyboard_on_key_up,
|
||||
on_textinput=self.keyboard_on_textinput)
|
||||
if self._requested_keyboard:
|
||||
keyboard.release()
|
||||
self._keyboard = None
|
||||
self._requested_keyboard = False
|
||||
del FocusBehavior._keyboards[keyboard]
|
||||
else:
|
||||
FocusBehavior._keyboards[keyboard] = None
|
||||
|
||||
def keyboard_on_textinput(self, window, text):
|
||||
pass
|
||||
|
||||
def _keyboard_released(self):
|
||||
self.focus = False
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
if (not self.disabled and self.is_focusable and
|
||||
('button' not in touch.profile or
|
||||
not touch.button.startswith('scroll'))):
|
||||
self.focus = True
|
||||
FocusBehavior.ignored_touch.append(touch)
|
||||
return super(FocusBehavior, self).on_touch_down(touch)
|
||||
|
||||
@staticmethod
|
||||
def _handle_post_on_touch_up(touch):
|
||||
''' Called by window after each touch has finished.
|
||||
'''
|
||||
touches = FocusBehavior.ignored_touch
|
||||
if touch in touches:
|
||||
touches.remove(touch)
|
||||
return
|
||||
if 'button' in touch.profile and touch.button in\
|
||||
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
|
||||
return
|
||||
for focusable in list(FocusBehavior._keyboards.values()):
|
||||
if focusable is None or not focusable.unfocus_on_touch:
|
||||
continue
|
||||
focusable.focus = False
|
||||
|
||||
def _get_focus_next(self, focus_dir):
|
||||
current = self
|
||||
walk_tree = 'walk' if focus_dir == 'focus_next' else 'walk_reverse'
|
||||
|
||||
while 1:
|
||||
# if we hit a focusable, walk through focus_xxx
|
||||
while getattr(current, focus_dir) is not None:
|
||||
current = getattr(current, focus_dir)
|
||||
if current is self or current is StopIteration:
|
||||
return None # make sure we don't loop forever
|
||||
if current.is_focusable and not current.disabled:
|
||||
return current
|
||||
|
||||
# hit unfocusable, walk widget tree
|
||||
itr = getattr(current, walk_tree)(loopback=True)
|
||||
if focus_dir == 'focus_next':
|
||||
next(itr) # current is returned first when walking forward
|
||||
for current in itr:
|
||||
if isinstance(current, FocusBehavior):
|
||||
break
|
||||
# why did we stop
|
||||
if isinstance(current, FocusBehavior):
|
||||
if current is self:
|
||||
return None
|
||||
if current.is_focusable and not current.disabled:
|
||||
return current
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_focus_next(self):
|
||||
'''Returns the next focusable widget using either :attr:`focus_next`
|
||||
or the :attr:`children` similar to the order when tabbing forwards
|
||||
with the ``tab`` key.
|
||||
'''
|
||||
return self._get_focus_next('focus_next')
|
||||
|
||||
def get_focus_previous(self):
|
||||
'''Returns the previous focusable widget using either
|
||||
:attr:`focus_previous` or the :attr:`children` similar to the
|
||||
order when ``tab`` + ``shift`` key are triggered together.
|
||||
'''
|
||||
return self._get_focus_next('focus_previous')
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
'''The method bound to the keyboard when the instance has focus.
|
||||
|
||||
When the instance becomes focused, this method is bound to the
|
||||
keyboard and will be called for every input press. The parameters are
|
||||
the same as :meth:`kivy.core.window.WindowBase.on_key_down`.
|
||||
|
||||
When overwriting the method in the derived widget, super should be
|
||||
called to enable tab cycling. If the derived widget wishes to use tab
|
||||
for its own purposes, it can call super after it has processed the
|
||||
character (if it does not wish to consume the tab).
|
||||
|
||||
Similar to other keyboard functions, it should return True if the
|
||||
key was consumed.
|
||||
'''
|
||||
if keycode[1] == 'tab': # deal with cycle
|
||||
modifiers = set(modifiers)
|
||||
if {'ctrl', 'alt', 'meta', 'super', 'compose'} & modifiers:
|
||||
return False
|
||||
if 'shift' in modifiers:
|
||||
next = self.get_focus_previous()
|
||||
else:
|
||||
next = self.get_focus_next()
|
||||
if next:
|
||||
self.focus = False
|
||||
|
||||
next.focus = True
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
'''The method bound to the keyboard when the instance has focus.
|
||||
|
||||
When the instance becomes focused, this method is bound to the
|
||||
keyboard and will be called for every input release. The parameters are
|
||||
the same as :meth:`kivy.core.window.WindowBase.on_key_up`.
|
||||
|
||||
When overwriting the method in the derived widget, super should be
|
||||
called to enable de-focusing on escape. If the derived widget wishes
|
||||
to use escape for its own purposes, it can call super after it has
|
||||
processed the character (if it does not wish to consume the escape).
|
||||
|
||||
See :meth:`keyboard_on_key_down`
|
||||
'''
|
||||
if keycode[1] == 'escape':
|
||||
self.focus = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_keyboard(self):
|
||||
'''
|
||||
Convenience function to show the keyboard in managed mode.
|
||||
'''
|
||||
if self.keyboard_mode == 'managed':
|
||||
self._bind_keyboard()
|
||||
|
||||
def hide_keyboard(self):
|
||||
'''
|
||||
Convenience function to hide the keyboard in managed mode.
|
||||
'''
|
||||
if self.keyboard_mode == 'managed':
|
||||
self._unbind_keyboard()
|
||||
590
kivy/uix/behaviors/knspace.py
Normal file
590
kivy/uix/behaviors/knspace.py
Normal file
@@ -0,0 +1,590 @@
|
||||
'''
|
||||
Kivy Namespaces
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
The :class:`KNSpaceBehavior` `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
class provides namespace functionality for Kivy objects. It allows kivy objects
|
||||
to be named and then accessed using namespaces.
|
||||
|
||||
:class:`KNSpace` instances are the namespaces that store the named objects
|
||||
in Kivy :class:`~kivy.properties.ObjectProperty` instances.
|
||||
In addition, when inheriting from :class:`KNSpaceBehavior`, if the derived
|
||||
object is named, the name will automatically be added to the associated
|
||||
namespace and will point to a :attr:`~kivy.uix.widget.proxy_ref` of the
|
||||
derived object.
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
|
||||
By default, there's only a single namespace: the :attr:`knspace` namespace. The
|
||||
simplest example is adding a widget to the namespace:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.uix.behaviors.knspace import knspace
|
||||
widget = Widget()
|
||||
knspace.my_widget = widget
|
||||
|
||||
This adds a kivy :class:`~kivy.properties.ObjectProperty` with `rebind=True`
|
||||
and `allownone=True` to the :attr:`knspace` namespace with a property name
|
||||
`my_widget`. And the property now also points to this widget.
|
||||
|
||||
This can be done automatically with:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knsname: 'my_widget'
|
||||
|
||||
Now, `knspace.my_widget` will point to that widget.
|
||||
|
||||
When one creates a second widget with the same name, the namespace will
|
||||
also change to point to the new widget. E.g.:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget
|
||||
widget2 = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget2
|
||||
|
||||
Setting the namespace
|
||||
---------------------
|
||||
|
||||
One can also create ones own namespace rather than using the default
|
||||
:attr:`knspace` by directly setting :attr:`KNSpaceBehavior.knspace`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
my_new_namespace = KNSpace()
|
||||
widget.knspace = my_new_namespace
|
||||
|
||||
Initially, `my_widget` is added to the default namespace, but when the widget's
|
||||
namespace is changed to `my_new_namespace`, the reference to `my_widget` is
|
||||
moved to that namespace. We could have also of course first set the namespace
|
||||
to `my_new_namespace` and then have named the widget `my_widget`, thereby
|
||||
avoiding the initial assignment to the default namespace.
|
||||
|
||||
Similarly, in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knspace: KNSpace()
|
||||
knsname: 'my_widget'
|
||||
|
||||
Inheriting the namespace
|
||||
------------------------
|
||||
|
||||
In the previous example, we directly set the namespace we wished to use.
|
||||
In the following example, we inherit it from the parent, so we only have to set
|
||||
it once:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
<MyLabel@KNSpaceBehavior+Label>
|
||||
|
||||
<MyComplexWidget@MyWidget>:
|
||||
knsname: 'my_complex'
|
||||
MyLabel:
|
||||
knsname: 'label1'
|
||||
MyLabel:
|
||||
knsname: 'label2'
|
||||
|
||||
Then, we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
widget.knspace = new_knspace
|
||||
|
||||
The rule is that if no knspace has been assigned to a widget, it looks for a
|
||||
namespace in its parent and parent's parent and so on until it find one to
|
||||
use. If none are found, it uses the default :attr:`knspace`.
|
||||
|
||||
When `MyComplexWidget` is created, it still used the default namespace.
|
||||
However, when we assigned the root widget its new namespace, all its
|
||||
children switched to using that new namespace as well. So `new_knspace` now
|
||||
contains `label1` and `label2` as well as `my_complex`.
|
||||
|
||||
If we had first done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
knspace.label1.knspace = knspace
|
||||
widget.knspace = new_knspace
|
||||
|
||||
Then `label1` would remain stored in the default :attr:`knspace` since it was
|
||||
directly set, but `label2` and `my_complex` would still be added to the new
|
||||
namespace.
|
||||
|
||||
One can customize the attribute used to search the parent tree by changing
|
||||
:attr:`KNSpaceBehavior.knspace_key`. If the desired knspace is not reachable
|
||||
through a widgets parent tree, e.g. in a popup that is not a widget's child,
|
||||
:attr:`KNSpaceBehavior.knspace_key` can be used to establish a different
|
||||
search order.
|
||||
|
||||
Accessing the namespace
|
||||
-----------------------
|
||||
|
||||
As seen in the previous example, if not directly assigned, the namespace is
|
||||
found by searching the parent tree. Consequently, if a namespace was assigned
|
||||
further up the parent tree, all its children and below could access that
|
||||
namespace through their :attr:`KNSpaceBehavior.knspace` property.
|
||||
|
||||
This allows the creation of multiple widgets with identically given names
|
||||
if each root widget instance is assigned a new namespace. For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Now, when we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
knspace1, knspace2 = KNSpace(), KNSpace()
|
||||
composite1 = MyCompositeWidget()
|
||||
composite1.knspace = knspace1
|
||||
|
||||
composite2 = MyCompositeWidget()
|
||||
composite2.knspace = knspace2
|
||||
|
||||
knspace1.pretty = "Here's the ladder, now fix the roof!"
|
||||
knspace2.pretty = "Get that raccoon off me!"
|
||||
|
||||
Because each of the `MyCompositeWidget` instances have a different namespace
|
||||
their children also use different namespaces. Consequently, the
|
||||
pretty and complex widgets of each instance will have different text.
|
||||
|
||||
Further, because both the namespace :class:`~kivy.properties.ObjectProperty`
|
||||
references, and :attr:`KNSpaceBehavior.knspace` have `rebind=True`, the
|
||||
text of the `MyComplexWidget` label is rebound to match the text of
|
||||
`MyPrettyWidget` when either the root's namespace changes or when the
|
||||
`root.knspace.pretty` property changes, as expected.
|
||||
|
||||
Forking a namespace
|
||||
-------------------
|
||||
|
||||
Forking a namespace provides the opportunity to create a new namespace
|
||||
from a parent namespace so that the forked namespace will contain everything
|
||||
in the origin namespace, but the origin namespace will not have access to
|
||||
anything added to the forked namespace.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
child = knspace.fork()
|
||||
grandchild = child.fork()
|
||||
|
||||
child.label = Label()
|
||||
grandchild.button = Button()
|
||||
|
||||
Now label is accessible by both child and grandchild, but not by knspace. And
|
||||
button is only accessible by the grandchild but not by the child or by knspace.
|
||||
Finally, doing `grandchild.label = Label()` will leave `grandchild.label`
|
||||
and `child.label` pointing to different labels.
|
||||
|
||||
A motivating example is the example from above:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
knspace: 'fork'
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Notice the addition of `knspace: 'fork'`. This is identical to doing
|
||||
`knspace: self.knspace.fork()`. However, doing that would lead to infinite
|
||||
recursion as that kv rule would be executed recursively because `self.knspace`
|
||||
will keep on changing. However, allowing `knspace: 'fork'` cirumvents that.
|
||||
See :attr:`KNSpaceBehavior.knspace`.
|
||||
|
||||
Now, having forked, we just need to do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
composite1 = MyCompositeWidget()
|
||||
composite2 = MyCompositeWidget()
|
||||
|
||||
composite1.knspace.pretty = "Here's the ladder, now fix the roof!"
|
||||
composite2.knspace.pretty = "Get that raccoon off me!"
|
||||
|
||||
Since by forking we automatically created a unique namespace for each
|
||||
`MyCompositeWidget` instance.
|
||||
'''
|
||||
|
||||
__all__ = ('KNSpace', 'KNSpaceBehavior', 'knspace')
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.properties import StringProperty, ObjectProperty, AliasProperty
|
||||
from kivy.context import register_context
|
||||
|
||||
|
||||
class KNSpace(EventDispatcher):
|
||||
'''Each :class:`KNSpace` instance is a namespace that stores the named Kivy
|
||||
objects associated with this namespace. Each named object is
|
||||
stored as the value of a Kivy :class:`~kivy.properties.ObjectProperty` of
|
||||
this instance whose property name is the object's given name. Both `rebind`
|
||||
and `allownone` are set to `True` for the property.
|
||||
|
||||
See :attr:`KNSpaceBehavior.knspace` for details on how a namespace is
|
||||
associated with a named object.
|
||||
|
||||
When storing an object in the namespace, the object's `proxy_ref` is
|
||||
stored if the object has such an attribute.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`parent`: (internal) A :class:`KNSpace` instance or None.
|
||||
If specified, it's a parent namespace, in which case, the current
|
||||
namespace will have in its namespace all its named objects
|
||||
as well as the named objects of its parent and parent's parent
|
||||
etc. See :meth:`fork` for more details.
|
||||
'''
|
||||
|
||||
parent = None
|
||||
'''(internal) The parent namespace instance, :class:`KNSpace`, or None. See
|
||||
:meth:`fork`.
|
||||
'''
|
||||
__has_applied = None
|
||||
|
||||
keep_ref = False
|
||||
'''Whether a direct reference should be kept to the stored objects.
|
||||
If ``True``, we use the direct object, otherwise we use
|
||||
:attr:`~kivy.uix.widget.proxy_ref` when present.
|
||||
|
||||
Defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, parent=None, keep_ref=False, **kwargs):
|
||||
self.keep_ref = keep_ref
|
||||
super(KNSpace, self).__init__(**kwargs)
|
||||
self.parent = parent
|
||||
self.__has_applied = set(self.properties().keys())
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
has_applied = self.__has_applied
|
||||
if prop is None:
|
||||
if hasattr(self, name):
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
self.apply_property(
|
||||
**{name:
|
||||
ObjectProperty(None, rebind=True, allownone=True)}
|
||||
)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
has_applied.add(name)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
elif name not in has_applied:
|
||||
self.apply_property(**{name: prop})
|
||||
has_applied.add(name)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in super(KNSpace, self).__getattribute__('__dict__'):
|
||||
return super(KNSpace, self).__getattribute__(name)
|
||||
|
||||
try:
|
||||
value = super(KNSpace, self).__getattribute__(name)
|
||||
except AttributeError:
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
raise AttributeError(name)
|
||||
return getattr(parent, name)
|
||||
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return getattr(parent, name) # if parent doesn't have it
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def property(self, name, quiet=False):
|
||||
# needs to overwrite EventDispatcher.property so kv lang will work
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
if prop is not None:
|
||||
return prop
|
||||
|
||||
prop = ObjectProperty(None, rebind=True, allownone=True)
|
||||
self.apply_property(**{name: prop})
|
||||
self.__has_applied.add(name)
|
||||
return prop
|
||||
|
||||
def fork(self):
|
||||
'''Returns a new :class:`KNSpace` instance which will have access to
|
||||
all the named objects in the current namespace but will also have a
|
||||
namespace of its own that is unique to it.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
forked_knspace1 = knspace.fork()
|
||||
forked_knspace2 = knspace.fork()
|
||||
|
||||
Now, any names added to `knspace` will be accessible by the
|
||||
`forked_knspace1` and `forked_knspace2` namespaces by the normal means.
|
||||
However, any names added to `forked_knspace1` will not be accessible
|
||||
from `knspace` or `forked_knspace2`. Similar for `forked_knspace2`.
|
||||
'''
|
||||
return KNSpace(parent=self)
|
||||
|
||||
|
||||
class KNSpaceBehavior(object):
|
||||
'''Inheriting from this class allows naming of the inherited objects, which
|
||||
are then added to the associated namespace :attr:`knspace` and accessible
|
||||
through it.
|
||||
|
||||
Please see the :mod:`knspace behaviors module <kivy.uix.behaviors.knspace>`
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
_knspace = ObjectProperty(None, allownone=True)
|
||||
_knsname = StringProperty('')
|
||||
__last_knspace = None
|
||||
__callbacks = None
|
||||
|
||||
def __init__(self, knspace=None, **kwargs):
|
||||
self.knspace = knspace
|
||||
super(KNSpaceBehavior, self).__init__(**kwargs)
|
||||
|
||||
def __knspace_clear_callbacks(self, *largs):
|
||||
for obj, name, uid in self.__callbacks:
|
||||
obj.unbind_uid(name, uid)
|
||||
last = self.__last_knspace
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
assert self._knspace is None
|
||||
assert last
|
||||
|
||||
new = self.__set_parent_knspace()
|
||||
if new is last:
|
||||
return
|
||||
self.property('_knspace').dispatch(self)
|
||||
|
||||
name = self.knsname
|
||||
if not name:
|
||||
return
|
||||
|
||||
if getattr(last, name) == self:
|
||||
setattr(last, name, None)
|
||||
|
||||
if new:
|
||||
setattr(new, name, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
|
||||
def __set_parent_knspace(self):
|
||||
callbacks = self.__callbacks = []
|
||||
fbind = self.fbind
|
||||
append = callbacks.append
|
||||
parent_key = self.knspace_key
|
||||
clear = self.__knspace_clear_callbacks
|
||||
|
||||
append((self, 'knspace_key', fbind('knspace_key', clear)))
|
||||
if not parent_key:
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
append((self, parent_key, fbind(parent_key, clear)))
|
||||
parent = getattr(self, parent_key, None)
|
||||
while parent is not None:
|
||||
fbind = parent.fbind
|
||||
|
||||
parent_knspace = getattr(parent, 'knspace', 0)
|
||||
if parent_knspace != 0:
|
||||
append((parent, 'knspace', fbind('knspace', clear)))
|
||||
self.__last_knspace = parent_knspace
|
||||
return parent_knspace
|
||||
|
||||
append((parent, parent_key, fbind(parent_key, clear)))
|
||||
new_parent = getattr(parent, parent_key, None)
|
||||
if new_parent is parent:
|
||||
break
|
||||
parent = new_parent
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
def _get_knspace(self):
|
||||
_knspace = self._knspace
|
||||
if _knspace is not None:
|
||||
return _knspace
|
||||
|
||||
if self.__callbacks is not None:
|
||||
return self.__last_knspace
|
||||
|
||||
# we only get here if we never accessed our knspace
|
||||
return self.__set_parent_knspace()
|
||||
|
||||
def _set_knspace(self, value):
|
||||
if value is self._knspace:
|
||||
return
|
||||
|
||||
knspace = self._knspace or self.__last_knspace
|
||||
name = self.knsname
|
||||
if name and knspace and getattr(knspace, name) == self:
|
||||
setattr(knspace, name, None) # reset old namespace
|
||||
|
||||
if value == 'fork':
|
||||
if not knspace:
|
||||
knspace = self.knspace # get parents in case we haven't before
|
||||
if knspace:
|
||||
value = knspace.fork()
|
||||
else:
|
||||
raise ValueError('Cannot fork with no namespace')
|
||||
|
||||
for obj, prop_name, uid in self.__callbacks or []:
|
||||
obj.unbind_uid(prop_name, uid)
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
if name:
|
||||
if value is None: # if None, first update the recursive knspace
|
||||
knspace = self.__set_parent_knspace()
|
||||
if knspace:
|
||||
setattr(knspace, name, self)
|
||||
self._knspace = None # cause a kv trigger
|
||||
else:
|
||||
setattr(value, name, self)
|
||||
knspace = self._knspace = value
|
||||
|
||||
if not knspace:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
else:
|
||||
if value is None:
|
||||
self.__set_parent_knspace() # update before trigger below
|
||||
self._knspace = value
|
||||
|
||||
knspace = AliasProperty(
|
||||
_get_knspace, _set_knspace, bind=('_knspace', ), cache=False,
|
||||
rebind=True, allownone=True)
|
||||
'''The namespace instance, :class:`KNSpace`, associated with this widget.
|
||||
The :attr:`knspace` namespace stores this widget when naming this widget
|
||||
with :attr:`knsname`.
|
||||
|
||||
If the namespace has been set with a :class:`KNSpace` instance, e.g. with
|
||||
`self.knspace = KNSpace()`, then that instance is returned (setting with
|
||||
`None` doesn't count). Otherwise, if :attr:`knspace_key` is not None, we
|
||||
look for a namespace to use in the object that is stored in the property
|
||||
named :attr:`knspace_key`, of this instance. I.e.
|
||||
`object = getattr(self, self.knspace_key)`.
|
||||
|
||||
If that object has a knspace property, then we return its value. Otherwise,
|
||||
we go further up, e.g. with `getattr(object, self.knspace_key)` and look
|
||||
for its `knspace` property.
|
||||
|
||||
Finally, if we reach a value of `None`, or :attr:`knspace_key` was `None`,
|
||||
the default :attr:`~kivy.uix.behaviors.knspace.knspace` namespace is
|
||||
returned.
|
||||
|
||||
If :attr:`knspace` is set to the string `'fork'`, the current namespace
|
||||
in :attr:`knspace` will be forked with :meth:`KNSpace.fork` and the
|
||||
resulting namespace will be assigned to this instance's :attr:`knspace`.
|
||||
See the module examples for a motivating example.
|
||||
|
||||
Both `rebind` and `allownone` are `True`.
|
||||
'''
|
||||
|
||||
knspace_key = StringProperty('parent', allownone=True)
|
||||
'''The name of the property of this instance, to use to search upwards for
|
||||
a namespace to use by this instance. Defaults to `'parent'` so that we'll
|
||||
search the parent tree. See :attr:`knspace`.
|
||||
|
||||
When `None`, we won't search the parent tree for the namespace.
|
||||
`allownone` is `True`.
|
||||
'''
|
||||
|
||||
def _get_knsname(self):
|
||||
return self._knsname
|
||||
|
||||
def _set_knsname(self, value):
|
||||
old_name = self._knsname
|
||||
knspace = self.knspace
|
||||
if old_name and knspace and getattr(knspace, old_name) == self:
|
||||
setattr(knspace, old_name, None)
|
||||
|
||||
self._knsname = value
|
||||
if value:
|
||||
if knspace:
|
||||
setattr(knspace, value, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(value))
|
||||
|
||||
knsname = AliasProperty(
|
||||
_get_knsname, _set_knsname, bind=('_knsname', ), cache=False)
|
||||
'''The name given to this instance. If named, the name will be added to the
|
||||
associated :attr:`knspace` namespace, which will then point to the
|
||||
`proxy_ref` of this instance.
|
||||
|
||||
When named, one can access this object by e.g. self.knspace.name, where
|
||||
`name` is the given name of this instance. See :attr:`knspace` and the
|
||||
module description for more details.
|
||||
'''
|
||||
|
||||
|
||||
knspace = register_context('knspace', KNSpace)
|
||||
'''The default :class:`KNSpace` namespace. See :attr:`KNSpaceBehavior.knspace`
|
||||
for more details.
|
||||
'''
|
||||
156
kivy/uix/behaviors/togglebutton.py
Normal file
156
kivy/uix/behaviors/togglebutton.py
Normal file
@@ -0,0 +1,156 @@
|
||||
'''
|
||||
ToggleButton Behavior
|
||||
=====================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.togglebutton.ToggleButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.togglebutton.ToggleButton` behavior. You can combine this
|
||||
class with other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative togglebuttons that preserve Kivy togglebutton behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds togglebutton behavior to an image to make a checkbox
|
||||
that behaves like a togglebutton::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ToggleButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ToggleButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_state(self, widget, value):
|
||||
if value == 'down':
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
else:
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
'''
|
||||
|
||||
__all__ = ('ToggleButtonBehavior', )
|
||||
|
||||
from kivy.properties import ObjectProperty, BooleanProperty
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from weakref import ref
|
||||
|
||||
|
||||
class ToggleButtonBehavior(ButtonBehavior):
|
||||
'''This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:mod:`~kivy.uix.togglebutton` behavior. Please see the
|
||||
:mod:`togglebutton behaviors module <kivy.uix.behaviors.togglebutton>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
__groups = {}
|
||||
|
||||
group = ObjectProperty(None, allownone=True)
|
||||
'''Group of the button. If `None`, no group will be used (the button will be
|
||||
independent). If specified, :attr:`group` must be a hashable object, like
|
||||
a string. Only one button in a group can be in a 'down' state.
|
||||
|
||||
:attr:`group` is a :class:`~kivy.properties.ObjectProperty` and defaults to
|
||||
`None`.
|
||||
'''
|
||||
|
||||
allow_no_selection = BooleanProperty(True)
|
||||
'''This specifies whether the widgets in a group allow no selection i.e.
|
||||
everything to be deselected.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`allow_no_selection` is a :class:`BooleanProperty` and defaults to
|
||||
`True`
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._previous_group = None
|
||||
super(ToggleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_group(self, *largs):
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if self._previous_group:
|
||||
group = groups[self._previous_group]
|
||||
for item in group[:]:
|
||||
if item() is self:
|
||||
group.remove(item)
|
||||
break
|
||||
group = self._previous_group = self.group
|
||||
if group not in groups:
|
||||
groups[group] = []
|
||||
r = ref(self, ToggleButtonBehavior._clear_groups)
|
||||
groups[group].append(r)
|
||||
|
||||
def _release_group(self, current):
|
||||
if self.group is None:
|
||||
return
|
||||
group = self.__groups[self.group]
|
||||
for item in group[:]:
|
||||
widget = item()
|
||||
if widget is None:
|
||||
group.remove(item)
|
||||
if widget is current:
|
||||
continue
|
||||
widget.state = 'normal'
|
||||
|
||||
def _do_press(self):
|
||||
if (not self.allow_no_selection and
|
||||
self.group and self.state == 'down'):
|
||||
return
|
||||
|
||||
self._release_group(self)
|
||||
self.state = 'normal' if self.state == 'down' else 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _clear_groups(wk):
|
||||
# auto flush the element when the weak reference have been deleted
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
for group in list(groups.values()):
|
||||
if wk in group:
|
||||
group.remove(wk)
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def get_widgets(groupname):
|
||||
'''Return a list of the widgets contained in a specific group. If the
|
||||
group doesn't exist, an empty list will be returned.
|
||||
|
||||
.. note::
|
||||
|
||||
Always release the result of this method! Holding a reference to
|
||||
any of these widgets can prevent them from being garbage collected.
|
||||
If in doubt, do::
|
||||
|
||||
l = ToggleButtonBehavior.get_widgets('mygroup')
|
||||
# do your job
|
||||
del l
|
||||
|
||||
.. warning::
|
||||
|
||||
It's possible that some widgets that you have previously
|
||||
deleted are still in the list. The garbage collector might need
|
||||
to release other objects before flushing them.
|
||||
'''
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if groupname not in groups:
|
||||
return []
|
||||
return [x() for x in groups[groupname] if x()][:]
|
||||
318
kivy/uix/behaviors/touchripple.py
Normal file
318
kivy/uix/behaviors/touchripple.py
Normal file
@@ -0,0 +1,318 @@
|
||||
'''
|
||||
Touch Ripple
|
||||
============
|
||||
|
||||
.. versionadded:: 1.10.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
This module contains `mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes
|
||||
to add a touch ripple visual effect known from `Google Material Design
|
||||
<https://en.wikipedia.org/wiki/Material_Design>_` to widgets.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleBehavior` provides
|
||||
rendering the ripple animation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleButtonBehavior`
|
||||
basically provides the same functionality as
|
||||
:class:`~kivy.uix.behaviors.button.ButtonBehavior` but rendering the ripple
|
||||
animation instead of default press/release visualization.
|
||||
'''
|
||||
from kivy.animation import Animation
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import CanvasBase, Color, Ellipse, ScissorPush, ScissorPop
|
||||
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, \
|
||||
ObjectProperty, StringProperty
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TouchRippleBehavior',
|
||||
'TouchRippleButtonBehavior'
|
||||
)
|
||||
|
||||
|
||||
class TouchRippleBehavior(object):
|
||||
'''Touch ripple behavior.
|
||||
|
||||
Supposed to be used as mixin on widget classes.
|
||||
|
||||
Ripple behavior does not trigger automatically, concrete implementation
|
||||
needs to call :func:`ripple_show` respective :func:`ripple_fade` manually.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here we create a Label which renders the touch ripple animation on
|
||||
interaction::
|
||||
|
||||
class RippleLabel(TouchRippleBehavior, Label):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RippleLabel, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
collide_point = self.collide_point(touch.x, touch.y)
|
||||
if collide_point:
|
||||
touch.grab(self)
|
||||
self.ripple_show(touch)
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is self:
|
||||
touch.ungrab(self)
|
||||
self.ripple_fade()
|
||||
return True
|
||||
return False
|
||||
'''
|
||||
|
||||
ripple_rad_default = NumericProperty(10)
|
||||
'''Default radius the animation starts from.
|
||||
|
||||
:attr:`ripple_rad_default` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `10`.
|
||||
'''
|
||||
|
||||
ripple_duration_in = NumericProperty(.5)
|
||||
'''Animation duration taken to show the overlay.
|
||||
|
||||
:attr:`ripple_duration_in` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_duration_out = NumericProperty(.2)
|
||||
'''Animation duration taken to fade the overlay.
|
||||
|
||||
:attr:`ripple_duration_out` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.2`.
|
||||
'''
|
||||
|
||||
ripple_fade_from_alpha = NumericProperty(.5)
|
||||
'''Alpha channel for ripple color the animation starts with.
|
||||
|
||||
:attr:`ripple_fade_from_alpha` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_fade_to_alpha = NumericProperty(.8)
|
||||
'''Alpha channel for ripple color the animation targets to.
|
||||
|
||||
:attr:`ripple_fade_to_alpha` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.8`.
|
||||
'''
|
||||
|
||||
ripple_scale = NumericProperty(2.)
|
||||
'''Max scale of the animation overlay calculated from max(width/height) of
|
||||
the decorated widget.
|
||||
|
||||
:attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `2.0`.
|
||||
'''
|
||||
|
||||
ripple_func_in = StringProperty('in_cubic')
|
||||
'''Animation callback for showing the overlay.
|
||||
|
||||
:attr:`ripple_func_in` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `in_cubic`.
|
||||
'''
|
||||
|
||||
ripple_func_out = StringProperty('out_quad')
|
||||
'''Animation callback for hiding the overlay.
|
||||
|
||||
:attr:`ripple_func_out` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `out_quad`.
|
||||
'''
|
||||
|
||||
ripple_rad = NumericProperty(10)
|
||||
ripple_pos = ListProperty([0, 0])
|
||||
ripple_color = ListProperty((1., 1., 1., .5))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(TouchRippleBehavior, self).__init__(**kwargs)
|
||||
self.ripple_pane = CanvasBase()
|
||||
self.canvas.add(self.ripple_pane)
|
||||
self.bind(
|
||||
ripple_color=self._ripple_set_color,
|
||||
ripple_pos=self._ripple_set_ellipse,
|
||||
ripple_rad=self._ripple_set_ellipse
|
||||
)
|
||||
self.ripple_ellipse = None
|
||||
self.ripple_col_instruction = None
|
||||
|
||||
def ripple_show(self, touch):
|
||||
'''Begin ripple animation on current widget.
|
||||
|
||||
Expects touch event as argument.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
self._ripple_reset_pane()
|
||||
x, y = self.to_window(*self.pos)
|
||||
width, height = self.size
|
||||
if isinstance(self, RelativeLayout):
|
||||
self.ripple_pos = ripple_pos = (touch.x - x, touch.y - y)
|
||||
else:
|
||||
self.ripple_pos = ripple_pos = (touch.x, touch.y)
|
||||
rc = self.ripple_color
|
||||
ripple_rad = self.ripple_rad
|
||||
self.ripple_color = [rc[0], rc[1], rc[2], self.ripple_fade_from_alpha]
|
||||
with self.ripple_pane:
|
||||
ScissorPush(
|
||||
x=int(round(x)),
|
||||
y=int(round(y)),
|
||||
width=int(round(width)),
|
||||
height=int(round(height))
|
||||
)
|
||||
self.ripple_col_instruction = Color(rgba=self.ripple_color)
|
||||
self.ripple_ellipse = Ellipse(
|
||||
size=(ripple_rad, ripple_rad),
|
||||
pos=(
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
)
|
||||
ScissorPop()
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
t=self.ripple_func_in,
|
||||
ripple_color=[rc[0], rc[1], rc[2], self.ripple_fade_to_alpha],
|
||||
duration=self.ripple_duration_in
|
||||
)
|
||||
anim.start(self)
|
||||
|
||||
def ripple_fade(self):
|
||||
'''Finish ripple animation on current widget.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
width, height = self.size
|
||||
rc = self.ripple_color
|
||||
duration = self.ripple_duration_out
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
ripple_color=[rc[0], rc[1], rc[2], 0.],
|
||||
t=self.ripple_func_out,
|
||||
duration=duration
|
||||
)
|
||||
anim.bind(on_complete=self._ripple_anim_complete)
|
||||
anim.start(self)
|
||||
|
||||
def _ripple_set_ellipse(self, instance, value):
|
||||
ellipse = self.ripple_ellipse
|
||||
if not ellipse:
|
||||
return
|
||||
ripple_pos = self.ripple_pos
|
||||
ripple_rad = self.ripple_rad
|
||||
ellipse.size = (ripple_rad, ripple_rad)
|
||||
ellipse.pos = (
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
|
||||
def _ripple_set_color(self, instance, value):
|
||||
if not self.ripple_col_instruction:
|
||||
return
|
||||
self.ripple_col_instruction.rgba = value
|
||||
|
||||
def _ripple_anim_complete(self, anim, instance):
|
||||
self._ripple_reset_pane()
|
||||
|
||||
def _ripple_reset_pane(self):
|
||||
self.ripple_rad = self.ripple_rad_default
|
||||
self.ripple_pane.clear()
|
||||
|
||||
|
||||
class TouchRippleButtonBehavior(TouchRippleBehavior):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
a similar behavior to :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
but provides touch ripple animation instead of button pressed/released as
|
||||
visual effect.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
super(TouchRippleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.ripple_show(touch)
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(TouchRippleButtonBehavior, self).on_touch_up(touch)
|
||||
assert(self in touch.ud)
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
if self.disabled:
|
||||
return
|
||||
self.ripple_fade()
|
||||
if not self.always_release and not self.collide_point(*touch.pos):
|
||||
return
|
||||
|
||||
# defer on_release until ripple_fade has completed
|
||||
def defer_release(dt):
|
||||
self.dispatch('on_release')
|
||||
Clock.schedule_once(defer_release, self.ripple_duration_out)
|
||||
return True
|
||||
|
||||
def on_disabled(self, instance, value):
|
||||
# ensure ripple animation completes if disabled gets set to True
|
||||
if value:
|
||||
self.ripple_fade()
|
||||
return super(TouchRippleButtonBehavior, self).on_disabled(
|
||||
instance, value)
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
331
kivy/uix/boxlayout.py
Normal file
331
kivy/uix/boxlayout.py
Normal 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)
|
||||
402
kivy/uix/bubble.py
Normal file
402
kivy/uix/bubble.py
Normal file
@@ -0,0 +1,402 @@
|
||||
'''
|
||||
Bubble
|
||||
======
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
.. image:: images/bubble.jpg
|
||||
:align: right
|
||||
|
||||
The Bubble widget is a form of menu or a small popup where the menu options
|
||||
are stacked either vertically or horizontally.
|
||||
|
||||
The :class:`Bubble` contains an arrow pointing in the direction you
|
||||
choose.
|
||||
|
||||
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')
|
||||
|
||||
The widgets added to the Bubble are ordered horizontally by default, like a
|
||||
Boxlayout. You can change that by::
|
||||
|
||||
orientation = 'vertical'
|
||||
|
||||
To add items to the bubble::
|
||||
|
||||
bubble = Bubble(orientation = 'vertical')
|
||||
bubble.add_widget(your_widget_instance)
|
||||
|
||||
To remove items::
|
||||
|
||||
bubble.remove_widget(widget)
|
||||
or
|
||||
bubble.clear_widgets()
|
||||
|
||||
To access the list of children, use content.children::
|
||||
|
||||
bubble.content.children
|
||||
|
||||
.. warning::
|
||||
This is important! Do not use bubble.children
|
||||
|
||||
To change the appearance of the bubble::
|
||||
|
||||
bubble.background_color = (1, 0, 0, .5) #50% translucent red
|
||||
bubble.border = [0, 0, 0, 0]
|
||||
background_image = 'path/to/background/image'
|
||||
arrow_image = 'path/to/arrow/image'
|
||||
'''
|
||||
|
||||
__all__ = ('Bubble', 'BubbleButton', 'BubbleContent')
|
||||
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.scatter import Scatter
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.properties import ObjectProperty, StringProperty, OptionProperty, \
|
||||
ListProperty, BooleanProperty, ColorProperty
|
||||
from kivy.clock import Clock
|
||||
from kivy.base import EventLoop
|
||||
from kivy.metrics import dp
|
||||
|
||||
|
||||
class BubbleButton(Button):
|
||||
'''A button intended for use in a Bubble 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(GridLayout):
|
||||
pass
|
||||
|
||||
|
||||
class Bubble(GridLayout):
|
||||
'''Bubble class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a). To use it you have to set
|
||||
either :attr:`background_image` or :attr:`arrow_image` first.
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used 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.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16)
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/bubble')
|
||||
'''Background image of the bubble.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/bubble'.
|
||||
'''
|
||||
|
||||
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'.
|
||||
'''
|
||||
|
||||
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 relative 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'.
|
||||
'''
|
||||
|
||||
content = ObjectProperty(None)
|
||||
'''This is the object where the main content of the bubble is held.
|
||||
|
||||
:attr:`content` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to 'None'.
|
||||
'''
|
||||
|
||||
orientation = OptionProperty('horizontal',
|
||||
options=('horizontal', 'vertical'))
|
||||
'''This specifies the manner in which the children inside bubble
|
||||
are arranged. Can be one of 'vertical' or 'horizontal'.
|
||||
|
||||
:attr:`orientation` is a :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'horizontal'.
|
||||
'''
|
||||
|
||||
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'.
|
||||
'''
|
||||
|
||||
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:: 1.11.0
|
||||
|
||||
:attr:`border_auto_scale` is a
|
||||
:class:`~kivy.properties.OptionProperty` and defaults to
|
||||
'both_lower'.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._prev_arrow_pos = None
|
||||
self._arrow_layout = BoxLayout()
|
||||
self._bk_img = Image(
|
||||
source=self.background_image, allow_stretch=True,
|
||||
keep_ratio=False, color=self.background_color)
|
||||
self.background_texture = self._bk_img.texture
|
||||
self._arrow_img = Image(source=self.arrow_image,
|
||||
allow_stretch=True,
|
||||
color=self.background_color)
|
||||
self.content = content = BubbleContent(parent=self)
|
||||
super(Bubble, self).__init__(**kwargs)
|
||||
content.parent = None
|
||||
self.add_widget(content)
|
||||
self.on_arrow_pos()
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
content = self.content
|
||||
if content is None:
|
||||
return
|
||||
if widget == content or widget == self._arrow_img\
|
||||
or widget == self._arrow_layout:
|
||||
super(Bubble, self).add_widget(widget, *args, **kwargs)
|
||||
else:
|
||||
content.add_widget(widget, *args, **kwargs)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
content = self.content
|
||||
if not content:
|
||||
return
|
||||
if widget == content or widget == self._arrow_img\
|
||||
or widget == self._arrow_layout:
|
||||
super(Bubble, self).remove_widget(widget, *args, **kwargs)
|
||||
else:
|
||||
content.remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
if self.content:
|
||||
self.content.clear_widgets(*args, **kwargs)
|
||||
|
||||
def on_show_arrow(self, instance, value):
|
||||
self._arrow_img.opacity = int(value)
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
Clock.schedule_once(self._update_arrow)
|
||||
|
||||
def on_pos(self, instance, pos):
|
||||
lt = self.limit_to
|
||||
|
||||
if lt:
|
||||
self.limit_to = None
|
||||
if lt is EventLoop.window:
|
||||
x = y = 0
|
||||
top = lt.height
|
||||
right = lt.width
|
||||
else:
|
||||
x, y = lt.x, lt.y
|
||||
top, right = lt.top, lt.right
|
||||
|
||||
self.x = max(self.x, x)
|
||||
self.right = min(self.right, right)
|
||||
self.top = min(self.top, top)
|
||||
self.y = max(self.y, y)
|
||||
self.limit_to = lt
|
||||
|
||||
def on_background_image(self, *l):
|
||||
self._bk_img.source = self.background_image
|
||||
|
||||
def on_background_color(self, *l):
|
||||
if self.content is None:
|
||||
return
|
||||
self._arrow_img.color = self._bk_img.color = self.background_color
|
||||
|
||||
def on_orientation(self, *l):
|
||||
content = self.content
|
||||
if not content:
|
||||
return
|
||||
if self.orientation[0] == 'v':
|
||||
content.cols = 1
|
||||
content.rows = 99
|
||||
else:
|
||||
content.cols = 99
|
||||
content.rows = 1
|
||||
|
||||
def on_arrow_image(self, *l):
|
||||
self._arrow_img.source = self.arrow_image
|
||||
|
||||
def on_arrow_pos(self, *l):
|
||||
self_content = self.content
|
||||
if not self_content:
|
||||
Clock.schedule_once(self.on_arrow_pos)
|
||||
return
|
||||
if self_content not in self.children:
|
||||
Clock.schedule_once(self.on_arrow_pos)
|
||||
return
|
||||
self_arrow_pos = self.arrow_pos
|
||||
if self._prev_arrow_pos == self_arrow_pos:
|
||||
return
|
||||
self._prev_arrow_pos = self_arrow_pos
|
||||
|
||||
self_arrow_layout = self._arrow_layout
|
||||
self_arrow_layout.clear_widgets()
|
||||
self_arrow_img = self._arrow_img
|
||||
self._sctr = self._arrow_img
|
||||
super(Bubble, self).clear_widgets()
|
||||
self_content.parent = None
|
||||
|
||||
self_arrow_img.size_hint = (1, None)
|
||||
self_arrow_img.height = dp(self_arrow_img.texture_size[1])
|
||||
self_arrow_img.pos = 0, 0
|
||||
widget_list = []
|
||||
arrow_list = []
|
||||
parent = self_arrow_img.parent
|
||||
if parent:
|
||||
parent.remove_widget(self_arrow_img)
|
||||
|
||||
if self_arrow_pos[0] == 'b' or self_arrow_pos[0] == 't':
|
||||
self.cols = 1
|
||||
self.rows = 3
|
||||
self_arrow_layout.orientation = 'horizontal'
|
||||
self_arrow_img.width = self.width / 3
|
||||
self_arrow_layout.size_hint = (1, None)
|
||||
self_arrow_layout.height = self_arrow_img.height
|
||||
if self_arrow_pos[0] == 'b':
|
||||
if self_arrow_pos == 'bottom_mid':
|
||||
widget_list = (self_content, self_arrow_img)
|
||||
else:
|
||||
if self_arrow_pos == 'bottom_left':
|
||||
arrow_list = (self_arrow_img, Widget(), Widget())
|
||||
elif self_arrow_pos == 'bottom_right':
|
||||
# add two dummy widgets
|
||||
arrow_list = (Widget(), Widget(), self_arrow_img)
|
||||
widget_list = (self_content, self_arrow_layout)
|
||||
else:
|
||||
sctr = Scatter(do_translation=False,
|
||||
rotation=180,
|
||||
do_rotation=False,
|
||||
do_scale=False,
|
||||
size_hint=(None, None),
|
||||
size=self_arrow_img.size)
|
||||
sctr.add_widget(self_arrow_img)
|
||||
if self_arrow_pos == 'top_mid':
|
||||
# add two dummy widgets
|
||||
arrow_list = (Widget(), sctr, Widget())
|
||||
elif self_arrow_pos == 'top_left':
|
||||
arrow_list = (sctr, Widget(), Widget())
|
||||
elif self_arrow_pos == 'top_right':
|
||||
arrow_list = (Widget(), Widget(), sctr)
|
||||
widget_list = (self_arrow_layout, self_content)
|
||||
elif self_arrow_pos[0] == 'l' or self_arrow_pos[0] == 'r':
|
||||
self.cols = 3
|
||||
self.rows = 1
|
||||
self_arrow_img.width = self.height / 3
|
||||
self_arrow_layout.orientation = 'vertical'
|
||||
self_arrow_layout.cols = 1
|
||||
self_arrow_layout.size_hint = (None, 1)
|
||||
self_arrow_layout.width = self_arrow_img.height
|
||||
|
||||
rotation = -90 if self_arrow_pos[0] == 'l' else 90
|
||||
self._sctr = sctr = Scatter(do_translation=False,
|
||||
rotation=rotation,
|
||||
do_rotation=False,
|
||||
do_scale=False,
|
||||
size_hint=(None, None),
|
||||
size=(self_arrow_img.size))
|
||||
sctr.add_widget(self_arrow_img)
|
||||
|
||||
if self_arrow_pos[-4:] == '_top':
|
||||
arrow_list = (Widget(size_hint=(1, .07)),
|
||||
sctr, Widget(size_hint=(1, .3)))
|
||||
elif self_arrow_pos[-4:] == '_mid':
|
||||
arrow_list = (Widget(), sctr, Widget())
|
||||
Clock.schedule_once(self._update_arrow)
|
||||
elif self_arrow_pos[-7:] == '_bottom':
|
||||
arrow_list = (Widget(), Widget(), sctr)
|
||||
|
||||
if self_arrow_pos[0] == 'l':
|
||||
widget_list = (self_arrow_layout, self_content)
|
||||
else:
|
||||
widget_list = (self_content, self_arrow_layout)
|
||||
|
||||
# add widgets to arrow_layout
|
||||
add = self_arrow_layout.add_widget
|
||||
for widg in arrow_list:
|
||||
add(widg)
|
||||
|
||||
# add widgets to self
|
||||
add = self.add_widget
|
||||
for widg in widget_list:
|
||||
add(widg)
|
||||
|
||||
def _update_arrow(self, *dt):
|
||||
if self.arrow_pos in ('left_mid', 'right_mid'):
|
||||
self._sctr.center_y = self._arrow_layout.center_y
|
||||
|
||||
@property
|
||||
def _fills_row_first(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def _fills_from_left_to_right(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def _fills_from_top_to_bottom(self):
|
||||
return True
|
||||
137
kivy/uix/button.py
Normal file
137
kivy/uix/button.py
Normal 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 colour. 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)
|
||||
'''
|
||||
118
kivy/uix/camera.py
Normal file
118
kivy/uix/camera.py
Normal 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()
|
||||
695
kivy/uix/carousel.py
Normal file
695
kivy/uix/carousel.py
Normal 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, allow_stretch=True)
|
||||
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, allow_stretch=True)
|
||||
carousel.add_widget(image)
|
||||
return carousel
|
||||
|
||||
Example1().run()
|
||||
197
kivy/uix/checkbox.py
Normal file
197
kivy/uix/checkbox.py
Normal 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)
|
||||
238
kivy/uix/codeinput.py
Normal file
238
kivy/uix/codeinput.py
Normal 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()
|
||||
485
kivy/uix/colorpicker.py
Normal file
485
kivy/uix/colorpicker.py
Normal file
@@ -0,0 +1,485 @@
|
||||
'''
|
||||
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 kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.properties import (NumericProperty, BoundedNumericProperty,
|
||||
ListProperty, ObjectProperty,
|
||||
ReferenceListProperty, StringProperty,
|
||||
AliasProperty)
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import Mesh, InstructionGroup, Color
|
||||
from kivy.utils import get_color_from_hex, get_hex_from_color
|
||||
from kivy.logger import Logger
|
||||
from math import cos, sin, pi, sqrt, atan
|
||||
from colorsys import rgb_to_hsv, hsv_to_rgb
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
_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
|
||||
|
||||
_hsv = ListProperty([1, 1, 1, 0])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ColorWheel, self).__init__(**kwargs)
|
||||
|
||||
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))]
|
||||
|
||||
def on__origin(self, instance, value):
|
||||
self.init_wheel(None)
|
||||
|
||||
def on__radius(self, instance, value):
|
||||
self.init_wheel(None)
|
||||
|
||||
def init_wheel(self, dt):
|
||||
# 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
|
||||
goal_sv_idx >= 0 and
|
||||
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, max(0, float(text)))
|
||||
if mode == 'rgb':
|
||||
self.color[clr_idx] = float(text) / 255.
|
||||
else:
|
||||
hsv = list(self.hsv[:])
|
||||
hsv[clr_idx] = float(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()
|
||||
391
kivy/uix/dropdown.py
Normal file
391
kivy/uix/dropdown.py
Normal 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)
|
||||
772
kivy/uix/effectwidget.py
Normal file
772
kivy/uix/effectwidget.py
Normal 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
|
||||
1134
kivy/uix/filechooser.py
Normal file
1134
kivy/uix/filechooser.py
Normal file
File diff suppressed because it is too large
Load Diff
148
kivy/uix/floatlayout.py
Normal file
148
kivy/uix/floatlayout.py
Normal 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)
|
||||
625
kivy/uix/gesturesurface.py
Normal file
625
kivy/uix/gesturesurface.py
Normal 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
|
||||
629
kivy/uix/gridlayout.py
Normal file
629
kivy/uix/gridlayout.py
Normal 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)
|
||||
434
kivy/uix/image.py
Normal file
434
kivy/uix/image.py
Normal file
@@ -0,0 +1,434 @@
|
||||
'''
|
||||
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 and fits inside the widget bounding box.
|
||||
If you don't want that, you can set `allow_stretch` to True and `keep_ratio`
|
||||
to False.
|
||||
|
||||
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
|
||||
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:`allow_stretch` and :attr:`keep_ratio` properties.
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
'''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
|
||||
|
||||
:attr:`allow_stretch` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
keep_ratio = BooleanProperty(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
|
||||
|
||||
:attr:`keep_ratio` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
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(.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
|
||||
|
||||
# ensure that the width is always maximized to the container width
|
||||
if self.allow_stretch:
|
||||
if not self.keep_ratio:
|
||||
return [w, h]
|
||||
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.allow_stretch:
|
||||
ih = h
|
||||
else:
|
||||
ih = min(h, th)
|
||||
iw = ih * ratio
|
||||
return [iw, ih]
|
||||
|
||||
norm_image_size = AliasProperty(get_norm_image_size,
|
||||
bind=('texture', 'size', 'allow_stretch',
|
||||
'image_ratio', 'keep_ratio'),
|
||||
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)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
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()
|
||||
1107
kivy/uix/label.py
Normal file
1107
kivy/uix/label.py
Normal file
File diff suppressed because it is too large
Load Diff
322
kivy/uix/layout.py
Normal file
322
kivy/uix/layout.py
Normal 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])
|
||||
339
kivy/uix/modalview.py
Normal file
339
kivy/uix/modalview.py
Normal 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 colour. 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()
|
||||
233
kivy/uix/pagelayout.py
Normal file
233
kivy/uix/pagelayout.py
Normal 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)
|
||||
266
kivy/uix/popup.py
Normal file
266
kivy/uix/popup.py
Normal 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()
|
||||
95
kivy/uix/progressbar.py
Normal file
95
kivy/uix/progressbar.py
Normal 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))
|
||||
183
kivy/uix/recycleboxlayout.py
Normal file
183
kivy/uix/recycleboxlayout.py
Normal 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))
|
||||
255
kivy/uix/recyclegridlayout.py
Normal file
255
kivy/uix/recyclegridlayout.py
Normal 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, )
|
||||
446
kivy/uix/recyclelayout.py
Normal file
446
kivy/uix/recyclelayout.py
Normal 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
Reference in New Issue
Block a user