first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
56
kivy_venv/lib/python3.11/site-packages/kivy/uix/__init__.py
Normal file
56
kivy_venv/lib/python3.11/site-packages/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`
|
||||
|
||||
----
|
||||
'''
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
484
kivy_venv/lib/python3.11/site-packages/kivy/uix/accordion.py
Normal file
484
kivy_venv/lib/python3.11/site-packages/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)
|
934
kivy_venv/lib/python3.11/site-packages/kivy/uix/actionbar.py
Normal file
934
kivy_venv/lib/python3.11/site-packages/kivy/uix/actionbar.py
Normal file
|
@ -0,0 +1,934 @@
|
|||
'''
|
||||
Action Bar
|
||||
==========
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. image:: images/actionbar.png
|
||||
:align: right
|
||||
|
||||
The ActionBar widget is like Android's `ActionBar
|
||||
<http://developer.android.com/guide/topics/ui/actionbar.html>`_, where items
|
||||
are stacked horizontally. When the area becomes to small, widgets are moved
|
||||
into the :class:`ActionOverflow` area.
|
||||
|
||||
An :class:`ActionBar` must contain an :class:`ActionView` with various
|
||||
:class:`ContextualActionViews <kivy.uix.actionbar.ContextualActionView>`.
|
||||
An :class:`ActionView` must contain a child :class:`ActionPrevious` which may
|
||||
have title, app_icon and previous_icon properties. :class:`ActionView` children
|
||||
must be
|
||||
subclasses of :class:`ActionItems <ActionItem>`. Some predefined ones include
|
||||
an :class:`ActionButton`, an :class:`ActionToggleButton`, an
|
||||
:class:`ActionCheck`, an :class:`ActionSeparator` and an :class:`ActionGroup`.
|
||||
|
||||
An :class:`ActionGroup` is used to display :class:`ActionItems <ActionItem>`
|
||||
in a group. An :class:`ActionView` will always display an :class:`ActionGroup`
|
||||
after other :class:`ActionItems <ActionItem>`. An :class:`ActionView` contains
|
||||
an :class:`ActionOverflow`, but this is only made visible when required i.e.
|
||||
the available area is too small to fit all the widgets. A
|
||||
:class:`ContextualActionView` is a subclass of an:class:`ActionView`.
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
:class:`ActionGroup` core rewritten from :class:`Spinner` to pure
|
||||
:class:`DropDown`
|
||||
'''
|
||||
|
||||
__all__ = ('ActionBarException', 'ActionItem', 'ActionButton',
|
||||
'ActionToggleButton', 'ActionCheck', 'ActionSeparator',
|
||||
'ActionDropDown', 'ActionGroup', 'ActionOverflow',
|
||||
'ActionView', 'ContextualActionView', 'ActionPrevious',
|
||||
'ActionBar')
|
||||
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.uix.checkbox import CheckBox
|
||||
from kivy.uix.spinner import Spinner
|
||||
from kivy.uix.label import Label
|
||||
from kivy.config import Config
|
||||
from kivy.properties import ObjectProperty, NumericProperty, BooleanProperty, \
|
||||
StringProperty, ListProperty, OptionProperty, AliasProperty, ColorProperty
|
||||
from kivy.metrics import sp
|
||||
from kivy.lang import Builder
|
||||
from functools import partial
|
||||
|
||||
|
||||
window_icon = ''
|
||||
if Config:
|
||||
window_icon = Config.get('kivy', 'window_icon')
|
||||
|
||||
|
||||
class ActionBarException(Exception):
|
||||
'''
|
||||
ActionBarException class
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionItem(object):
|
||||
'''
|
||||
ActionItem class, an abstract class for all ActionBar widgets. To create a
|
||||
custom widget for an ActionBar, inherit from this class. See module
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
minimum_width = NumericProperty('90sp')
|
||||
'''
|
||||
Minimum Width required by an ActionItem.
|
||||
|
||||
:attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to '90sp'.
|
||||
'''
|
||||
|
||||
def get_pack_width(self):
|
||||
return max(self.minimum_width, self.width)
|
||||
|
||||
pack_width = AliasProperty(get_pack_width,
|
||||
bind=('minimum_width', 'width'),
|
||||
cache=True)
|
||||
'''
|
||||
(read-only) The actual width to use when packing the items. Equal to the
|
||||
greater of minimum_width and width.
|
||||
|
||||
:attr:`pack_width` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
important = BooleanProperty(False)
|
||||
'''
|
||||
Determines if an ActionItem is important or not. If an item is important
|
||||
and space is limited, this item will be displayed in preference to others.
|
||||
|
||||
:attr:`important` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
inside_group = BooleanProperty(False)
|
||||
'''
|
||||
(internal) Determines if an ActionItem is displayed inside an
|
||||
ActionGroup or not.
|
||||
|
||||
:attr:`inside_group` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_item')
|
||||
'''
|
||||
Background image of the ActionItem used for the default graphical
|
||||
representation when the ActionItem is not pressed.
|
||||
|
||||
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_item'.
|
||||
'''
|
||||
|
||||
background_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_item_down')
|
||||
'''
|
||||
Background image of the ActionItem used for the default graphical
|
||||
representation when an ActionItem is pressed.
|
||||
|
||||
:attr:`background_down` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_item_down'.
|
||||
'''
|
||||
|
||||
mipmap = BooleanProperty(True)
|
||||
'''
|
||||
Defines whether the image/icon dispayed on top of the button uses a
|
||||
mipmap or not.
|
||||
|
||||
:attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `True`.
|
||||
'''
|
||||
|
||||
|
||||
class ActionButton(Button, ActionItem):
|
||||
'''
|
||||
ActionButton class, see module documentation for more information.
|
||||
|
||||
The text color, width and size_hint_x are set manually via the Kv language
|
||||
file. It covers a lot of cases: with/without an icon, with/without a group
|
||||
and takes care of the padding between elements.
|
||||
|
||||
You don't have much control over these properties, so if you want to
|
||||
customize its appearance, we suggest you create you own button
|
||||
representation. You can do this by creating a class that subclasses an
|
||||
existing widget and an :class:`ActionItem`::
|
||||
|
||||
class MyOwnActionButton(Button, ActionItem):
|
||||
pass
|
||||
|
||||
You can then create your own style using the Kv language.
|
||||
'''
|
||||
|
||||
icon = StringProperty(None, allownone=True)
|
||||
'''
|
||||
Source image to use when the Button is part of the ActionBar. If the
|
||||
Button is in a group, the text will be preferred.
|
||||
|
||||
:attr:`icon` is a :class:`~kivy.properties.StringProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
|
||||
class ActionPrevious(BoxLayout, ActionItem):
|
||||
'''
|
||||
ActionPrevious class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
with_previous = BooleanProperty(True)
|
||||
'''
|
||||
Specifies whether the previous_icon will be shown or not. Note that it is
|
||||
up to the user to implement the desired behavior using the *on_press* or
|
||||
similar events.
|
||||
|
||||
:attr:`with_previous` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
app_icon = StringProperty(window_icon)
|
||||
'''
|
||||
Application icon for the ActionView.
|
||||
|
||||
:attr:`app_icon` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to the window icon if set, otherwise
|
||||
'data/logo/kivy-icon-32.png'.
|
||||
'''
|
||||
|
||||
app_icon_width = NumericProperty(0)
|
||||
'''
|
||||
Width of app_icon image.
|
||||
|
||||
:attr:`app_icon_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
app_icon_height = NumericProperty(0)
|
||||
'''
|
||||
Height of app_icon image.
|
||||
|
||||
:attr:`app_icon_height` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
color = ColorProperty([1, 1, 1, 1])
|
||||
'''
|
||||
Text color, in the format (r, g, b, a)
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults
|
||||
to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
previous_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/previous_normal')
|
||||
'''
|
||||
Image for the 'previous' ActionButtons default graphical representation.
|
||||
|
||||
:attr:`previous_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/previous_normal'.
|
||||
'''
|
||||
|
||||
previous_image_width = NumericProperty(0)
|
||||
'''
|
||||
Width of previous_image image.
|
||||
|
||||
:attr:`width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
previous_image_height = NumericProperty(0)
|
||||
'''
|
||||
Height of previous_image image.
|
||||
|
||||
:attr:`app_icon_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
title = StringProperty('')
|
||||
'''
|
||||
Title for ActionView.
|
||||
|
||||
:attr:`title` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to ''.
|
||||
'''
|
||||
|
||||
markup = BooleanProperty(False)
|
||||
'''
|
||||
If True, the text will be rendered using the
|
||||
:class:`~kivy.core.text.markup.MarkupLabel`: you can change the style of
|
||||
the text using tags. Check the :doc:`api-kivy.core.text.markup`
|
||||
documentation for more information.
|
||||
|
||||
:attr:`markup` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
super(ActionPrevious, self).__init__(**kwargs)
|
||||
if not self.app_icon:
|
||||
self.app_icon = 'data/logo/kivy-icon-32.png'
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
|
||||
class ActionToggleButton(ActionItem, ToggleButton):
|
||||
'''
|
||||
ActionToggleButton class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
icon = StringProperty(None, allownone=True)
|
||||
'''
|
||||
Source image to use when the Button is part of the ActionBar. If the
|
||||
Button is in a group, the text will be preferred.
|
||||
'''
|
||||
|
||||
|
||||
class ActionLabel(ActionItem, Label):
|
||||
'''
|
||||
ActionLabel class, see module documentation for more information.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionCheck(ActionItem, CheckBox):
|
||||
'''
|
||||
ActionCheck class, see module documentation for more information.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionSeparator(ActionItem, Widget):
|
||||
'''
|
||||
ActionSeparator class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/separator')
|
||||
'''
|
||||
Background image for the separators default graphical representation.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/separator'.
|
||||
'''
|
||||
|
||||
|
||||
class ActionDropDown(DropDown):
|
||||
'''
|
||||
ActionDropDown class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
|
||||
class ActionGroup(ActionItem, Button):
|
||||
'''
|
||||
ActionGroup class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
use_separator = BooleanProperty(False)
|
||||
'''
|
||||
Specifies whether to use a separator after/before this group or not.
|
||||
|
||||
:attr:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
separator_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/separator')
|
||||
'''
|
||||
Background Image for an ActionSeparator in an ActionView.
|
||||
|
||||
:attr:`separator_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/separator'.
|
||||
'''
|
||||
|
||||
separator_width = NumericProperty(0)
|
||||
'''
|
||||
Width of the ActionSeparator in an ActionView.
|
||||
|
||||
:attr:`separator_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
'''
|
||||
|
||||
mode = OptionProperty('normal', options=('normal', 'spinner'))
|
||||
'''
|
||||
Sets the current mode of an ActionGroup. If mode is 'normal', the
|
||||
ActionGroups children will be displayed normally if there is enough
|
||||
space, otherwise they will be displayed in a spinner. If mode is
|
||||
'spinner', then the children will always be displayed in a spinner.
|
||||
|
||||
:attr:`mode` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'normal'.
|
||||
'''
|
||||
|
||||
dropdown_width = NumericProperty(0)
|
||||
'''
|
||||
If non zero, provides the width for the associated DropDown. This is
|
||||
useful when some items in the ActionGroup's DropDown are wider than usual
|
||||
and you don't want to make the ActionGroup widget itself wider.
|
||||
|
||||
:attr:`dropdown_width` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
is_open = BooleanProperty(False)
|
||||
'''By default, the DropDown is not open. Set to True to open it.
|
||||
|
||||
:attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.list_action_item = []
|
||||
self._list_overflow_items = []
|
||||
super(ActionGroup, self).__init__(**kwargs)
|
||||
|
||||
# real is_open independent on public event
|
||||
self._is_open = False
|
||||
|
||||
# create DropDown for the group and save its state to _is_open
|
||||
self._dropdown = ActionDropDown()
|
||||
self._dropdown.bind(attach_to=lambda ins, value: setattr(
|
||||
self, '_is_open', True if value else False
|
||||
))
|
||||
|
||||
# put open/close responsibility to the event
|
||||
# - trigger dropdown opening when clicked
|
||||
self.bind(on_release=lambda *args: setattr(
|
||||
self, 'is_open', True
|
||||
))
|
||||
|
||||
# - trigger dropdown closing when an item
|
||||
# in the dropdown is clicked
|
||||
self._dropdown.bind(on_dismiss=lambda *args: setattr(
|
||||
self, 'is_open', False
|
||||
))
|
||||
|
||||
def on_is_open(self, instance, value):
|
||||
# opening only if the DropDown is closed
|
||||
if value and not self._is_open:
|
||||
self._toggle_dropdown()
|
||||
self._dropdown.open(self)
|
||||
return
|
||||
|
||||
# closing is_open manually, dismiss manually
|
||||
if not value and self._is_open:
|
||||
self._dropdown.dismiss()
|
||||
|
||||
def _toggle_dropdown(self, *largs):
|
||||
ddn = self._dropdown
|
||||
ddn.size_hint_x = None
|
||||
|
||||
# if container was set incorrectly and/or is missing
|
||||
if not ddn.container:
|
||||
return
|
||||
children = ddn.container.children
|
||||
|
||||
# set DropDown width manually or if not set, then widen
|
||||
# the ActionGroup + DropDown until the widest child fits
|
||||
if children:
|
||||
ddn.width = self.dropdown_width or max(
|
||||
self.width, max(c.pack_width for c in children)
|
||||
)
|
||||
else:
|
||||
ddn.width = self.width
|
||||
|
||||
# set the DropDown children's height
|
||||
for item in children:
|
||||
item.size_hint_y = None
|
||||
item.height = max([self.height, sp(48)])
|
||||
|
||||
# dismiss DropDown manually
|
||||
# auto_dismiss applies to touching outside of the DropDown
|
||||
item.bind(on_release=ddn.dismiss)
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `item` to `widget`.
|
||||
'''
|
||||
# if adding ActionSeparator ('normal' mode,
|
||||
# everything visible), add it to the parent
|
||||
if isinstance(widget, ActionSeparator):
|
||||
super(ActionGroup, self).add_widget(widget, *args, **kwargs)
|
||||
return
|
||||
|
||||
if not isinstance(widget, ActionItem):
|
||||
raise ActionBarException('ActionGroup only accepts ActionItem')
|
||||
|
||||
self.list_action_item.append(widget)
|
||||
|
||||
def show_group(self):
|
||||
# 'normal' mode, items can fit to the view
|
||||
self.clear_widgets()
|
||||
for item in self._list_overflow_items + self.list_action_item:
|
||||
item.inside_group = True
|
||||
self._dropdown.add_widget(item)
|
||||
|
||||
def clear_widgets(self, *args, **kwargs):
|
||||
self._dropdown.clear_widgets(*args, **kwargs)
|
||||
|
||||
|
||||
class ActionOverflow(ActionGroup):
|
||||
'''
|
||||
ActionOverflow class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
overflow_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/overflow')
|
||||
'''
|
||||
Image to be used as an Overflow Image.
|
||||
|
||||
:attr:`overflow_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/overflow'.
|
||||
'''
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `action_item` to `widget`.
|
||||
'''
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
if isinstance(widget, ActionSeparator):
|
||||
return
|
||||
|
||||
if not isinstance(widget, ActionItem):
|
||||
raise ActionBarException('ActionView only accepts ActionItem'
|
||||
' (got {!r}'.format(widget))
|
||||
|
||||
else:
|
||||
if index == 0:
|
||||
index = len(self._list_overflow_items)
|
||||
self._list_overflow_items.insert(index, widget)
|
||||
|
||||
def show_default_items(self, parent):
|
||||
# display overflow and its items if widget's directly added to it
|
||||
if self._list_overflow_items == []:
|
||||
return
|
||||
self.show_group()
|
||||
super(ActionView, parent).add_widget(self)
|
||||
|
||||
|
||||
class ActionView(BoxLayout):
|
||||
'''
|
||||
ActionView class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
action_previous = ObjectProperty(None)
|
||||
'''
|
||||
Previous button for an ActionView.
|
||||
|
||||
:attr:`action_previous` is an :class:`~kivy.properties.ObjectProperty`
|
||||
and defaults to None.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''
|
||||
Background color in the format (r, g, b, a).
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_view')
|
||||
'''
|
||||
Background image of an ActionViews default graphical representation.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_view'.
|
||||
'''
|
||||
|
||||
use_separator = BooleanProperty(False)
|
||||
'''
|
||||
Specify whether to use a separator before every ActionGroup or not.
|
||||
|
||||
:attr:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
overflow_group = ObjectProperty(None)
|
||||
'''
|
||||
Widget to be used for the overflow.
|
||||
|
||||
:attr:`overflow_group` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to an instance of :class:`ActionOverflow`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._list_action_items = []
|
||||
self._list_action_group = []
|
||||
super(ActionView, self).__init__(**kwargs)
|
||||
self._state = ''
|
||||
if not self.overflow_group:
|
||||
self.overflow_group = ActionOverflow(
|
||||
use_separator=self.use_separator)
|
||||
|
||||
def on_action_previous(self, instance, value):
|
||||
self._list_action_items.insert(0, value)
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `action_item` to `widget`.
|
||||
'''
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
if not isinstance(widget, ActionItem):
|
||||
raise ActionBarException('ActionView only accepts ActionItem'
|
||||
' (got {!r}'.format(widget))
|
||||
|
||||
elif isinstance(widget, ActionOverflow):
|
||||
self.overflow_group = widget
|
||||
widget.use_separator = self.use_separator
|
||||
|
||||
elif isinstance(widget, ActionGroup):
|
||||
self._list_action_group.append(widget)
|
||||
widget.use_separator = self.use_separator
|
||||
|
||||
elif isinstance(widget, ActionPrevious):
|
||||
self.action_previous = widget
|
||||
|
||||
else:
|
||||
super(ActionView, self).add_widget(widget, index, *args, **kwargs)
|
||||
if index == 0:
|
||||
index = len(self._list_action_items)
|
||||
self._list_action_items.insert(index, widget)
|
||||
|
||||
def on_use_separator(self, instance, value):
|
||||
for group in self._list_action_group:
|
||||
group.use_separator = value
|
||||
if self.overflow_group:
|
||||
self.overflow_group.use_separator = value
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
super(ActionView, self).remove_widget(widget, *args, **kwargs)
|
||||
if isinstance(widget, ActionOverflow):
|
||||
for item in widget.list_action_item:
|
||||
if item in self._list_action_items:
|
||||
self._list_action_items.remove(item)
|
||||
|
||||
if widget in self._list_action_items:
|
||||
self._list_action_items.remove(widget)
|
||||
|
||||
def _clear_all(self):
|
||||
lst = self._list_action_items[:]
|
||||
self.clear_widgets()
|
||||
for group in self._list_action_group:
|
||||
group.clear_widgets()
|
||||
|
||||
self.overflow_group.clear_widgets()
|
||||
self.overflow_group.list_action_item = []
|
||||
self._list_action_items = lst
|
||||
|
||||
def _layout_all(self):
|
||||
# all the items can fit to the view, so expand everything
|
||||
super_add = super(ActionView, self).add_widget
|
||||
self._state = 'all'
|
||||
self._clear_all()
|
||||
if not self.action_previous.parent:
|
||||
super_add(self.action_previous)
|
||||
if len(self._list_action_items) > 1:
|
||||
for child in self._list_action_items[1:]:
|
||||
child.inside_group = False
|
||||
super_add(child)
|
||||
|
||||
for group in self._list_action_group:
|
||||
if group.mode == 'spinner':
|
||||
super_add(group)
|
||||
group.show_group()
|
||||
else:
|
||||
if group.list_action_item != []:
|
||||
super_add(ActionSeparator())
|
||||
for child in group.list_action_item:
|
||||
child.inside_group = False
|
||||
super_add(child)
|
||||
|
||||
self.overflow_group.show_default_items(self)
|
||||
|
||||
def _layout_group(self):
|
||||
# layout all the items in order to pack them per group
|
||||
super_add = super(ActionView, self).add_widget
|
||||
self._state = 'group'
|
||||
self._clear_all()
|
||||
if not self.action_previous.parent:
|
||||
super_add(self.action_previous)
|
||||
if len(self._list_action_items) > 1:
|
||||
for child in self._list_action_items[1:]:
|
||||
super_add(child)
|
||||
child.inside_group = False
|
||||
|
||||
for group in self._list_action_group:
|
||||
super_add(group)
|
||||
group.show_group()
|
||||
|
||||
self.overflow_group.show_default_items(self)
|
||||
|
||||
def _layout_random(self):
|
||||
# layout the items in order to pack all of them grouped, and display
|
||||
# only the action items having 'important'
|
||||
super_add = super(ActionView, self).add_widget
|
||||
self._state = 'random'
|
||||
self._clear_all()
|
||||
hidden_items = []
|
||||
hidden_groups = []
|
||||
total_width = 0
|
||||
if not self.action_previous.parent:
|
||||
super_add(self.action_previous)
|
||||
|
||||
width = (self.width - self.overflow_group.pack_width -
|
||||
self.action_previous.minimum_width)
|
||||
|
||||
if len(self._list_action_items):
|
||||
for child in self._list_action_items[1:]:
|
||||
if child.important:
|
||||
if child.pack_width + total_width < width:
|
||||
super_add(child)
|
||||
child.inside_group = False
|
||||
total_width += child.pack_width
|
||||
else:
|
||||
hidden_items.append(child)
|
||||
else:
|
||||
hidden_items.append(child)
|
||||
|
||||
# if space is left then display ActionItem inside their
|
||||
# ActionGroup
|
||||
if total_width < self.width:
|
||||
for group in self._list_action_group:
|
||||
if group.pack_width + total_width +\
|
||||
group.separator_width < width:
|
||||
super_add(group)
|
||||
group.show_group()
|
||||
total_width += (group.pack_width +
|
||||
group.separator_width)
|
||||
|
||||
else:
|
||||
hidden_groups.append(group)
|
||||
group_index = len(self.children) - 1
|
||||
# if space is left then display other ActionItems
|
||||
if total_width < self.width:
|
||||
for child in hidden_items[:]:
|
||||
if child.pack_width + total_width < width:
|
||||
super_add(child, group_index)
|
||||
total_width += child.pack_width
|
||||
child.inside_group = False
|
||||
hidden_items.remove(child)
|
||||
|
||||
# for all the remaining ActionItems and ActionItems with in
|
||||
# ActionGroups, Display them inside overflow_group
|
||||
extend_hidden = hidden_items.extend
|
||||
for group in hidden_groups:
|
||||
extend_hidden(group.list_action_item)
|
||||
|
||||
overflow_group = self.overflow_group
|
||||
|
||||
if hidden_items != []:
|
||||
over_add = super(overflow_group.__class__,
|
||||
overflow_group).add_widget
|
||||
for child in hidden_items:
|
||||
over_add(child)
|
||||
|
||||
overflow_group.show_group()
|
||||
if not self.overflow_group.parent:
|
||||
super_add(overflow_group)
|
||||
|
||||
def on_width(self, width, *args):
|
||||
# determine the layout to use
|
||||
|
||||
# can we display all of them?
|
||||
total_width = 0
|
||||
for child in self._list_action_items:
|
||||
total_width += child.pack_width
|
||||
for group in self._list_action_group:
|
||||
for child in group.list_action_item:
|
||||
total_width += child.pack_width
|
||||
if total_width <= self.width:
|
||||
if self._state != 'all':
|
||||
self._layout_all()
|
||||
return
|
||||
|
||||
# can we display them per group?
|
||||
total_width = 0
|
||||
for child in self._list_action_items:
|
||||
total_width += child.pack_width
|
||||
for group in self._list_action_group:
|
||||
total_width += group.pack_width
|
||||
if total_width < self.width:
|
||||
# ok, we can display all the items grouped
|
||||
if self._state != 'group':
|
||||
self._layout_group()
|
||||
return
|
||||
|
||||
# none of the solutions worked, display them in pack mode
|
||||
self._layout_random()
|
||||
|
||||
|
||||
class ContextualActionView(ActionView):
|
||||
'''
|
||||
ContextualActionView class, see the module documentation for more
|
||||
information.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ActionBar(BoxLayout):
|
||||
'''
|
||||
ActionBar class, which acts as the main container for an
|
||||
:class:`ActionView` instance. The ActionBar determines the overall
|
||||
styling aspects of the bar. :class:`ActionItem`\\s are not added to
|
||||
this class directly, but to the contained :class:`ActionView` instance.
|
||||
|
||||
:Events:
|
||||
`on_previous`
|
||||
Fired when action_previous of action_view is pressed.
|
||||
|
||||
Please see the module documentation for more information.
|
||||
'''
|
||||
|
||||
action_view = ObjectProperty(None)
|
||||
'''
|
||||
action_view of the ActionBar.
|
||||
|
||||
:attr:`action_view` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None or the last ActionView instance added to the ActionBar.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''
|
||||
Background color, in the format (r, g, b, a).
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/action_bar')
|
||||
|
||||
'''
|
||||
Background image of the ActionBars default graphical representation.
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/action_bar'.
|
||||
'''
|
||||
|
||||
border = ListProperty([2, 2, 2, 2])
|
||||
'''
|
||||
The border to be applied to the :attr:`background_image`.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
[2, 2, 2, 2]
|
||||
'''
|
||||
|
||||
__events__ = ('on_previous',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ActionBar, self).__init__(**kwargs)
|
||||
self._stack_cont_action_view = []
|
||||
self._emit_previous = partial(self.dispatch, 'on_previous')
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2.1.0
|
||||
Renamed argument `view` to `widget`.
|
||||
'''
|
||||
if isinstance(widget, ContextualActionView):
|
||||
self._stack_cont_action_view.append(widget)
|
||||
if widget.action_previous is not None:
|
||||
widget.action_previous.unbind(on_release=self._emit_previous)
|
||||
widget.action_previous.bind(on_release=self._emit_previous)
|
||||
self.clear_widgets()
|
||||
super(ActionBar, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
elif isinstance(widget, ActionView):
|
||||
self.action_view = widget
|
||||
super(ActionBar, self).add_widget(widget, *args, **kwargs)
|
||||
|
||||
else:
|
||||
raise ActionBarException(
|
||||
'ActionBar can only add ContextualActionView or ActionView')
|
||||
|
||||
def on_previous(self, *args):
|
||||
self._pop_contextual_action_view()
|
||||
|
||||
def _pop_contextual_action_view(self):
|
||||
'''Remove the current ContextualActionView and display either the
|
||||
previous one or the ActionView.
|
||||
'''
|
||||
self._stack_cont_action_view.pop()
|
||||
self.clear_widgets()
|
||||
if self._stack_cont_action_view == []:
|
||||
super(ActionBar, self).add_widget(self.action_view)
|
||||
else:
|
||||
super(ActionBar, self).add_widget(self._stack_cont_action_view[-1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.factory import Factory
|
||||
|
||||
# XXX clean the first registration done from '__main__' here.
|
||||
# otherwise kivy.uix.actionbar.ActionPrevious != __main__.ActionPrevious
|
||||
Factory.unregister('ActionPrevious')
|
||||
|
||||
Builder.load_string('''
|
||||
<MainWindow>:
|
||||
ActionBar:
|
||||
pos_hint: {'top':1}
|
||||
ActionView:
|
||||
use_separator: True
|
||||
ActionPrevious:
|
||||
title: 'Action Bar'
|
||||
with_previous: False
|
||||
ActionOverflow:
|
||||
ActionButton:
|
||||
text: 'Btn0'
|
||||
icon: 'atlas://data/images/defaulttheme/audio-volume-high'
|
||||
ActionButton:
|
||||
text: 'Btn1'
|
||||
ActionButton:
|
||||
text: 'Btn2'
|
||||
ActionGroup:
|
||||
text: 'Group 1'
|
||||
ActionButton:
|
||||
text: 'Btn3'
|
||||
ActionButton:
|
||||
text: 'Btn4'
|
||||
ActionGroup:
|
||||
dropdown_width: 200
|
||||
text: 'Group 2'
|
||||
ActionButton:
|
||||
text: 'Btn5'
|
||||
ActionButton:
|
||||
text: 'Btn6'
|
||||
ActionButton:
|
||||
text: 'Btn7'
|
||||
''')
|
||||
|
||||
class MainWindow(FloatLayout):
|
||||
pass
|
||||
|
||||
float_layout = MainWindow()
|
||||
runTouchApp(float_layout)
|
122
kivy_venv/lib/python3.11/site-packages/kivy/uix/anchorlayout.py
Normal file
122
kivy_venv/lib/python3.11/site-packages/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
|
|
@ -0,0 +1,95 @@
|
|||
'''
|
||||
Behaviors
|
||||
=========
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Behavior mixin classes
|
||||
----------------------
|
||||
|
||||
This module implements behaviors that can be
|
||||
`mixed in <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
with existing base widgets. The idea behind these classes is to encapsulate
|
||||
properties and events associated with certain types of widgets.
|
||||
|
||||
Isolating these properties and events in a mixin class allows you to define
|
||||
your own implementation for standard kivy widgets that can act as drop-in
|
||||
replacements. This means you can re-style and re-define widgets as desired
|
||||
without breaking compatibility: as long as they implement the behaviors
|
||||
correctly, they can simply replace the standard widgets.
|
||||
|
||||
Adding behaviors
|
||||
----------------
|
||||
|
||||
Say you want to add :class:`~kivy.uix.button.Button` capabilities to an
|
||||
:class:`~kivy.uix.image.Image`, you could do::
|
||||
|
||||
class IconButton(ButtonBehavior, Image):
|
||||
pass
|
||||
|
||||
This would give you an :class:`~kivy.uix.image.Image` with the events and
|
||||
properties inherited from :class:`ButtonBehavior`. For example, the *on_press*
|
||||
and *on_release* events would be fired when appropriate::
|
||||
|
||||
class IconButton(ButtonBehavior, Image):
|
||||
def on_press(self):
|
||||
print("on_press")
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
IconButton:
|
||||
on_press: print('on_press')
|
||||
|
||||
Naturally, you could also bind to any property changes the behavior class
|
||||
offers:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def state_changed(*args):
|
||||
print('state changed')
|
||||
|
||||
button = IconButton()
|
||||
button.bind(state=state_changed)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The behavior class must always be _before_ the widget class. If you don't
|
||||
specify the inheritance in this order, the behavior will not work because
|
||||
the behavior methods are overwritten by the class method listed first.
|
||||
|
||||
Similarly, if you combine a behavior class with a class which
|
||||
requires the use of the methods also defined by the behavior class, the
|
||||
resulting class may not function properly. For example, when combining the
|
||||
:class:`ButtonBehavior` with a :class:`~kivy.uix.slider.Slider`, both of
|
||||
which use the :meth:`~kivy.uix.widget.Widget.on_touch_up` method,
|
||||
the resulting class may not work properly.
|
||||
|
||||
.. versionchanged:: 1.9.1
|
||||
|
||||
The individual behavior classes, previously in one big `behaviors.py`
|
||||
file, has been split into a single file for each class under the
|
||||
:mod:`~kivy.uix.behaviors` module. All the behaviors are still imported
|
||||
in the :mod:`~kivy.uix.behaviors` module so they are accessible as before
|
||||
(e.g. both `from kivy.uix.behaviors import ButtonBehavior` and
|
||||
`from kivy.uix.behaviors.button import ButtonBehavior` work).
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', 'ToggleButtonBehavior', 'DragBehavior',
|
||||
'FocusBehavior', 'CompoundSelectionBehavior',
|
||||
'CodeNavigationBehavior', 'EmacsBehavior', 'CoverBehavior',
|
||||
'TouchRippleBehavior', 'TouchRippleButtonBehavior')
|
||||
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from kivy.uix.behaviors.togglebutton import ToggleButtonBehavior
|
||||
from kivy.uix.behaviors.drag import DragBehavior
|
||||
from kivy.uix.behaviors.focus import FocusBehavior
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.behaviors.codenavigation import CodeNavigationBehavior
|
||||
from kivy.uix.behaviors.emacs import EmacsBehavior
|
||||
from kivy.uix.behaviors.cover import CoverBehavior
|
||||
from kivy.uix.behaviors.touchripple import TouchRippleBehavior
|
||||
from kivy.uix.behaviors.touchripple import TouchRippleButtonBehavior
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,212 @@
|
|||
'''
|
||||
Button Behavior
|
||||
===============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. You can combine this class with
|
||||
other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative buttons that preserve Kivy button behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds button behavior to an image to make a checkbox that
|
||||
behaves like a button::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_press(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
|
||||
def on_release(self):
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
See :class:`~kivy.uix.behaviors.ButtonBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('ButtonBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
from kivy.properties import OptionProperty, ObjectProperty, \
|
||||
BooleanProperty, NumericProperty
|
||||
from time import time
|
||||
|
||||
|
||||
class ButtonBehavior(object):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.button.Button` behavior. Please see the
|
||||
:mod:`button behaviors module <kivy.uix.behaviors.button>` documentation
|
||||
for more information.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
|
||||
'''
|
||||
|
||||
state = OptionProperty('normal', options=('normal', 'down'))
|
||||
'''The state of the button, must be one of 'normal' or 'down'.
|
||||
The state is 'down' only when the button is currently touched/clicked,
|
||||
otherwise its 'normal'.
|
||||
|
||||
:attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
|
||||
to 'normal'.
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
min_state_time = NumericProperty(0)
|
||||
'''The minimum period of time which the widget must remain in the
|
||||
`'down'` state.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
:attr:`min_state_time` is a float and defaults to 0.035. This value is
|
||||
taken from :class:`~kivy.config.Config`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The default value is now False.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
if 'min_state_time' not in kwargs:
|
||||
self.min_state_time = float(Config.get('graphics',
|
||||
'min_state_time'))
|
||||
super(ButtonBehavior, self).__init__(**kwargs)
|
||||
self.__state_event = None
|
||||
self.__touch_time = None
|
||||
self.fbind('state', self.cancel_event)
|
||||
|
||||
def _do_press(self):
|
||||
self.state = 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
self.state = 'normal'
|
||||
|
||||
def cancel_event(self, *args):
|
||||
if self.__state_event:
|
||||
self.__state_event.cancel()
|
||||
self.__state_event = None
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(ButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.__touch_time = time()
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(ButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(ButtonBehavior, self).on_touch_up(touch)
|
||||
assert self in touch.ud
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
|
||||
if (not self.always_release and
|
||||
not self.collide_point(*touch.pos)):
|
||||
self._do_release()
|
||||
return
|
||||
|
||||
touchtime = time() - self.__touch_time
|
||||
if touchtime < self.min_state_time:
|
||||
self.__state_event = Clock.schedule_once(
|
||||
self._do_release, self.min_state_time - touchtime)
|
||||
else:
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
return True
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
||||
|
||||
def trigger_action(self, duration=0.1):
|
||||
'''Trigger whatever action(s) have been bound to the button by calling
|
||||
both the on_press and on_release callbacks.
|
||||
|
||||
This is similar to a quick button press without using any touch events,
|
||||
but note that like most kivy code, this is not guaranteed to be safe to
|
||||
call from external threads. If needed use
|
||||
:class:`Clock <kivy.clock.Clock>` to safely schedule this function and
|
||||
the resulting callbacks to be called from the main thread.
|
||||
|
||||
Duration is the length of the press in seconds. Pass 0 if you want
|
||||
the action to happen instantly.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
self._do_press()
|
||||
self.dispatch('on_press')
|
||||
|
||||
def trigger_release(dt):
|
||||
self._do_release()
|
||||
self.dispatch('on_release')
|
||||
if not duration:
|
||||
trigger_release(0)
|
||||
else:
|
||||
Clock.schedule_once(trigger_release, duration)
|
|
@ -0,0 +1,167 @@
|
|||
'''
|
||||
Code Navigation Behavior
|
||||
========================
|
||||
|
||||
The :class:`~kivy.uix.bahviors.CodeNavigationBehavior` modifies navigation
|
||||
behavior in the :class:`~kivy.uix.textinput.TextInput`, making it work like an
|
||||
IDE instead of a word processor.
|
||||
|
||||
Using this mixin gives the TextInput the ability to recognize whitespace,
|
||||
punctuation and case variations (e.g. CamelCase) when moving over text. It
|
||||
is currently used by the :class:`~kivy.uix.codeinput.CodeInput` widget.
|
||||
'''
|
||||
|
||||
__all__ = ('CodeNavigationBehavior', )
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
import string
|
||||
|
||||
|
||||
class CodeNavigationBehavior(EventDispatcher):
|
||||
'''Code navigation behavior. Modifies the navigation behavior in TextInput
|
||||
to work like an IDE instead of a word processor. Please see the
|
||||
:mod:`code navigation behaviors module <kivy.uix.behaviors.codenavigation>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
def _move_cursor_word_left(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
pos -= 1
|
||||
|
||||
if pos == 0:
|
||||
return 0, 0
|
||||
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if col == -1:
|
||||
if row == 0:
|
||||
return 0, 0
|
||||
row -= 1
|
||||
rline = lines[row]
|
||||
col = len(rline)
|
||||
lc = c
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
if lc not in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in ws:
|
||||
col += 1
|
||||
break
|
||||
if mode in ('normal', 'camel') and c in punct:
|
||||
col += 1
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
col += 1
|
||||
break
|
||||
if mode == 'us' and c != '_' and (c in punct or c in ws):
|
||||
col += 1
|
||||
break
|
||||
|
||||
if mode == 'us' and c != '_':
|
||||
mode = ('normal' if c in ucase
|
||||
else 'ws' if c in ws
|
||||
else 'camel')
|
||||
elif mode == 'ws' and c not in ws:
|
||||
mode = ('normal' if c in ucase
|
||||
else 'us' if c == '_'
|
||||
else 'punct' if c in punct
|
||||
else 'camel')
|
||||
|
||||
col -= 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == len(lines) - 1:
|
||||
return row, len(lines[row])
|
||||
row += 1
|
||||
col = 0
|
||||
|
||||
return col, row
|
||||
|
||||
def _move_cursor_word_right(self, index=None):
|
||||
pos = index or self.cursor_index()
|
||||
col, row = self.get_cursor_from_index(pos)
|
||||
lines = self._lines
|
||||
mrow = len(lines) - 1
|
||||
|
||||
if row == mrow and col == len(lines[row]):
|
||||
return col, row
|
||||
|
||||
ucase = string.ascii_uppercase
|
||||
lcase = string.ascii_lowercase
|
||||
ws = string.whitespace
|
||||
punct = string.punctuation
|
||||
|
||||
mode = 'normal'
|
||||
|
||||
rline = lines[row]
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c == '_':
|
||||
mode = 'us'
|
||||
elif c in punct:
|
||||
mode = 'punct'
|
||||
elif c in lcase:
|
||||
mode = 'camel'
|
||||
|
||||
while True:
|
||||
if mode in ('normal', 'camel', 'punct') and c in ws:
|
||||
mode = 'ws'
|
||||
elif mode in ('normal', 'camel') and c == '_':
|
||||
mode = 'us'
|
||||
elif mode == 'normal' and c not in ucase:
|
||||
mode = 'camel'
|
||||
|
||||
if mode == 'us':
|
||||
if c in ws:
|
||||
mode = 'ws'
|
||||
elif c != '_':
|
||||
break
|
||||
if mode == 'ws' and c not in ws:
|
||||
break
|
||||
if mode == 'camel' and c in ucase:
|
||||
break
|
||||
if mode == 'punct' and (c == '_' or c not in punct):
|
||||
break
|
||||
if mode != 'punct' and c != '_' and c in punct:
|
||||
break
|
||||
|
||||
col += 1
|
||||
|
||||
if col > len(rline):
|
||||
if row == mrow:
|
||||
return len(rline), mrow
|
||||
row += 1
|
||||
rline = lines[row]
|
||||
col = 0
|
||||
|
||||
c = rline[col] if len(rline) > col else '\n'
|
||||
if c == '\n':
|
||||
break
|
||||
|
||||
return col, row
|
|
@ -0,0 +1,689 @@
|
|||
'''
|
||||
Compound Selection Behavior
|
||||
===========================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class implements the logic
|
||||
behind keyboard and touch selection of selectable widgets managed by the
|
||||
derived widget. For example, it can be combined with a
|
||||
:class:`~kivy.uix.gridlayout.GridLayout` to add selection to the layout.
|
||||
|
||||
Compound selection concepts
|
||||
---------------------------
|
||||
|
||||
At its core, it keeps a dynamic list of widgets that can be selected.
|
||||
Then, as the touches and keyboard input are passed in, it selects one or
|
||||
more of the widgets based on these inputs. For example, it uses the mouse
|
||||
scroll and keyboard up/down buttons to scroll through the list of widgets.
|
||||
Multiselection can also be achieved using the keyboard shift and ctrl keys.
|
||||
|
||||
Finally, in addition to the up/down type keyboard inputs, compound selection
|
||||
can also accept letters from the keyboard to be used to select nodes with
|
||||
associated strings that start with those letters, similar to how files
|
||||
are selected by a file browser.
|
||||
|
||||
Selection mechanics
|
||||
-------------------
|
||||
|
||||
When the controller needs to select a node, it calls :meth:`select_node` and
|
||||
:meth:`deselect_node`. Therefore, they must be overwritten in order alter
|
||||
node selection. By default, the class doesn't listen for keyboard or
|
||||
touch events, so the derived widget must call
|
||||
:meth:`select_with_touch`, :meth:`select_with_key_down`, and
|
||||
:meth:`select_with_key_up` on events that it wants to pass on for selection
|
||||
purposes.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
To add selection to a grid layout which will contain
|
||||
:class:`~kivy.uix.Button` widgets. For each button added to the layout, you
|
||||
need to bind the :attr:`~kivy.uix.widget.Widget.on_touch_down` of the button
|
||||
to :meth:`select_with_touch` to pass on the touch events::
|
||||
|
||||
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.core.window import Window
|
||||
from kivy.app import App
|
||||
|
||||
|
||||
class SelectableGrid(FocusBehavior, CompoundSelectionBehavior, GridLayout):
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key presses will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_down(
|
||||
window, keycode, text, modifiers):
|
||||
return True
|
||||
if self.select_with_key_down(window, keycode, text, modifiers):
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
"""Based on FocusBehavior that provides automatic keyboard
|
||||
access, key release will be used to select children.
|
||||
"""
|
||||
if super(SelectableGrid, self).keyboard_on_key_up(window, keycode):
|
||||
return True
|
||||
if self.select_with_key_up(window, keycode):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
""" Override the adding of widgets so we can bind and catch their
|
||||
*on_touch_down* events. """
|
||||
widget.bind(on_touch_down=self.button_touch_down,
|
||||
on_touch_up=self.button_touch_up)
|
||||
return super(SelectableGrid, self)\
|
||||
.add_widget(widget, *args, **kwargs)
|
||||
|
||||
def button_touch_down(self, button, touch):
|
||||
""" Use collision detection to select buttons when the touch occurs
|
||||
within their area. """
|
||||
if button.collide_point(*touch.pos):
|
||||
self.select_with_touch(button, touch)
|
||||
|
||||
def button_touch_up(self, button, touch):
|
||||
""" Use collision detection to de-select buttons when the touch
|
||||
occurs outside their area and *touch_multiselect* is not True. """
|
||||
if not (button.collide_point(*touch.pos) or
|
||||
self.touch_multiselect):
|
||||
self.deselect_node(button)
|
||||
|
||||
def select_node(self, node):
|
||||
node.background_color = (1, 0, 0, 1)
|
||||
return super(SelectableGrid, self).select_node(node)
|
||||
|
||||
def deselect_node(self, node):
|
||||
node.background_color = (1, 1, 1, 1)
|
||||
super(SelectableGrid, self).deselect_node(node)
|
||||
|
||||
def on_selected_nodes(self, grid, nodes):
|
||||
print("Selected nodes = {0}".format(nodes))
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
grid = SelectableGrid(cols=3, rows=2, touch_multiselect=True,
|
||||
multiselect=True)
|
||||
for i in range(0, 6):
|
||||
grid.add_widget(Button(text="Button {0}".format(i)))
|
||||
return grid
|
||||
|
||||
|
||||
TestApp().run()
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('CompoundSelectionBehavior', )
|
||||
|
||||
from time import time
|
||||
from os import environ
|
||||
|
||||
from kivy.properties import NumericProperty, BooleanProperty, ListProperty
|
||||
|
||||
|
||||
if 'KIVY_DOC' not in environ:
|
||||
from kivy.config import Config
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
else:
|
||||
_is_desktop = False
|
||||
|
||||
|
||||
class CompoundSelectionBehavior(object):
|
||||
'''The Selection behavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
implements the logic behind keyboard and touch
|
||||
selection of selectable widgets managed by the derived widget. Please see
|
||||
the :mod:`compound selection behaviors module
|
||||
<kivy.uix.behaviors.compoundselection>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
|
||||
selected_nodes = ListProperty([])
|
||||
'''The list of selected nodes.
|
||||
|
||||
.. note::
|
||||
|
||||
Multiple nodes can be selected right after one another e.g. using the
|
||||
keyboard. When listening to :attr:`selected_nodes`, one should be
|
||||
aware of this.
|
||||
|
||||
:attr:`selected_nodes` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to the empty list, []. It is read-only and should not be modified.
|
||||
'''
|
||||
|
||||
touch_multiselect = BooleanProperty(False)
|
||||
'''A special touch mode which determines whether touch events, as
|
||||
processed by :meth:`select_with_touch`, will add the currently touched
|
||||
node to the selection, or if it will clear the selection before adding the
|
||||
node. This allows the selection of multiple nodes by simply touching them.
|
||||
|
||||
This is different from :attr:`multiselect` because when it is True,
|
||||
simply touching an unselected node will select it, even if ctrl is not
|
||||
pressed. If it is False, however, ctrl must be pressed in order to
|
||||
add to the selection when :attr:`multiselect` is True.
|
||||
|
||||
.. note::
|
||||
|
||||
:attr:`multiselect`, when False, will disable
|
||||
:attr:`touch_multiselect`.
|
||||
|
||||
:attr:`touch_multiselect` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to False.
|
||||
'''
|
||||
|
||||
multiselect = BooleanProperty(False)
|
||||
'''Determines whether multiple nodes can be selected. If enabled, keyboard
|
||||
shift and ctrl selection, optionally combined with touch, for example, will
|
||||
be able to select multiple widgets in the normally expected manner.
|
||||
This dominates :attr:`touch_multiselect` when False.
|
||||
|
||||
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
touch_deselect_last = BooleanProperty(not _is_desktop)
|
||||
'''Determines whether the last selected node can be deselected when
|
||||
:attr:`multiselect` or :attr:`touch_multiselect` is False.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`touch_deselect_last` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True on mobile, False on desktop platforms.
|
||||
'''
|
||||
|
||||
keyboard_select = BooleanProperty(True)
|
||||
'''Determines whether the keyboard can be used for selection. If False,
|
||||
keyboard inputs will be ignored.
|
||||
|
||||
:attr:`keyboard_select` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True.
|
||||
'''
|
||||
|
||||
page_count = NumericProperty(10)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when pageup (or pagedown) is
|
||||
pressed.
|
||||
|
||||
:attr:`page_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 10.
|
||||
'''
|
||||
|
||||
up_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the up (or down) arrow on
|
||||
the keyboard is pressed.
|
||||
|
||||
:attr:`up_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
right_count = NumericProperty(1)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the right (or left) arrow
|
||||
on the keyboard is pressed.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 1.
|
||||
'''
|
||||
|
||||
scroll_count = NumericProperty(0)
|
||||
'''Determines by how much the selected node is moved up or down, relative
|
||||
to the position of the last selected node, when the mouse scroll wheel is
|
||||
scrolled.
|
||||
|
||||
:attr:`right_count` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
nodes_order_reversed = BooleanProperty(True)
|
||||
''' (Internal) Indicates whether the order of the nodes as displayed top-
|
||||
down is reversed compared to their order in :meth:`get_selectable_nodes`
|
||||
(e.g. how the children property is reversed compared to how
|
||||
it's displayed).
|
||||
'''
|
||||
|
||||
text_entry_timeout = NumericProperty(1.)
|
||||
'''When typing characters in rapid succession (i.e. the time difference
|
||||
since the last character is less than :attr:`text_entry_timeout`), the
|
||||
keys get concatenated and the combined text is passed as the key argument
|
||||
of :meth:`goto_node`.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
_anchor = None # the last anchor node selected (e.g. shift relative node)
|
||||
# the idx may be out of sync
|
||||
_anchor_idx = 0 # cache indexes in case list hasn't changed
|
||||
_last_selected_node = None # the absolute last node selected
|
||||
_last_node_idx = 0
|
||||
_ctrl_down = False # if it's pressed - for e.g. shift selection
|
||||
_shift_down = False
|
||||
# holds str used to find node, e.g. if word is typed. passed to goto_node
|
||||
_word_filter = ''
|
||||
_last_key_time = 0 # time since last press, for finding whole strs in node
|
||||
_key_list = [] # keys that are already pressed, to not press continuously
|
||||
_offset_counts = {} # cache of counts for faster access
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CompoundSelectionBehavior, self).__init__(**kwargs)
|
||||
self._key_list = []
|
||||
|
||||
def ensure_single_select(*l):
|
||||
if (not self.multiselect) and len(self.selected_nodes) > 1:
|
||||
self.clear_selection()
|
||||
update_counts = self._update_counts
|
||||
update_counts()
|
||||
fbind = self.fbind
|
||||
fbind('multiselect', ensure_single_select)
|
||||
fbind('page_count', update_counts)
|
||||
fbind('up_count', update_counts)
|
||||
fbind('right_count', update_counts)
|
||||
fbind('scroll_count', update_counts)
|
||||
|
||||
def select_with_touch(self, node, touch=None):
|
||||
'''(internal) Processes a touch on the node. This should be called by
|
||||
the derived widget when a node is touched and is to be used for
|
||||
selection. Depending on the keyboard keys pressed and the
|
||||
configuration, it could select or deslect this and other nodes in the
|
||||
selectable nodes list, :meth:`get_selectable_nodes`.
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node that received the touch. Can be None for a scroll
|
||||
type touch.
|
||||
`touch`
|
||||
Optionally, the touch. Defaults to None.
|
||||
|
||||
:Returns:
|
||||
bool, True if the touch was used, False otherwise.
|
||||
'''
|
||||
multi = self.multiselect
|
||||
multiselect = multi and (self._ctrl_down or self.touch_multiselect)
|
||||
range_select = multi and self._shift_down
|
||||
|
||||
if touch and 'button' in touch.profile and touch.button in\
|
||||
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
|
||||
node_src, idx_src = self._resolve_last_node()
|
||||
node, idx = self.goto_node(touch.button, node_src, idx_src)
|
||||
if node == node_src:
|
||||
return False
|
||||
if range_select:
|
||||
self._select_range(multiselect, True, node, idx)
|
||||
else:
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
if node is None:
|
||||
return False
|
||||
|
||||
if (node in self.selected_nodes and (not range_select)): # selected
|
||||
if multiselect:
|
||||
self.deselect_node(node)
|
||||
else:
|
||||
selected_node_count = len(self.selected_nodes)
|
||||
self.clear_selection()
|
||||
if not self.touch_deselect_last or selected_node_count > 1:
|
||||
self.select_node(node)
|
||||
elif range_select:
|
||||
# keep anchor only if not multiselect (ctrl-type selection)
|
||||
self._select_range(multiselect, not multiselect, node, 0)
|
||||
else: # it's not selected at this point
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
|
||||
def select_with_key_down(self, keyboard, scancode, codepoint, modifiers,
|
||||
**kwargs):
|
||||
'''Processes a key press. This is called when a key press is to be used
|
||||
for selection. Depending on the keyboard keys pressed and the
|
||||
configuration, it could select or deselect nodes or node ranges
|
||||
from the selectable nodes list, :meth:`get_selectable_nodes`.
|
||||
|
||||
The parameters are such that it could be bound directly to the
|
||||
on_key_down event of a keyboard. Therefore, it is safe to be called
|
||||
repeatedly when the key is held down as is done by the keyboard.
|
||||
|
||||
:Returns:
|
||||
bool, True if the keypress was used, False otherwise.
|
||||
'''
|
||||
if not self.keyboard_select:
|
||||
return False
|
||||
keys = self._key_list
|
||||
multi = self.multiselect
|
||||
node_src, idx_src = self._resolve_last_node()
|
||||
text = scancode[1]
|
||||
|
||||
if text == 'shift':
|
||||
self._shift_down = True
|
||||
elif text in ('ctrl', 'lctrl', 'rctrl'):
|
||||
self._ctrl_down = True
|
||||
elif (multi and 'ctrl' in modifiers and text in ('a', 'A') and
|
||||
text not in keys):
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
select = self.select_node
|
||||
for node in sister_nodes:
|
||||
select(node)
|
||||
keys.append(text)
|
||||
else:
|
||||
s = text
|
||||
if len(text) > 1:
|
||||
d = {'divide': '/', 'mul': '*', 'substract': '-', 'add': '+',
|
||||
'decimal': '.'}
|
||||
if text.startswith('numpad'):
|
||||
s = text[6:]
|
||||
if len(s) > 1:
|
||||
if s in d:
|
||||
s = d[s]
|
||||
else:
|
||||
s = None
|
||||
else:
|
||||
s = None
|
||||
|
||||
if s is not None:
|
||||
if s not in keys: # don't keep adding while holding down
|
||||
if time() - self._last_key_time <= self.text_entry_timeout:
|
||||
self._word_filter += s
|
||||
else:
|
||||
self._word_filter = s
|
||||
keys.append(s)
|
||||
|
||||
self._last_key_time = time()
|
||||
node, idx = self.goto_node(self._word_filter, node_src,
|
||||
idx_src)
|
||||
else:
|
||||
self._word_filter = ''
|
||||
node, idx = self.goto_node(text, node_src, idx_src)
|
||||
|
||||
if node == node_src:
|
||||
return False
|
||||
|
||||
multiselect = multi and 'ctrl' in modifiers
|
||||
if multi and 'shift' in modifiers:
|
||||
self._select_range(multiselect, True, node, idx)
|
||||
else:
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
self.select_node(node)
|
||||
return True
|
||||
self._word_filter = ''
|
||||
return False
|
||||
|
||||
def select_with_key_up(self, keyboard, scancode, **kwargs):
|
||||
'''(internal) Processes a key release. This must be called by the
|
||||
derived widget when a key that :meth:`select_with_key_down` returned
|
||||
True is released.
|
||||
|
||||
The parameters are such that it could be bound directly to the
|
||||
on_key_up event of a keyboard.
|
||||
|
||||
:Returns:
|
||||
bool, True if the key release was used, False otherwise.
|
||||
'''
|
||||
if scancode[1] == 'shift':
|
||||
self._shift_down = False
|
||||
elif scancode[1] in ('ctrl', 'lctrl', 'rctrl'):
|
||||
self._ctrl_down = False
|
||||
else:
|
||||
try:
|
||||
self._key_list.remove(scancode[1])
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _update_counts(self, *largs):
|
||||
# doesn't invert indices here
|
||||
pc = self.page_count
|
||||
uc = self.up_count
|
||||
rc = self.right_count
|
||||
sc = self.scroll_count
|
||||
self._offset_counts = {'pageup': -pc, 'pagedown': pc, 'up': -uc,
|
||||
'down': uc, 'right': rc, 'left': -rc, 'scrollup': sc,
|
||||
'scrolldown': -sc, 'scrollright': -sc, 'scrollleft': sc}
|
||||
|
||||
def _resolve_last_node(self):
|
||||
# for offset selection, we have a anchor, and we select everything
|
||||
# between anchor and added offset relative to last node
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
if not len(sister_nodes):
|
||||
return None, 0
|
||||
last_node = self._last_selected_node
|
||||
last_idx = self._last_node_idx
|
||||
end = len(sister_nodes) - 1
|
||||
|
||||
if last_node is None:
|
||||
last_node = self._anchor
|
||||
last_idx = self._anchor_idx
|
||||
if last_node is None:
|
||||
return sister_nodes[end], end
|
||||
if last_idx > end or sister_nodes[last_idx] != last_node:
|
||||
try:
|
||||
return last_node, self.get_index_of_node(last_node,
|
||||
sister_nodes)
|
||||
except ValueError:
|
||||
return sister_nodes[end], end
|
||||
return last_node, last_idx
|
||||
|
||||
def _select_range(self, multiselect, keep_anchor, node, idx):
|
||||
'''Selects a range between self._anchor and node or idx.
|
||||
If multiselect is True, it will be added to the selection, otherwise
|
||||
it will unselect everything before selecting the range. This is only
|
||||
called if self.multiselect is True.
|
||||
If keep anchor is False, the anchor is moved to node. This should
|
||||
always be True for keyboard selection.
|
||||
'''
|
||||
select = self.select_node
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
end = len(sister_nodes) - 1
|
||||
last_node = self._anchor
|
||||
last_idx = self._anchor_idx
|
||||
|
||||
if last_node is None:
|
||||
last_idx = end
|
||||
last_node = sister_nodes[end]
|
||||
else:
|
||||
if last_idx > end or sister_nodes[last_idx] != last_node:
|
||||
try:
|
||||
last_idx = self.get_index_of_node(last_node, sister_nodes)
|
||||
except ValueError:
|
||||
# list changed - cannot do select across them
|
||||
return
|
||||
if idx > end or sister_nodes[idx] != node:
|
||||
try: # just in case
|
||||
idx = self.get_index_of_node(node, sister_nodes)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if last_idx > idx:
|
||||
last_idx, idx = idx, last_idx
|
||||
if not multiselect:
|
||||
self.clear_selection()
|
||||
for item in sister_nodes[last_idx:idx + 1]:
|
||||
select(item)
|
||||
|
||||
if keep_anchor:
|
||||
self._anchor = last_node
|
||||
self._anchor_idx = last_idx
|
||||
else:
|
||||
self._anchor = node # in case idx was reversed, reset
|
||||
self._anchor_idx = idx
|
||||
self._last_selected_node = node
|
||||
self._last_node_idx = idx
|
||||
|
||||
def clear_selection(self):
|
||||
''' Deselects all the currently selected nodes.
|
||||
'''
|
||||
# keep the anchor and last selected node
|
||||
deselect = self.deselect_node
|
||||
nodes = self.selected_nodes
|
||||
# empty beforehand so lookup in deselect will be fast
|
||||
for node in nodes[:]:
|
||||
deselect(node)
|
||||
|
||||
def get_selectable_nodes(self):
|
||||
'''(internal) Returns a list of the nodes that can be selected. It can
|
||||
be overwritten by the derived widget to return the correct list.
|
||||
|
||||
This list is used to determine which nodes to select with group
|
||||
selection. E.g. the last element in the list will be selected when
|
||||
home is pressed, pagedown will move (or add to, if shift is held) the
|
||||
selection from the current position by negative :attr:`page_count`
|
||||
nodes starting from the position of the currently selected node in
|
||||
this list and so on. Still, nodes can be selected even if they are not
|
||||
in this list.
|
||||
|
||||
.. note::
|
||||
|
||||
It is safe to dynamically change this list including removing,
|
||||
adding, or re-arranging its elements. Nodes can be selected even
|
||||
if they are not on this list. And selected nodes removed from the
|
||||
list will remain selected until :meth:`deselect_node` is called.
|
||||
|
||||
.. warning::
|
||||
|
||||
Layouts display their children in the reverse order. That is, the
|
||||
contents of :attr:`~kivy.uix.widget.Widget.children` is displayed
|
||||
form right to left, bottom to top. Therefore, internally, the
|
||||
indices of the elements returned by this function are reversed to
|
||||
make it work by default for most layouts so that the final result
|
||||
is consistent e.g. home, although it will select the last element
|
||||
in this list visually, will select the first element when
|
||||
counting from top to bottom and left to right. If this behavior is
|
||||
not desired, a reversed list should be returned instead.
|
||||
|
||||
Defaults to returning :attr:`~kivy.uix.widget.Widget.children`.
|
||||
'''
|
||||
return self.children
|
||||
|
||||
def get_index_of_node(self, node, selectable_nodes):
|
||||
'''(internal) Returns the index of the `node` within the
|
||||
`selectable_nodes` returned by :meth:`get_selectable_nodes`.
|
||||
'''
|
||||
return selectable_nodes.index(node)
|
||||
|
||||
def goto_node(self, key, last_node, last_node_idx):
|
||||
'''(internal) Used by the controller to get the node at the position
|
||||
indicated by key. The key can be keyboard inputs, e.g. pageup,
|
||||
or scroll inputs from the mouse scroll wheel, e.g. scrollup.
|
||||
'last_node' is the last node selected and is used to find the resulting
|
||||
node. For example, if the key is up, the returned node is one node
|
||||
up from the last node.
|
||||
|
||||
It can be overwritten by the derived widget.
|
||||
|
||||
:Parameters:
|
||||
`key`
|
||||
str, the string used to find the desired node. It can be any
|
||||
of the keyboard keys, as well as the mouse scrollup,
|
||||
scrolldown, scrollright, and scrollleft strings. If letters
|
||||
are typed in quick succession, the letters will be combined
|
||||
before it's passed in as key and can be used to find nodes that
|
||||
have an associated string that starts with those letters.
|
||||
`last_node`
|
||||
The last node that was selected.
|
||||
`last_node_idx`
|
||||
The cached index of the last node selected in the
|
||||
:meth:`get_selectable_nodes` list. If the list hasn't changed
|
||||
it saves having to look up the index of `last_node` in that
|
||||
list.
|
||||
|
||||
:Returns:
|
||||
tuple, the node targeted by key and its index in the
|
||||
:meth:`get_selectable_nodes` list. Returning
|
||||
`(last_node, last_node_idx)` indicates a node wasn't found.
|
||||
'''
|
||||
sister_nodes = self.get_selectable_nodes()
|
||||
end = len(sister_nodes) - 1
|
||||
counts = self._offset_counts
|
||||
if end == -1:
|
||||
return last_node, last_node_idx
|
||||
if last_node_idx > end or sister_nodes[last_node_idx] != last_node:
|
||||
try: # just in case
|
||||
last_node_idx = self.get_index_of_node(last_node, sister_nodes)
|
||||
except ValueError:
|
||||
return last_node, last_node_idx
|
||||
|
||||
is_reversed = self.nodes_order_reversed
|
||||
if key in counts:
|
||||
count = -counts[key] if is_reversed else counts[key]
|
||||
idx = max(min(count + last_node_idx, end), 0)
|
||||
return sister_nodes[idx], idx
|
||||
elif key == 'home':
|
||||
if is_reversed:
|
||||
return sister_nodes[end], end
|
||||
return sister_nodes[0], 0
|
||||
elif key == 'end':
|
||||
if is_reversed:
|
||||
return sister_nodes[0], 0
|
||||
return sister_nodes[end], end
|
||||
else:
|
||||
return last_node, last_node_idx
|
||||
|
||||
def select_node(self, node):
|
||||
''' Selects a node.
|
||||
|
||||
It is called by the controller when it selects a node and can be
|
||||
called from the outside to select a node directly. The derived widget
|
||||
should overwrite this method and change the node state to selected
|
||||
when called.
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node to be selected.
|
||||
|
||||
:Returns:
|
||||
bool, True if the node was selected, False otherwise.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method must be called by the derived widget using super if it
|
||||
is overwritten.
|
||||
'''
|
||||
nodes = self.selected_nodes
|
||||
if node in nodes:
|
||||
return False
|
||||
|
||||
if (not self.multiselect) and len(nodes):
|
||||
self.clear_selection()
|
||||
if node not in nodes:
|
||||
nodes.append(node)
|
||||
self._anchor = node
|
||||
self._last_selected_node = node
|
||||
return True
|
||||
|
||||
def deselect_node(self, node):
|
||||
''' Deselects a possibly selected node.
|
||||
|
||||
It is called by the controller when it deselects a node and can also
|
||||
be called from the outside to deselect a node directly. The derived
|
||||
widget should overwrite this method and change the node to its
|
||||
unselected state when this is called
|
||||
|
||||
:Parameters:
|
||||
`node`
|
||||
The node to be deselected.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method must be called by the derived widget using super if it
|
||||
is overwritten.
|
||||
'''
|
||||
try:
|
||||
self.selected_nodes.remove(node)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
|
@ -0,0 +1,160 @@
|
|||
'''
|
||||
Cover Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.cover.CoverBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ is intended for rendering
|
||||
textures to full widget size keeping the aspect ratio of the original texture.
|
||||
|
||||
Use cases are i.e. rendering full size background images or video content in
|
||||
a dynamic layout.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following examples add cover behavior to an image:
|
||||
|
||||
In python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import CoverBehavior
|
||||
from kivy.uix.image import Image
|
||||
|
||||
|
||||
class CoverImage(CoverBehavior, Image):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverImage, self).__init__(**kwargs)
|
||||
texture = self._coreimage.texture
|
||||
self.reference_size = texture.size
|
||||
self.texture = texture
|
||||
|
||||
|
||||
class MainApp(App):
|
||||
|
||||
def build(self):
|
||||
return CoverImage(source='image.jpg')
|
||||
|
||||
MainApp().run()
|
||||
|
||||
In Kivy Language:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
CoverImage:
|
||||
source: 'image.png'
|
||||
|
||||
<CoverImage@CoverBehavior+Image>:
|
||||
reference_size: self.texture_size
|
||||
|
||||
See :class:`~kivy.uix.behaviors.cover.CoverBehavior` for details.
|
||||
'''
|
||||
|
||||
__all__ = ('CoverBehavior', )
|
||||
|
||||
from decimal import Decimal
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ListProperty
|
||||
|
||||
|
||||
Builder.load_string("""
|
||||
<-CoverBehavior>:
|
||||
canvas.before:
|
||||
StencilPush
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilUse
|
||||
canvas:
|
||||
Rectangle:
|
||||
texture: self.texture
|
||||
size: self.cover_size
|
||||
pos: self.cover_pos
|
||||
canvas.after:
|
||||
StencilUnUse
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
StencilPop
|
||||
""")
|
||||
|
||||
|
||||
class CoverBehavior(object):
|
||||
'''The CoverBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
provides rendering a texture covering full widget size keeping aspect ratio
|
||||
of the original texture.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
reference_size = ListProperty([])
|
||||
'''Reference size used for aspect ratio approximation calculation.
|
||||
|
||||
:attr:`reference_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[]`.
|
||||
'''
|
||||
|
||||
cover_size = ListProperty([0, 0])
|
||||
'''Size of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_size` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
cover_pos = ListProperty([0, 0])
|
||||
'''Position of the aspect ratio aware texture. Gets calculated in
|
||||
``CoverBehavior.calculate_cover``.
|
||||
|
||||
:attr:`cover_pos` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to `[0, 0]`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CoverBehavior, self).__init__(**kwargs)
|
||||
# bind covering
|
||||
self.bind(
|
||||
size=self.calculate_cover,
|
||||
pos=self.calculate_cover
|
||||
)
|
||||
|
||||
def _aspect_ratio_approximate(self, size):
|
||||
# return a decimal approximation of an aspect ratio.
|
||||
return Decimal('%.2f' % (float(size[0]) / size[1]))
|
||||
|
||||
def _scale_size(self, size, sizer):
|
||||
# return scaled size based on sizer, where sizer (n, None) scales x
|
||||
# to n and (None, n) scales y to n
|
||||
size_new = list(sizer)
|
||||
i = size_new.index(None)
|
||||
j = i * -1 + 1
|
||||
size_new[i] = (size_new[j] * size[i]) / size[j]
|
||||
return tuple(size_new)
|
||||
|
||||
def calculate_cover(self, *args):
|
||||
# return if no reference size yet
|
||||
if not self.reference_size:
|
||||
return
|
||||
size = self.size
|
||||
origin_appr = self._aspect_ratio_approximate(self.reference_size)
|
||||
crop_appr = self._aspect_ratio_approximate(size)
|
||||
# same aspect ratio
|
||||
if origin_appr == crop_appr:
|
||||
crop_size = self.size
|
||||
offset = (0, 0)
|
||||
# scale x
|
||||
elif origin_appr < crop_appr:
|
||||
crop_size = self._scale_size(self.reference_size, (size[0], None))
|
||||
offset = (0, ((crop_size[1] - size[1]) / 2) * -1)
|
||||
# scale y
|
||||
else:
|
||||
crop_size = self._scale_size(self.reference_size, (None, size[1]))
|
||||
offset = (((crop_size[0] - size[0]) / 2) * -1, 0)
|
||||
# set background size and position
|
||||
self.cover_size = crop_size
|
||||
self.cover_pos = offset
|
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
Drag Behavior
|
||||
=============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.drag.DragBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides Drag behavior.
|
||||
When combined with a widget, dragging in the rectangle defined by the
|
||||
:attr:`~kivy.uix.behaviors.drag.DragBehavior.drag_rectangle` will drag the
|
||||
widget.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example creates a draggable label::
|
||||
|
||||
from kivy.uix.label import Label
|
||||
from kivy.app import App
|
||||
from kivy.uix.behaviors import DragBehavior
|
||||
from kivy.lang import Builder
|
||||
|
||||
# You could also put the following in your kv file...
|
||||
kv = '''
|
||||
<DragLabel>:
|
||||
# Define the properties for the DragLabel
|
||||
drag_rectangle: self.x, self.y, self.width, self.height
|
||||
drag_timeout: 10000000
|
||||
drag_distance: 0
|
||||
|
||||
FloatLayout:
|
||||
# Define the root widget
|
||||
DragLabel:
|
||||
size_hint: 0.25, 0.2
|
||||
text: 'Drag me'
|
||||
'''
|
||||
|
||||
|
||||
class DragLabel(DragBehavior, Label):
|
||||
pass
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return Builder.load_string(kv)
|
||||
|
||||
TestApp().run()
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ('DragBehavior', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import NumericProperty, ReferenceListProperty
|
||||
from kivy.config import Config
|
||||
from kivy.metrics import sp
|
||||
from functools import partial
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_scroll_timeout = _scroll_distance = 0
|
||||
if Config:
|
||||
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
|
||||
_scroll_distance = Config.getint('widgets', 'scroll_distance')
|
||||
|
||||
|
||||
class DragBehavior(object):
|
||||
'''
|
||||
The DragBehavior `mixin <https://en.wikipedia.org/wiki/Mixin>`_ provides
|
||||
Drag behavior. When combined with a widget, dragging in the rectangle
|
||||
defined by :attr:`drag_rectangle` will drag the widget. Please see
|
||||
the :mod:`drag behaviors module <kivy.uix.behaviors.drag>` documentation
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
drag_distance = NumericProperty(_scroll_distance)
|
||||
'''Distance to move before dragging the :class:`DragBehavior`, in pixels.
|
||||
As soon as the distance has been traveled, the :class:`DragBehavior` will
|
||||
start to drag, and no touch event will be dispatched to the children.
|
||||
It is advisable that you base this value on the dpi of your target device's
|
||||
screen.
|
||||
|
||||
:attr:`drag_distance` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_distance` as defined in the user
|
||||
:class:`~kivy.config.Config` (20 pixels by default).
|
||||
'''
|
||||
|
||||
drag_timeout = NumericProperty(_scroll_timeout)
|
||||
'''Timeout allowed to trigger the :attr:`drag_distance`, in milliseconds.
|
||||
If the user has not moved :attr:`drag_distance` within the timeout,
|
||||
dragging will be disabled, and the touch event will be dispatched to the
|
||||
children.
|
||||
|
||||
:attr:`drag_timeout` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to the `scroll_timeout` as defined in the user
|
||||
:class:`~kivy.config.Config` (55 milliseconds by default).
|
||||
'''
|
||||
|
||||
drag_rect_x = NumericProperty(0)
|
||||
'''X position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_x` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_y = NumericProperty(0)
|
||||
'''Y position of the axis aligned bounding rectangle where dragging
|
||||
is allowed (in window coordinates).
|
||||
|
||||
:attr:`drag_rect_Y` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
drag_rect_width = NumericProperty(100)
|
||||
'''Width of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rect_height = NumericProperty(100)
|
||||
'''Height of the axis aligned bounding rectangle where dragging is allowed.
|
||||
|
||||
:attr:`drag_rect_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 100.
|
||||
'''
|
||||
|
||||
drag_rectangle = ReferenceListProperty(drag_rect_x, drag_rect_y,
|
||||
drag_rect_width, drag_rect_height)
|
||||
'''Position and size of the axis aligned bounding rectangle where dragging
|
||||
is allowed.
|
||||
|
||||
:attr:`drag_rectangle` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`drag_rect_x`, :attr:`drag_rect_y`, :attr:`drag_rect_width`,
|
||||
:attr:`drag_rect_height`) properties.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._drag_touch = None
|
||||
super(DragBehavior, self).__init__(**kwargs)
|
||||
|
||||
def _get_uid(self, prefix='sv'):
|
||||
return '{0}.{1}'.format(prefix, self.uid)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
xx, yy, w, h = self.drag_rectangle
|
||||
x, y = touch.pos
|
||||
if not self.collide_point(x, y):
|
||||
touch.ud[self._get_uid('svavoid')] = True
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
if self._drag_touch or ('button' in touch.profile and
|
||||
touch.button.startswith('scroll')) or\
|
||||
not ((xx < x <= xx + w) and (yy < y <= yy + h)):
|
||||
return super(DragBehavior, self).on_touch_down(touch)
|
||||
|
||||
# no mouse scrolling, so the user is going to drag with this touch.
|
||||
self._drag_touch = touch
|
||||
uid = self._get_uid()
|
||||
touch.grab(self)
|
||||
touch.ud[uid] = {
|
||||
'mode': 'unknown',
|
||||
'dx': 0,
|
||||
'dy': 0}
|
||||
Clock.schedule_once(self._change_touch_mode,
|
||||
self.drag_timeout / 1000.)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud or\
|
||||
self._drag_touch is not touch:
|
||||
return super(DragBehavior, self).on_touch_move(touch) or\
|
||||
self._get_uid() in touch.ud
|
||||
if touch.grab_current is not self:
|
||||
return True
|
||||
|
||||
uid = self._get_uid()
|
||||
ud = touch.ud[uid]
|
||||
mode = ud['mode']
|
||||
if mode == 'unknown':
|
||||
ud['dx'] += abs(touch.dx)
|
||||
ud['dy'] += abs(touch.dy)
|
||||
if ud['dx'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
if ud['dy'] > sp(self.drag_distance):
|
||||
mode = 'drag'
|
||||
ud['mode'] = mode
|
||||
if mode == 'drag':
|
||||
self.x += touch.dx
|
||||
self.y += touch.dy
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self._get_uid('svavoid') in touch.ud:
|
||||
return super(DragBehavior, self).on_touch_up(touch)
|
||||
|
||||
if self._drag_touch and self in [x() for x in touch.grab_list]:
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
ud = touch.ud[self._get_uid()]
|
||||
if ud['mode'] == 'unknown':
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
|
||||
else:
|
||||
if self._drag_touch is not touch:
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
|
||||
def _do_touch_up(self, touch, *largs):
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
# don't forget about grab event!
|
||||
for x in touch.grab_list[:]:
|
||||
touch.grab_list.remove(x)
|
||||
x = x()
|
||||
if not x:
|
||||
continue
|
||||
touch.grab_current = x
|
||||
super(DragBehavior, self).on_touch_up(touch)
|
||||
touch.grab_current = None
|
||||
|
||||
def _change_touch_mode(self, *largs):
|
||||
if not self._drag_touch:
|
||||
return
|
||||
uid = self._get_uid()
|
||||
touch = self._drag_touch
|
||||
ud = touch.ud[uid]
|
||||
if ud['mode'] != 'unknown':
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self._drag_touch = None
|
||||
touch.push()
|
||||
touch.apply_transform_2d(self.parent.to_widget)
|
||||
super(DragBehavior, self).on_touch_down(touch)
|
||||
touch.pop()
|
||||
return
|
|
@ -0,0 +1,140 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
Emacs Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.emacs.EmacsBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ allows you to add
|
||||
`Emacs <https://www.gnu.org/software/emacs/>`_ keyboard shortcuts for basic
|
||||
movement and editing to the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
The shortcuts currently available are listed below:
|
||||
|
||||
Emacs shortcuts
|
||||
---------------
|
||||
=============== ========================================================
|
||||
Shortcut Description
|
||||
--------------- --------------------------------------------------------
|
||||
Control + a Move cursor to the beginning of the line
|
||||
Control + e Move cursor to the end of the line
|
||||
Control + f Move cursor one character to the right
|
||||
Control + b Move cursor one character to the left
|
||||
Alt + f Move cursor to the end of the word to the right
|
||||
Alt + b Move cursor to the start of the word to the left
|
||||
Alt + Backspace Delete text left of the cursor to the beginning of word
|
||||
Alt + d Delete text right of the cursor to the end of the word
|
||||
Alt + w Copy selection
|
||||
Control + w Cut selection
|
||||
Control + y Paste selection
|
||||
=============== ========================================================
|
||||
|
||||
.. warning::
|
||||
If you have the :mod:`~kivy.modules.inspector` module enabled, the
|
||||
shortcut for opening the inspector (Control + e) conflicts with the
|
||||
Emacs shortcut to move to the end of the line (it will still move the
|
||||
cursor to the end of the line, but the inspector will open as well).
|
||||
'''
|
||||
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
|
||||
__all__ = ('EmacsBehavior', )
|
||||
|
||||
|
||||
class EmacsBehavior(object):
|
||||
'''
|
||||
A `mixin <https://en.wikipedia.org/wiki/Mixin>`_ that enables Emacs-style
|
||||
keyboard shortcuts for the :class:`~kivy.uix.textinput.TextInput` widget.
|
||||
Please see the :mod:`Emacs behaviors module <kivy.uix.behaviors.emacs>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
|
||||
key_bindings = StringProperty('emacs')
|
||||
'''String name which determines the type of key bindings to use with the
|
||||
:class:`~kivy.uix.textinput.TextInput`. This allows Emacs key bindings to
|
||||
be enabled/disabled programmatically for widgets that inherit from
|
||||
:class:`EmacsBehavior`. If the value is not ``'emacs'``, Emacs bindings
|
||||
will be disabled. Use ``'default'`` for switching to the default key
|
||||
bindings of TextInput.
|
||||
|
||||
:attr:`key_bindings` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to ``'emacs'``.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EmacsBehavior, self).__init__(**kwargs)
|
||||
|
||||
self.bindings = {
|
||||
'ctrl': {
|
||||
'a': lambda: self.do_cursor_movement('cursor_home'),
|
||||
'e': lambda: self.do_cursor_movement('cursor_end'),
|
||||
'f': lambda: self.do_cursor_movement('cursor_right'),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left'),
|
||||
'w': lambda: self._cut(self.selection_text),
|
||||
'y': self.paste,
|
||||
},
|
||||
'alt': {
|
||||
'w': self.copy,
|
||||
'f': lambda: self.do_cursor_movement('cursor_right',
|
||||
control=True),
|
||||
'b': lambda: self.do_cursor_movement('cursor_left',
|
||||
control=True),
|
||||
'd': self.delete_word_right,
|
||||
'\x08': self.delete_word_left, # alt + backspace
|
||||
},
|
||||
}
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
|
||||
key, key_str = keycode
|
||||
|
||||
# join the modifiers e.g. ['alt', 'ctrl']
|
||||
mod = '+'.join(modifiers) if modifiers else None
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if key in range(256) and self.key_bindings == 'emacs':
|
||||
if mod == 'ctrl' and chr(key) in self.bindings['ctrl'].keys():
|
||||
is_emacs_shortcut = True
|
||||
elif mod == 'alt' and chr(key) in self.bindings['alt'].keys():
|
||||
is_emacs_shortcut = True
|
||||
else: # e.g. ctrl+alt or alt+ctrl (alt-gr key)
|
||||
is_emacs_shortcut = False
|
||||
|
||||
if is_emacs_shortcut:
|
||||
# Look up mod and key
|
||||
emacs_shortcut = self.bindings[mod][chr(key)]
|
||||
emacs_shortcut()
|
||||
else:
|
||||
super(EmacsBehavior, self).keyboard_on_key_down(window, keycode,
|
||||
text, modifiers)
|
||||
|
||||
def delete_word_right(self):
|
||||
'''Delete text right of the cursor to the end of the word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
start_cursor = self.cursor
|
||||
self.do_cursor_movement('cursor_right', control=True)
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[start_index:end_index]
|
||||
self._set_unredo_delsel(start_index, end_index, s, from_undo=False)
|
||||
self.text = self.text[:start_index] + self.text[end_index:]
|
||||
self._set_cursor(pos=start_cursor)
|
||||
|
||||
def delete_word_left(self):
|
||||
'''Delete text left of the cursor to the beginning of word'''
|
||||
if self._selection:
|
||||
return
|
||||
start_index = self.cursor_index()
|
||||
self.do_cursor_movement('cursor_left', control=True)
|
||||
end_cursor = self.cursor
|
||||
end_index = self.cursor_index()
|
||||
if start_index != end_index:
|
||||
s = self.text[end_index:start_index]
|
||||
self._set_unredo_delsel(end_index, start_index, s, from_undo=False)
|
||||
self.text = self.text[:end_index] + self.text[start_index:]
|
||||
self._set_cursor(pos=end_cursor)
|
|
@ -0,0 +1,595 @@
|
|||
'''
|
||||
Focus Behavior
|
||||
==============
|
||||
|
||||
The :class:`~kivy.uix.behaviors.FocusBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
keyboard focus behavior. When combined with other
|
||||
FocusBehavior widgets it allows one to cycle focus among them by pressing
|
||||
tab. In addition, upon gaining focus, the instance will automatically
|
||||
receive keyboard input.
|
||||
|
||||
Focus, very different from selection, is intimately tied with the keyboard;
|
||||
each keyboard can focus on zero or one widgets, and each widget can only
|
||||
have the focus of one keyboard. However, multiple keyboards can focus
|
||||
simultaneously on different widgets. When escape is hit, the widget having
|
||||
the focus of that keyboard will de-focus.
|
||||
|
||||
Managing focus
|
||||
--------------
|
||||
|
||||
In essence, focus is implemented as a doubly linked list, where each
|
||||
node holds a (weak) reference to the instance before it and after it,
|
||||
as visualized when cycling through the nodes using tab (forward) or
|
||||
shift+tab (backward). If a previous or next widget is not specified,
|
||||
:attr:`focus_next` and :attr:`focus_previous` defaults to `None`. This
|
||||
means that the :attr:`~kivy.uix.widget.Widget.children` list and
|
||||
:attr:`parents <kivy.uix.widget.Widget.parent>` are
|
||||
walked to find the next focusable widget, unless :attr:`focus_next` or
|
||||
:attr:`focus_previous` is set to the `StopIteration` class, in which case
|
||||
focus stops there.
|
||||
|
||||
For example, to cycle focus between :class:`~kivy.uix.button.Button`
|
||||
elements of a :class:`~kivy.uix.gridlayout.GridLayout`::
|
||||
|
||||
class FocusButton(FocusBehavior, Button):
|
||||
pass
|
||||
|
||||
grid = GridLayout(cols=4)
|
||||
for i in range(40):
|
||||
grid.add_widget(FocusButton(text=str(i)))
|
||||
# clicking on a widget will activate focus, and tab can now be used
|
||||
# to cycle through
|
||||
|
||||
When using a software keyboard, typical on mobile and touch devices, the
|
||||
keyboard display behavior is determined by the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
|
||||
this property to ensure the focused widget is not covered or obscured by the
|
||||
keyboard.
|
||||
|
||||
Initializing focus
|
||||
------------------
|
||||
|
||||
Widgets needs to be visible before they can receive the focus. This means that
|
||||
setting their *focus* property to True before they are visible will have no
|
||||
effect. To initialize focus, you can use the 'on_parent' event::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.textinput import TextInput
|
||||
|
||||
class MyTextInput(TextInput):
|
||||
def on_parent(self, widget, parent):
|
||||
self.focus = True
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyTextInput()
|
||||
|
||||
SampleApp().run()
|
||||
|
||||
If you are using a :class:`~kivy.uix.popup`, you can use the 'on_open' event.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
.. warning::
|
||||
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
'''
|
||||
|
||||
__all__ = ('FocusBehavior', )
|
||||
|
||||
from kivy.properties import OptionProperty, ObjectProperty, BooleanProperty, \
|
||||
AliasProperty
|
||||
from kivy.config import Config
|
||||
from kivy.base import EventLoop
|
||||
|
||||
# When we are generating documentation, Config doesn't exist
|
||||
_is_desktop = False
|
||||
_keyboard_mode = 'system'
|
||||
if Config:
|
||||
_is_desktop = Config.getboolean('kivy', 'desktop')
|
||||
_keyboard_mode = Config.get('kivy', 'keyboard_mode')
|
||||
|
||||
|
||||
class FocusBehavior(object):
|
||||
'''Provides keyboard focus behavior. When combined with other
|
||||
FocusBehavior widgets it allows one to cycle focus among them by pressing
|
||||
tab. Please see the
|
||||
:mod:`focus behavior module documentation <kivy.uix.behaviors.focus>`
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
'''
|
||||
|
||||
_requested_keyboard = False
|
||||
_keyboard = ObjectProperty(None, allownone=True)
|
||||
_keyboards = {}
|
||||
|
||||
ignored_touch = []
|
||||
'''A list of touches that should not be used to defocus. After on_touch_up,
|
||||
every touch that is not in :attr:`ignored_touch` will defocus all the
|
||||
focused widgets if the config keyboard mode is not multi. Touches on
|
||||
focusable widgets that were used to focus are automatically added here.
|
||||
|
||||
Example usage::
|
||||
|
||||
class Unfocusable(Widget):
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
FocusBehavior.ignored_touch.append(touch)
|
||||
|
||||
Notice that you need to access this as a class, not an instance variable.
|
||||
'''
|
||||
|
||||
def _set_keyboard(self, value):
|
||||
focus = self.focus
|
||||
keyboard = self._keyboard
|
||||
keyboards = FocusBehavior._keyboards
|
||||
if keyboard:
|
||||
self.focus = False # this'll unbind
|
||||
if self._keyboard: # remove assigned keyboard from dict
|
||||
del keyboards[keyboard]
|
||||
if value and value not in keyboards:
|
||||
keyboards[value] = None
|
||||
self._keyboard = value
|
||||
self.focus = focus
|
||||
|
||||
def _get_keyboard(self):
|
||||
return self._keyboard
|
||||
keyboard = AliasProperty(_get_keyboard, _set_keyboard,
|
||||
bind=('_keyboard', ))
|
||||
'''The keyboard to bind to (or bound to the widget) when focused.
|
||||
|
||||
When None, a keyboard is requested and released whenever the widget comes
|
||||
into and out of focus. If not None, it must be a keyboard, which gets
|
||||
bound and unbound from the widget whenever it's in or out of focus. It is
|
||||
useful only when more than one keyboard is available, so it is recommended
|
||||
to be set to None when only one keyboard is available.
|
||||
|
||||
If more than one keyboard is available, whenever an instance gets focused
|
||||
a new keyboard will be requested if None. Unless the other instances lose
|
||||
focus (e.g. if tab was used), a new keyboard will appear. When this is
|
||||
undesired, the keyboard property can be used. For example, if there are
|
||||
two users with two keyboards, then each keyboard can be assigned to
|
||||
different groups of instances of FocusBehavior, ensuring that within
|
||||
each group, only one FocusBehavior will have focus, and will receive input
|
||||
from the correct keyboard. See `keyboard_mode` in :mod:`~kivy.config` for
|
||||
more information on the keyboard modes.
|
||||
|
||||
**Keyboard and focus behavior**
|
||||
|
||||
When using the keyboard, there are some important default behaviors you
|
||||
should keep in mind.
|
||||
|
||||
* When Config's `keyboard_mode` is multi, each new touch is considered
|
||||
a touch by a different user and will set the focus (if clicked on a
|
||||
focusable) with a new keyboard. Already focused elements will not lose
|
||||
their focus (even if an unfocusable widget is touched).
|
||||
|
||||
* If the keyboard property is set, that keyboard will be used when the
|
||||
instance gets focused. If widgets with different keyboards are linked
|
||||
through :attr:`focus_next` and :attr:`focus_previous`, then as they are
|
||||
tabbed through, different keyboards will become active. Therefore,
|
||||
typically it's undesirable to link instances which are assigned
|
||||
different keyboards.
|
||||
|
||||
* When a widget has focus, setting its keyboard to None will remove its
|
||||
keyboard, but the widget will then immediately try to get
|
||||
another keyboard. In order to remove its keyboard, rather set its
|
||||
:attr:`focus` to False.
|
||||
|
||||
* When using a software keyboard, typical on mobile and touch devices, the
|
||||
keyboard display behavior is determined by the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
|
||||
this property to ensure the focused widget is not covered or obscured.
|
||||
|
||||
:attr:`keyboard` is an :class:`~kivy.properties.AliasProperty` and defaults
|
||||
to None.
|
||||
|
||||
.. warning:
|
||||
|
||||
When assigning a keyboard, the keyboard must not be released while
|
||||
it is still assigned to an instance. Similarly, the keyboard created
|
||||
by the instance on focus and assigned to :attr:`keyboard` if None,
|
||||
will be released by the instance when the instance loses focus.
|
||||
Therefore, it is not safe to assign this keyboard to another instance's
|
||||
:attr:`keyboard`.
|
||||
'''
|
||||
|
||||
is_focusable = BooleanProperty(_is_desktop)
|
||||
'''Whether the instance can become focused. If focused, it'll lose focus
|
||||
when set to False.
|
||||
|
||||
:attr:`is_focusable` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True on a desktop (i.e. `desktop` is True in
|
||||
:mod:`~kivy.config`), False otherwise.
|
||||
'''
|
||||
|
||||
focus = BooleanProperty(False)
|
||||
'''Whether the instance currently has focus.
|
||||
|
||||
Setting it to True will bind to and/or request the keyboard, and input
|
||||
will be forwarded to the instance. Setting it to False will unbind
|
||||
and/or release the keyboard. For a given keyboard, only one widget can
|
||||
have its focus, so focusing one will automatically unfocus the other
|
||||
instance holding its focus.
|
||||
|
||||
When using a software keyboard, please refer to the
|
||||
:attr:`~kivy.core.window.WindowBase.softinput_mode` property to determine
|
||||
how the keyboard display is handled.
|
||||
|
||||
:attr:`focus` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
focused = focus
|
||||
'''An alias of :attr:`focus`.
|
||||
|
||||
:attr:`focused` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
|
||||
.. warning::
|
||||
:attr:`focused` is an alias of :attr:`focus` and will be removed in
|
||||
2.0.0.
|
||||
'''
|
||||
|
||||
keyboard_suggestions = BooleanProperty(True)
|
||||
'''If True provides auto suggestions on top of keyboard.
|
||||
This will only work if :attr:`input_type` is set to `text`, `url`, `mail` or
|
||||
`address`.
|
||||
|
||||
.. warning::
|
||||
On Android, `keyboard_suggestions` relies on
|
||||
`InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS` to work, but some keyboards
|
||||
just ignore this flag. If you want to disable suggestions at all on
|
||||
Android, you can set `input_type` to `null`, which will request the
|
||||
input method to run in a limited "generate key events" mode.
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
:attr:`keyboard_suggestions` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to True
|
||||
'''
|
||||
|
||||
def _set_on_focus_next(self, instance, value):
|
||||
'''If changing focus, ensure your code does not create an infinite loop.
|
||||
eg:
|
||||
```python
|
||||
widget.focus_next = widget
|
||||
widget.focus_previous = widget
|
||||
```
|
||||
'''
|
||||
next_ = self._old_focus_next
|
||||
if next_ is value: # prevent infinite loop
|
||||
return
|
||||
|
||||
if isinstance(next_, FocusBehavior):
|
||||
next_.focus_previous = None
|
||||
self._old_focus_next = value
|
||||
if value is None or value is StopIteration:
|
||||
return
|
||||
if not isinstance(value, FocusBehavior):
|
||||
raise ValueError('focus_next accepts only objects based on'
|
||||
' FocusBehavior, or the `StopIteration` class.')
|
||||
value.focus_previous = self
|
||||
|
||||
focus_next = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`FocusBehavior` instance to acquire focus when
|
||||
tab is pressed and this instance has focus, if not `None` or
|
||||
`StopIteration`.
|
||||
|
||||
When tab is pressed, focus cycles through all the :class:`FocusBehavior`
|
||||
widgets that are linked through :attr:`focus_next` and are focusable. If
|
||||
:attr:`focus_next` is `None`, it instead walks the children lists to find
|
||||
the next focusable widget. Finally, if :attr:`focus_next` is
|
||||
the `StopIteration` class, focus won't move forward, but end here.
|
||||
|
||||
.. note:
|
||||
|
||||
Setting :attr:`focus_next` automatically sets :attr:`focus_previous`
|
||||
of the other instance to point to this instance, if not None or
|
||||
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
|
||||
also sets the :attr:`focus_previous` property of the instance
|
||||
previously in :attr:`focus_next` to `None`. Therefore, it is only
|
||||
required to set one of the :attr:`focus_previous` or
|
||||
:attr:`focus_next` links since the other side will be set
|
||||
automatically.
|
||||
|
||||
:attr:`focus_next` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
def _set_on_focus_previous(self, instance, value):
|
||||
prev = self._old_focus_previous
|
||||
if prev is value:
|
||||
return
|
||||
|
||||
if isinstance(prev, FocusBehavior):
|
||||
prev.focus_next = None
|
||||
self._old_focus_previous = value
|
||||
if value is None or value is StopIteration:
|
||||
return
|
||||
if not isinstance(value, FocusBehavior):
|
||||
raise ValueError('focus_previous accepts only objects based'
|
||||
'on FocusBehavior, or the `StopIteration` class.')
|
||||
value.focus_next = self
|
||||
|
||||
focus_previous = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`FocusBehavior` instance to acquire focus when
|
||||
shift+tab is pressed on this instance, if not None or `StopIteration`.
|
||||
|
||||
When shift+tab is pressed, focus cycles through all the
|
||||
:class:`FocusBehavior` widgets that are linked through
|
||||
:attr:`focus_previous` and are focusable. If :attr:`focus_previous` is
|
||||
`None`, it instead walks the children tree to find the
|
||||
previous focusable widget. Finally, if :attr:`focus_previous` is the
|
||||
`StopIteration` class, focus won't move backward, but end here.
|
||||
|
||||
.. note:
|
||||
|
||||
Setting :attr:`focus_previous` automatically sets :attr:`focus_next`
|
||||
of the other instance to point to this instance, if not None or
|
||||
`StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
|
||||
also sets the :attr:`focus_next` property of the instance previously in
|
||||
:attr:`focus_previous` to `None`. Therefore, it is only required
|
||||
to set one of the :attr:`focus_previous` or :attr:`focus_next`
|
||||
links since the other side will be set automatically.
|
||||
|
||||
:attr:`focus_previous` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
keyboard_mode = OptionProperty('auto', options=('auto', 'managed'))
|
||||
'''Determines how the keyboard visibility should be managed. 'auto' will
|
||||
result in the standard behavior of showing/hiding on focus. 'managed'
|
||||
requires setting the keyboard visibility manually, or calling the helper
|
||||
functions :meth:`show_keyboard` and :meth:`hide_keyboard`.
|
||||
|
||||
:attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and
|
||||
defaults to 'auto'. Can be one of 'auto' or 'managed'.
|
||||
'''
|
||||
|
||||
input_type = OptionProperty('null', options=('null', 'text', 'number',
|
||||
'url', 'mail', 'datetime',
|
||||
'tel', 'address'))
|
||||
'''The kind of input keyboard to request.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. versionchanged:: 2.1.0
|
||||
Changed default value from `text` to `null`. Added `null` to options.
|
||||
|
||||
.. warning::
|
||||
As the default value has been changed, you may need to adjust
|
||||
`input_type` in your code.
|
||||
|
||||
:attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and
|
||||
defaults to 'null'. Can be one of 'null', 'text', 'number', 'url', 'mail',
|
||||
'datetime', 'tel' or 'address'.
|
||||
'''
|
||||
|
||||
unfocus_on_touch = BooleanProperty(_keyboard_mode not in
|
||||
('multi', 'systemandmulti'))
|
||||
'''Whether a instance should lose focus when clicked outside the instance.
|
||||
|
||||
When a user clicks on a widget that is focus aware and shares the same
|
||||
keyboard as this widget (which in the case with only one keyboard),
|
||||
then as the other widgets gain focus, this widget loses focus. In addition
|
||||
to that, if this property is `True`, clicking on any widget other than this
|
||||
widget, will remove focus from this widget.
|
||||
|
||||
:attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False` if the `keyboard_mode` in :attr:`~kivy.config.Config`
|
||||
is `'multi'` or `'systemandmulti'`, otherwise it defaults to `True`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._old_focus_next = None
|
||||
self._old_focus_previous = None
|
||||
super(FocusBehavior, self).__init__(**kwargs)
|
||||
|
||||
self._keyboard_mode = _keyboard_mode
|
||||
fbind = self.fbind
|
||||
fbind('focus', self._on_focus)
|
||||
fbind('disabled', self._on_focusable)
|
||||
fbind('is_focusable', self._on_focusable)
|
||||
fbind('focus_next', self._set_on_focus_next)
|
||||
fbind('focus_previous', self._set_on_focus_previous)
|
||||
|
||||
def _on_focusable(self, instance, value):
|
||||
if self.disabled or not self.is_focusable:
|
||||
self.focus = False
|
||||
|
||||
def _on_focus(self, instance, value, *largs):
|
||||
if self.keyboard_mode == 'auto':
|
||||
if value:
|
||||
self._bind_keyboard()
|
||||
else:
|
||||
self._unbind_keyboard()
|
||||
|
||||
def _ensure_keyboard(self):
|
||||
if self._keyboard is None:
|
||||
self._requested_keyboard = True
|
||||
keyboard = self._keyboard = EventLoop.window.request_keyboard(
|
||||
self._keyboard_released,
|
||||
self,
|
||||
input_type=self.input_type,
|
||||
keyboard_suggestions=self.keyboard_suggestions,
|
||||
)
|
||||
keyboards = FocusBehavior._keyboards
|
||||
if keyboard not in keyboards:
|
||||
keyboards[keyboard] = None
|
||||
|
||||
def _bind_keyboard(self):
|
||||
self._ensure_keyboard()
|
||||
keyboard = self._keyboard
|
||||
|
||||
if not keyboard or self.disabled or not self.is_focusable:
|
||||
self.focus = False
|
||||
return
|
||||
keyboards = FocusBehavior._keyboards
|
||||
old_focus = keyboards[keyboard] # keyboard should be in dict
|
||||
if old_focus:
|
||||
old_focus.focus = False
|
||||
# keyboard shouldn't have been released here, see keyboard warning
|
||||
keyboards[keyboard] = self
|
||||
keyboard.bind(on_key_down=self.keyboard_on_key_down,
|
||||
on_key_up=self.keyboard_on_key_up,
|
||||
on_textinput=self.keyboard_on_textinput)
|
||||
|
||||
def _unbind_keyboard(self):
|
||||
keyboard = self._keyboard
|
||||
if keyboard:
|
||||
keyboard.unbind(on_key_down=self.keyboard_on_key_down,
|
||||
on_key_up=self.keyboard_on_key_up,
|
||||
on_textinput=self.keyboard_on_textinput)
|
||||
if self._requested_keyboard:
|
||||
keyboard.release()
|
||||
self._keyboard = None
|
||||
self._requested_keyboard = False
|
||||
del FocusBehavior._keyboards[keyboard]
|
||||
else:
|
||||
FocusBehavior._keyboards[keyboard] = None
|
||||
|
||||
def keyboard_on_textinput(self, window, text):
|
||||
pass
|
||||
|
||||
def _keyboard_released(self):
|
||||
self.focus = False
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
if (not self.disabled and self.is_focusable and
|
||||
('button' not in touch.profile or
|
||||
not touch.button.startswith('scroll'))):
|
||||
self.focus = True
|
||||
FocusBehavior.ignored_touch.append(touch)
|
||||
return super(FocusBehavior, self).on_touch_down(touch)
|
||||
|
||||
@staticmethod
|
||||
def _handle_post_on_touch_up(touch):
|
||||
''' Called by window after each touch has finished.
|
||||
'''
|
||||
touches = FocusBehavior.ignored_touch
|
||||
if touch in touches:
|
||||
touches.remove(touch)
|
||||
return
|
||||
if 'button' in touch.profile and touch.button in\
|
||||
('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
|
||||
return
|
||||
for focusable in list(FocusBehavior._keyboards.values()):
|
||||
if focusable is None or not focusable.unfocus_on_touch:
|
||||
continue
|
||||
focusable.focus = False
|
||||
|
||||
def _get_focus_next(self, focus_dir):
|
||||
current = self
|
||||
walk_tree = 'walk' if focus_dir == 'focus_next' else 'walk_reverse'
|
||||
|
||||
while 1:
|
||||
# if we hit a focusable, walk through focus_xxx
|
||||
while getattr(current, focus_dir) is not None:
|
||||
current = getattr(current, focus_dir)
|
||||
if current is self or current is StopIteration:
|
||||
return None # make sure we don't loop forever
|
||||
if current.is_focusable and not current.disabled:
|
||||
return current
|
||||
|
||||
# hit unfocusable, walk widget tree
|
||||
itr = getattr(current, walk_tree)(loopback=True)
|
||||
if focus_dir == 'focus_next':
|
||||
next(itr) # current is returned first when walking forward
|
||||
for current in itr:
|
||||
if isinstance(current, FocusBehavior):
|
||||
break
|
||||
# why did we stop
|
||||
if isinstance(current, FocusBehavior):
|
||||
if current is self:
|
||||
return None
|
||||
if current.is_focusable and not current.disabled:
|
||||
return current
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_focus_next(self):
|
||||
'''Returns the next focusable widget using either :attr:`focus_next`
|
||||
or the :attr:`children` similar to the order when tabbing forwards
|
||||
with the ``tab`` key.
|
||||
'''
|
||||
return self._get_focus_next('focus_next')
|
||||
|
||||
def get_focus_previous(self):
|
||||
'''Returns the previous focusable widget using either
|
||||
:attr:`focus_previous` or the :attr:`children` similar to the
|
||||
order when the ``tab`` + ``shift`` keys are triggered together.
|
||||
'''
|
||||
return self._get_focus_next('focus_previous')
|
||||
|
||||
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||
'''The method bound to the keyboard when the instance has focus.
|
||||
|
||||
When the instance becomes focused, this method is bound to the
|
||||
keyboard and will be called for every input press. The parameters are
|
||||
the same as :meth:`kivy.core.window.WindowBase.on_key_down`.
|
||||
|
||||
When overwriting the method in the derived widget, super should be
|
||||
called to enable tab cycling. If the derived widget wishes to use tab
|
||||
for its own purposes, it can call super after it has processed the
|
||||
character (if it does not wish to consume the tab).
|
||||
|
||||
Similar to other keyboard functions, it should return True if the
|
||||
key was consumed.
|
||||
'''
|
||||
if keycode[1] == 'tab': # deal with cycle
|
||||
modifiers = set(modifiers)
|
||||
if {'ctrl', 'alt', 'meta', 'super', 'compose'} & modifiers:
|
||||
return False
|
||||
if 'shift' in modifiers:
|
||||
next = self.get_focus_previous()
|
||||
else:
|
||||
next = self.get_focus_next()
|
||||
if next:
|
||||
self.focus = False
|
||||
|
||||
next.focus = True
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyboard_on_key_up(self, window, keycode):
|
||||
'''The method bound to the keyboard when the instance has focus.
|
||||
|
||||
When the instance becomes focused, this method is bound to the
|
||||
keyboard and will be called for every input release. The parameters are
|
||||
the same as :meth:`kivy.core.window.WindowBase.on_key_up`.
|
||||
|
||||
When overwriting the method in the derived widget, super should be
|
||||
called to enable de-focusing on escape. If the derived widget wishes
|
||||
to use escape for its own purposes, it can call super after it has
|
||||
processed the character (if it does not wish to consume the escape).
|
||||
|
||||
See :meth:`keyboard_on_key_down`
|
||||
'''
|
||||
if keycode[1] == 'escape':
|
||||
self.focus = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_keyboard(self):
|
||||
'''
|
||||
Convenience function to show the keyboard in managed mode.
|
||||
'''
|
||||
if self.keyboard_mode == 'managed':
|
||||
self._bind_keyboard()
|
||||
|
||||
def hide_keyboard(self):
|
||||
'''
|
||||
Convenience function to hide the keyboard in managed mode.
|
||||
'''
|
||||
if self.keyboard_mode == 'managed':
|
||||
self._unbind_keyboard()
|
|
@ -0,0 +1,590 @@
|
|||
'''
|
||||
Kivy Namespaces
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
The :class:`KNSpaceBehavior` `mixin <https://en.wikipedia.org/wiki/Mixin>`_
|
||||
class provides namespace functionality for Kivy objects. It allows kivy objects
|
||||
to be named and then accessed using namespaces.
|
||||
|
||||
:class:`KNSpace` instances are the namespaces that store the named objects
|
||||
in Kivy :class:`~kivy.properties.ObjectProperty` instances.
|
||||
In addition, when inheriting from :class:`KNSpaceBehavior`, if the derived
|
||||
object is named, the name will automatically be added to the associated
|
||||
namespace and will point to a :attr:`~kivy.uix.widget.proxy_ref` of the
|
||||
derived object.
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
|
||||
By default, there's only a single namespace: the :attr:`knspace` namespace. The
|
||||
simplest example is adding a widget to the namespace:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.uix.behaviors.knspace import knspace
|
||||
widget = Widget()
|
||||
knspace.my_widget = widget
|
||||
|
||||
This adds a kivy :class:`~kivy.properties.ObjectProperty` with `rebind=True`
|
||||
and `allownone=True` to the :attr:`knspace` namespace with a property name
|
||||
`my_widget`. And the property now also points to this widget.
|
||||
|
||||
This can be done automatically with:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
|
||||
Or in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knsname: 'my_widget'
|
||||
|
||||
Now, `knspace.my_widget` will point to that widget.
|
||||
|
||||
When one creates a second widget with the same name, the namespace will
|
||||
also change to point to the new widget. E.g.:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget
|
||||
widget2 = MyWidget(knsname='my_widget')
|
||||
# knspace.my_widget now points to widget2
|
||||
|
||||
Setting the namespace
|
||||
---------------------
|
||||
|
||||
One can also create ones own namespace rather than using the default
|
||||
:attr:`knspace` by directly setting :attr:`KNSpaceBehavior.knspace`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(KNSpaceBehavior, Widget):
|
||||
pass
|
||||
|
||||
widget = MyWidget(knsname='my_widget')
|
||||
my_new_namespace = KNSpace()
|
||||
widget.knspace = my_new_namespace
|
||||
|
||||
Initially, `my_widget` is added to the default namespace, but when the widget's
|
||||
namespace is changed to `my_new_namespace`, the reference to `my_widget` is
|
||||
moved to that namespace. We could have also of course first set the namespace
|
||||
to `my_new_namespace` and then have named the widget `my_widget`, thereby
|
||||
avoiding the initial assignment to the default namespace.
|
||||
|
||||
Similarly, in kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
|
||||
MyWidget:
|
||||
knspace: KNSpace()
|
||||
knsname: 'my_widget'
|
||||
|
||||
Inheriting the namespace
|
||||
------------------------
|
||||
|
||||
In the previous example, we directly set the namespace we wished to use.
|
||||
In the following example, we inherit it from the parent, so we only have to set
|
||||
it once:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget@KNSpaceBehavior+Widget>
|
||||
<MyLabel@KNSpaceBehavior+Label>
|
||||
|
||||
<MyComplexWidget@MyWidget>:
|
||||
knsname: 'my_complex'
|
||||
MyLabel:
|
||||
knsname: 'label1'
|
||||
MyLabel:
|
||||
knsname: 'label2'
|
||||
|
||||
Then, we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
widget.knspace = new_knspace
|
||||
|
||||
The rule is that if no knspace has been assigned to a widget, it looks for a
|
||||
namespace in its parent and parent's parent and so on until it find one to
|
||||
use. If none are found, it uses the default :attr:`knspace`.
|
||||
|
||||
When `MyComplexWidget` is created, it still used the default namespace.
|
||||
However, when we assigned the root widget its new namespace, all its
|
||||
children switched to using that new namespace as well. So `new_knspace` now
|
||||
contains `label1` and `label2` as well as `my_complex`.
|
||||
|
||||
If we had first done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyComplexWidget()
|
||||
new_knspace = KNSpace()
|
||||
knspace.label1.knspace = knspace
|
||||
widget.knspace = new_knspace
|
||||
|
||||
Then `label1` would remain stored in the default :attr:`knspace` since it was
|
||||
directly set, but `label2` and `my_complex` would still be added to the new
|
||||
namespace.
|
||||
|
||||
One can customize the attribute used to search the parent tree by changing
|
||||
:attr:`KNSpaceBehavior.knspace_key`. If the desired knspace is not reachable
|
||||
through a widgets parent tree, e.g. in a popup that is not a widget's child,
|
||||
:attr:`KNSpaceBehavior.knspace_key` can be used to establish a different
|
||||
search order.
|
||||
|
||||
Accessing the namespace
|
||||
-----------------------
|
||||
|
||||
As seen in the previous example, if not directly assigned, the namespace is
|
||||
found by searching the parent tree. Consequently, if a namespace was assigned
|
||||
further up the parent tree, all its children and below could access that
|
||||
namespace through their :attr:`KNSpaceBehavior.knspace` property.
|
||||
|
||||
This allows the creation of multiple widgets with identically given names
|
||||
if each root widget instance is assigned a new namespace. For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Now, when we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
knspace1, knspace2 = KNSpace(), KNSpace()
|
||||
composite1 = MyCompositeWidget()
|
||||
composite1.knspace = knspace1
|
||||
|
||||
composite2 = MyCompositeWidget()
|
||||
composite2.knspace = knspace2
|
||||
|
||||
knspace1.pretty = "Here's the ladder, now fix the roof!"
|
||||
knspace2.pretty = "Get that raccoon off me!"
|
||||
|
||||
Because each of the `MyCompositeWidget` instances have a different namespace
|
||||
their children also use different namespaces. Consequently, the
|
||||
pretty and complex widgets of each instance will have different text.
|
||||
|
||||
Further, because both the namespace :class:`~kivy.properties.ObjectProperty`
|
||||
references, and :attr:`KNSpaceBehavior.knspace` have `rebind=True`, the
|
||||
text of the `MyComplexWidget` label is rebound to match the text of
|
||||
`MyPrettyWidget` when either the root's namespace changes or when the
|
||||
`root.knspace.pretty` property changes, as expected.
|
||||
|
||||
Forking a namespace
|
||||
-------------------
|
||||
|
||||
Forking a namespace provides the opportunity to create a new namespace
|
||||
from a parent namespace so that the forked namespace will contain everything
|
||||
in the origin namespace, but the origin namespace will not have access to
|
||||
anything added to the forked namespace.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
child = knspace.fork()
|
||||
grandchild = child.fork()
|
||||
|
||||
child.label = Label()
|
||||
grandchild.button = Button()
|
||||
|
||||
Now label is accessible by both child and grandchild, but not by knspace. And
|
||||
button is only accessible by the grandchild but not by the child or by knspace.
|
||||
Finally, doing `grandchild.label = Label()` will leave `grandchild.label`
|
||||
and `child.label` pointing to different labels.
|
||||
|
||||
A motivating example is the example from above:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyComplexWidget@KNSpaceBehavior+Widget>:
|
||||
Label:
|
||||
text: root.knspace.pretty.text if root.knspace.pretty else ''
|
||||
|
||||
<MyPrettyWidget@KNSpaceBehavior+TextInput>:
|
||||
knsname: 'pretty'
|
||||
text: 'Hello'
|
||||
|
||||
<MyCompositeWidget@KNSpaceBehavior+BoxLayout>:
|
||||
knspace: 'fork'
|
||||
MyComplexWidget
|
||||
MyPrettyWidget
|
||||
|
||||
Notice the addition of `knspace: 'fork'`. This is identical to doing
|
||||
`knspace: self.knspace.fork()`. However, doing that would lead to infinite
|
||||
recursion as that kv rule would be executed recursively because `self.knspace`
|
||||
will keep on changing. However, allowing `knspace: 'fork'` cirumvents that.
|
||||
See :attr:`KNSpaceBehavior.knspace`.
|
||||
|
||||
Now, having forked, we just need to do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
composite1 = MyCompositeWidget()
|
||||
composite2 = MyCompositeWidget()
|
||||
|
||||
composite1.knspace.pretty = "Here's the ladder, now fix the roof!"
|
||||
composite2.knspace.pretty = "Get that raccoon off me!"
|
||||
|
||||
Since by forking we automatically created a unique namespace for each
|
||||
`MyCompositeWidget` instance.
|
||||
'''
|
||||
|
||||
__all__ = ('KNSpace', 'KNSpaceBehavior', 'knspace')
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.properties import StringProperty, ObjectProperty, AliasProperty
|
||||
from kivy.context import register_context
|
||||
|
||||
|
||||
class KNSpace(EventDispatcher):
|
||||
'''Each :class:`KNSpace` instance is a namespace that stores the named Kivy
|
||||
objects associated with this namespace. Each named object is
|
||||
stored as the value of a Kivy :class:`~kivy.properties.ObjectProperty` of
|
||||
this instance whose property name is the object's given name. Both `rebind`
|
||||
and `allownone` are set to `True` for the property.
|
||||
|
||||
See :attr:`KNSpaceBehavior.knspace` for details on how a namespace is
|
||||
associated with a named object.
|
||||
|
||||
When storing an object in the namespace, the object's `proxy_ref` is
|
||||
stored if the object has such an attribute.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`parent`: (internal) A :class:`KNSpace` instance or None.
|
||||
If specified, it's a parent namespace, in which case, the current
|
||||
namespace will have in its namespace all its named objects
|
||||
as well as the named objects of its parent and parent's parent
|
||||
etc. See :meth:`fork` for more details.
|
||||
'''
|
||||
|
||||
parent = None
|
||||
'''(internal) The parent namespace instance, :class:`KNSpace`, or None. See
|
||||
:meth:`fork`.
|
||||
'''
|
||||
__has_applied = None
|
||||
|
||||
keep_ref = False
|
||||
'''Whether a direct reference should be kept to the stored objects.
|
||||
If ``True``, we use the direct object, otherwise we use
|
||||
:attr:`~kivy.uix.widget.proxy_ref` when present.
|
||||
|
||||
Defaults to False.
|
||||
'''
|
||||
|
||||
def __init__(self, parent=None, keep_ref=False, **kwargs):
|
||||
self.keep_ref = keep_ref
|
||||
super(KNSpace, self).__init__(**kwargs)
|
||||
self.parent = parent
|
||||
self.__has_applied = set(self.properties().keys())
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
has_applied = self.__has_applied
|
||||
if prop is None:
|
||||
if hasattr(self, name):
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
self.apply_property(
|
||||
**{name:
|
||||
ObjectProperty(None, rebind=True, allownone=True)}
|
||||
)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
has_applied.add(name)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
elif name not in has_applied:
|
||||
self.apply_property(**{name: prop})
|
||||
has_applied.add(name)
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
else:
|
||||
if not self.keep_ref:
|
||||
value = getattr(value, 'proxy_ref', value)
|
||||
super(KNSpace, self).__setattr__(name, value)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in super(KNSpace, self).__getattribute__('__dict__'):
|
||||
return super(KNSpace, self).__getattribute__(name)
|
||||
|
||||
try:
|
||||
value = super(KNSpace, self).__getattribute__(name)
|
||||
except AttributeError:
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
raise AttributeError(name)
|
||||
return getattr(parent, name)
|
||||
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
parent = super(KNSpace, self).__getattribute__('parent')
|
||||
if parent is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return getattr(parent, name) # if parent doesn't have it
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def property(self, name, quiet=False):
|
||||
# needs to overwrite EventDispatcher.property so kv lang will work
|
||||
prop = super(KNSpace, self).property(name, quiet=True)
|
||||
if prop is not None:
|
||||
return prop
|
||||
|
||||
prop = ObjectProperty(None, rebind=True, allownone=True)
|
||||
self.apply_property(**{name: prop})
|
||||
self.__has_applied.add(name)
|
||||
return prop
|
||||
|
||||
def fork(self):
|
||||
'''Returns a new :class:`KNSpace` instance which will have access to
|
||||
all the named objects in the current namespace but will also have a
|
||||
namespace of its own that is unique to it.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
forked_knspace1 = knspace.fork()
|
||||
forked_knspace2 = knspace.fork()
|
||||
|
||||
Now, any names added to `knspace` will be accessible by the
|
||||
`forked_knspace1` and `forked_knspace2` namespaces by the normal means.
|
||||
However, any names added to `forked_knspace1` will not be accessible
|
||||
from `knspace` or `forked_knspace2`. Similar for `forked_knspace2`.
|
||||
'''
|
||||
return KNSpace(parent=self)
|
||||
|
||||
|
||||
class KNSpaceBehavior(object):
|
||||
'''Inheriting from this class allows naming of the inherited objects, which
|
||||
are then added to the associated namespace :attr:`knspace` and accessible
|
||||
through it.
|
||||
|
||||
Please see the :mod:`knspace behaviors module <kivy.uix.behaviors.knspace>`
|
||||
documentation for more information.
|
||||
'''
|
||||
|
||||
_knspace = ObjectProperty(None, allownone=True)
|
||||
_knsname = StringProperty('')
|
||||
__last_knspace = None
|
||||
__callbacks = None
|
||||
|
||||
def __init__(self, knspace=None, **kwargs):
|
||||
self.knspace = knspace
|
||||
super(KNSpaceBehavior, self).__init__(**kwargs)
|
||||
|
||||
def __knspace_clear_callbacks(self, *largs):
|
||||
for obj, name, uid in self.__callbacks:
|
||||
obj.unbind_uid(name, uid)
|
||||
last = self.__last_knspace
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
assert self._knspace is None
|
||||
assert last
|
||||
|
||||
new = self.__set_parent_knspace()
|
||||
if new is last:
|
||||
return
|
||||
self.property('_knspace').dispatch(self)
|
||||
|
||||
name = self.knsname
|
||||
if not name:
|
||||
return
|
||||
|
||||
if getattr(last, name) == self:
|
||||
setattr(last, name, None)
|
||||
|
||||
if new:
|
||||
setattr(new, name, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
|
||||
def __set_parent_knspace(self):
|
||||
callbacks = self.__callbacks = []
|
||||
fbind = self.fbind
|
||||
append = callbacks.append
|
||||
parent_key = self.knspace_key
|
||||
clear = self.__knspace_clear_callbacks
|
||||
|
||||
append((self, 'knspace_key', fbind('knspace_key', clear)))
|
||||
if not parent_key:
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
append((self, parent_key, fbind(parent_key, clear)))
|
||||
parent = getattr(self, parent_key, None)
|
||||
while parent is not None:
|
||||
fbind = parent.fbind
|
||||
|
||||
parent_knspace = getattr(parent, 'knspace', 0)
|
||||
if parent_knspace != 0:
|
||||
append((parent, 'knspace', fbind('knspace', clear)))
|
||||
self.__last_knspace = parent_knspace
|
||||
return parent_knspace
|
||||
|
||||
append((parent, parent_key, fbind(parent_key, clear)))
|
||||
new_parent = getattr(parent, parent_key, None)
|
||||
if new_parent is parent:
|
||||
break
|
||||
parent = new_parent
|
||||
self.__last_knspace = knspace
|
||||
return knspace
|
||||
|
||||
def _get_knspace(self):
|
||||
_knspace = self._knspace
|
||||
if _knspace is not None:
|
||||
return _knspace
|
||||
|
||||
if self.__callbacks is not None:
|
||||
return self.__last_knspace
|
||||
|
||||
# we only get here if we never accessed our knspace
|
||||
return self.__set_parent_knspace()
|
||||
|
||||
def _set_knspace(self, value):
|
||||
if value is self._knspace:
|
||||
return
|
||||
|
||||
knspace = self._knspace or self.__last_knspace
|
||||
name = self.knsname
|
||||
if name and knspace and getattr(knspace, name) == self:
|
||||
setattr(knspace, name, None) # reset old namespace
|
||||
|
||||
if value == 'fork':
|
||||
if not knspace:
|
||||
knspace = self.knspace # get parents in case we haven't before
|
||||
if knspace:
|
||||
value = knspace.fork()
|
||||
else:
|
||||
raise ValueError('Cannot fork with no namespace')
|
||||
|
||||
for obj, prop_name, uid in self.__callbacks or []:
|
||||
obj.unbind_uid(prop_name, uid)
|
||||
self.__last_knspace = self.__callbacks = None
|
||||
|
||||
if name:
|
||||
if value is None: # if None, first update the recursive knspace
|
||||
knspace = self.__set_parent_knspace()
|
||||
if knspace:
|
||||
setattr(knspace, name, self)
|
||||
self._knspace = None # cause a kv trigger
|
||||
else:
|
||||
setattr(value, name, self)
|
||||
knspace = self._knspace = value
|
||||
|
||||
if not knspace:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(name))
|
||||
else:
|
||||
if value is None:
|
||||
self.__set_parent_knspace() # update before trigger below
|
||||
self._knspace = value
|
||||
|
||||
knspace = AliasProperty(
|
||||
_get_knspace, _set_knspace, bind=('_knspace', ), cache=False,
|
||||
rebind=True, allownone=True)
|
||||
'''The namespace instance, :class:`KNSpace`, associated with this widget.
|
||||
The :attr:`knspace` namespace stores this widget when naming this widget
|
||||
with :attr:`knsname`.
|
||||
|
||||
If the namespace has been set with a :class:`KNSpace` instance, e.g. with
|
||||
`self.knspace = KNSpace()`, then that instance is returned (setting with
|
||||
`None` doesn't count). Otherwise, if :attr:`knspace_key` is not None, we
|
||||
look for a namespace to use in the object that is stored in the property
|
||||
named :attr:`knspace_key`, of this instance. I.e.
|
||||
`object = getattr(self, self.knspace_key)`.
|
||||
|
||||
If that object has a knspace property, then we return its value. Otherwise,
|
||||
we go further up, e.g. with `getattr(object, self.knspace_key)` and look
|
||||
for its `knspace` property.
|
||||
|
||||
Finally, if we reach a value of `None`, or :attr:`knspace_key` was `None`,
|
||||
the default :attr:`~kivy.uix.behaviors.knspace.knspace` namespace is
|
||||
returned.
|
||||
|
||||
If :attr:`knspace` is set to the string `'fork'`, the current namespace
|
||||
in :attr:`knspace` will be forked with :meth:`KNSpace.fork` and the
|
||||
resulting namespace will be assigned to this instance's :attr:`knspace`.
|
||||
See the module examples for a motivating example.
|
||||
|
||||
Both `rebind` and `allownone` are `True`.
|
||||
'''
|
||||
|
||||
knspace_key = StringProperty('parent', allownone=True)
|
||||
'''The name of the property of this instance, to use to search upwards for
|
||||
a namespace to use by this instance. Defaults to `'parent'` so that we'll
|
||||
search the parent tree. See :attr:`knspace`.
|
||||
|
||||
When `None`, we won't search the parent tree for the namespace.
|
||||
`allownone` is `True`.
|
||||
'''
|
||||
|
||||
def _get_knsname(self):
|
||||
return self._knsname
|
||||
|
||||
def _set_knsname(self, value):
|
||||
old_name = self._knsname
|
||||
knspace = self.knspace
|
||||
if old_name and knspace and getattr(knspace, old_name) == self:
|
||||
setattr(knspace, old_name, None)
|
||||
|
||||
self._knsname = value
|
||||
if value:
|
||||
if knspace:
|
||||
setattr(knspace, value, self)
|
||||
else:
|
||||
raise ValueError('Object has name "{}", but no namespace'.
|
||||
format(value))
|
||||
|
||||
knsname = AliasProperty(
|
||||
_get_knsname, _set_knsname, bind=('_knsname', ), cache=False)
|
||||
'''The name given to this instance. If named, the name will be added to the
|
||||
associated :attr:`knspace` namespace, which will then point to the
|
||||
`proxy_ref` of this instance.
|
||||
|
||||
When named, one can access this object by e.g. self.knspace.name, where
|
||||
`name` is the given name of this instance. See :attr:`knspace` and the
|
||||
module description for more details.
|
||||
'''
|
||||
|
||||
|
||||
knspace = register_context('knspace', KNSpace)
|
||||
'''The default :class:`KNSpace` namespace. See :attr:`KNSpaceBehavior.knspace`
|
||||
for more details.
|
||||
'''
|
|
@ -0,0 +1,156 @@
|
|||
'''
|
||||
ToggleButton Behavior
|
||||
=====================
|
||||
|
||||
The :class:`~kivy.uix.behaviors.togglebutton.ToggleButtonBehavior`
|
||||
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:class:`~kivy.uix.togglebutton.ToggleButton` behavior. You can combine this
|
||||
class with other widgets, such as an :class:`~kivy.uix.image.Image`, to provide
|
||||
alternative togglebuttons that preserve Kivy togglebutton behavior.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following example adds togglebutton behavior to an image to make a checkbox
|
||||
that behaves like a togglebutton::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.behaviors import ToggleButtonBehavior
|
||||
|
||||
|
||||
class MyButton(ToggleButtonBehavior, Image):
|
||||
def __init__(self, **kwargs):
|
||||
super(MyButton, self).__init__(**kwargs)
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
def on_state(self, widget, value):
|
||||
if value == 'down':
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_on'
|
||||
else:
|
||||
self.source = 'atlas://data/images/defaulttheme/checkbox_off'
|
||||
|
||||
|
||||
class SampleApp(App):
|
||||
def build(self):
|
||||
return MyButton()
|
||||
|
||||
|
||||
SampleApp().run()
|
||||
'''
|
||||
|
||||
__all__ = ('ToggleButtonBehavior', )
|
||||
|
||||
from kivy.properties import ObjectProperty, BooleanProperty
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from weakref import ref
|
||||
|
||||
|
||||
class ToggleButtonBehavior(ButtonBehavior):
|
||||
'''This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
:mod:`~kivy.uix.togglebutton` behavior. Please see the
|
||||
:mod:`togglebutton behaviors module <kivy.uix.behaviors.togglebutton>`
|
||||
documentation for more information.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
__groups = {}
|
||||
|
||||
group = ObjectProperty(None, allownone=True)
|
||||
'''Group of the button. If `None`, no group will be used (the button will be
|
||||
independent). If specified, :attr:`group` must be a hashable object, like
|
||||
a string. Only one button in a group can be in a 'down' state.
|
||||
|
||||
:attr:`group` is a :class:`~kivy.properties.ObjectProperty` and defaults to
|
||||
`None`.
|
||||
'''
|
||||
|
||||
allow_no_selection = BooleanProperty(True)
|
||||
'''This specifies whether the widgets in a group allow no selection i.e.
|
||||
everything to be deselected.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`allow_no_selection` is a :class:`BooleanProperty` and defaults to
|
||||
`True`
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._previous_group = None
|
||||
super(ToggleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_group(self, *largs):
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if self._previous_group:
|
||||
group = groups[self._previous_group]
|
||||
for item in group[:]:
|
||||
if item() is self:
|
||||
group.remove(item)
|
||||
break
|
||||
group = self._previous_group = self.group
|
||||
if group not in groups:
|
||||
groups[group] = []
|
||||
r = ref(self, ToggleButtonBehavior._clear_groups)
|
||||
groups[group].append(r)
|
||||
|
||||
def _release_group(self, current):
|
||||
if self.group is None:
|
||||
return
|
||||
group = self.__groups[self.group]
|
||||
for item in group[:]:
|
||||
widget = item()
|
||||
if widget is None:
|
||||
group.remove(item)
|
||||
if widget is current:
|
||||
continue
|
||||
widget.state = 'normal'
|
||||
|
||||
def _do_press(self):
|
||||
if (not self.allow_no_selection and
|
||||
self.group and self.state == 'down'):
|
||||
return
|
||||
|
||||
self._release_group(self)
|
||||
self.state = 'normal' if self.state == 'down' else 'down'
|
||||
|
||||
def _do_release(self, *args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _clear_groups(wk):
|
||||
# auto flush the element when the weak reference have been deleted
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
for group in list(groups.values()):
|
||||
if wk in group:
|
||||
group.remove(wk)
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def get_widgets(groupname):
|
||||
'''Return a list of the widgets contained in a specific group. If the
|
||||
group doesn't exist, an empty list will be returned.
|
||||
|
||||
.. note::
|
||||
|
||||
Always release the result of this method! Holding a reference to
|
||||
any of these widgets can prevent them from being garbage collected.
|
||||
If in doubt, do::
|
||||
|
||||
l = ToggleButtonBehavior.get_widgets('mygroup')
|
||||
# do your job
|
||||
del l
|
||||
|
||||
.. warning::
|
||||
|
||||
It's possible that some widgets that you have previously
|
||||
deleted are still in the list. The garbage collector might need
|
||||
to release other objects before flushing them.
|
||||
'''
|
||||
groups = ToggleButtonBehavior.__groups
|
||||
if groupname not in groups:
|
||||
return []
|
||||
return [x() for x in groups[groupname] if x()][:]
|
|
@ -0,0 +1,318 @@
|
|||
'''
|
||||
Touch Ripple
|
||||
============
|
||||
|
||||
.. versionadded:: 1.10.1
|
||||
|
||||
.. warning::
|
||||
This code is still experimental, and its API is subject to change in a
|
||||
future version.
|
||||
|
||||
This module contains `mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes
|
||||
to add a touch ripple visual effect known from `Google Material Design
|
||||
<https://en.wikipedia.org/wiki/Material_Design>_` to widgets.
|
||||
|
||||
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
||||
documentation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleBehavior` provides
|
||||
rendering the ripple animation.
|
||||
|
||||
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleButtonBehavior`
|
||||
basically provides the same functionality as
|
||||
:class:`~kivy.uix.behaviors.button.ButtonBehavior` but rendering the ripple
|
||||
animation instead of default press/release visualization.
|
||||
'''
|
||||
from kivy.animation import Animation
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import CanvasBase, Color, Ellipse, ScissorPush, ScissorPop
|
||||
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, \
|
||||
ObjectProperty, StringProperty
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TouchRippleBehavior',
|
||||
'TouchRippleButtonBehavior'
|
||||
)
|
||||
|
||||
|
||||
class TouchRippleBehavior(object):
|
||||
'''Touch ripple behavior.
|
||||
|
||||
Supposed to be used as mixin on widget classes.
|
||||
|
||||
Ripple behavior does not trigger automatically, concrete implementation
|
||||
needs to call :func:`ripple_show` respective :func:`ripple_fade` manually.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here we create a Label which renders the touch ripple animation on
|
||||
interaction::
|
||||
|
||||
class RippleLabel(TouchRippleBehavior, Label):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RippleLabel, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
collide_point = self.collide_point(touch.x, touch.y)
|
||||
if collide_point:
|
||||
touch.grab(self)
|
||||
self.ripple_show(touch)
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is self:
|
||||
touch.ungrab(self)
|
||||
self.ripple_fade()
|
||||
return True
|
||||
return False
|
||||
'''
|
||||
|
||||
ripple_rad_default = NumericProperty(10)
|
||||
'''Default radius the animation starts from.
|
||||
|
||||
:attr:`ripple_rad_default` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `10`.
|
||||
'''
|
||||
|
||||
ripple_duration_in = NumericProperty(.5)
|
||||
'''Animation duration taken to show the overlay.
|
||||
|
||||
:attr:`ripple_duration_in` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_duration_out = NumericProperty(.2)
|
||||
'''Animation duration taken to fade the overlay.
|
||||
|
||||
:attr:`ripple_duration_out` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.2`.
|
||||
'''
|
||||
|
||||
ripple_fade_from_alpha = NumericProperty(.5)
|
||||
'''Alpha channel for ripple color the animation starts with.
|
||||
|
||||
:attr:`ripple_fade_from_alpha` is a
|
||||
:class:`~kivy.properties.NumericProperty` and defaults to `0.5`.
|
||||
'''
|
||||
|
||||
ripple_fade_to_alpha = NumericProperty(.8)
|
||||
'''Alpha channel for ripple color the animation targets to.
|
||||
|
||||
:attr:`ripple_fade_to_alpha` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.8`.
|
||||
'''
|
||||
|
||||
ripple_scale = NumericProperty(2.)
|
||||
'''Max scale of the animation overlay calculated from max(width/height) of
|
||||
the decorated widget.
|
||||
|
||||
:attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `2.0`.
|
||||
'''
|
||||
|
||||
ripple_func_in = StringProperty('in_cubic')
|
||||
'''Animation callback for showing the overlay.
|
||||
|
||||
:attr:`ripple_func_in` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `in_cubic`.
|
||||
'''
|
||||
|
||||
ripple_func_out = StringProperty('out_quad')
|
||||
'''Animation callback for hiding the overlay.
|
||||
|
||||
:attr:`ripple_func_out` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `out_quad`.
|
||||
'''
|
||||
|
||||
ripple_rad = NumericProperty(10)
|
||||
ripple_pos = ListProperty([0, 0])
|
||||
ripple_color = ListProperty((1., 1., 1., .5))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(TouchRippleBehavior, self).__init__(**kwargs)
|
||||
self.ripple_pane = CanvasBase()
|
||||
self.canvas.add(self.ripple_pane)
|
||||
self.bind(
|
||||
ripple_color=self._ripple_set_color,
|
||||
ripple_pos=self._ripple_set_ellipse,
|
||||
ripple_rad=self._ripple_set_ellipse
|
||||
)
|
||||
self.ripple_ellipse = None
|
||||
self.ripple_col_instruction = None
|
||||
|
||||
def ripple_show(self, touch):
|
||||
'''Begin ripple animation on current widget.
|
||||
|
||||
Expects touch event as argument.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
self._ripple_reset_pane()
|
||||
x, y = self.to_window(*self.pos)
|
||||
width, height = self.size
|
||||
if isinstance(self, RelativeLayout):
|
||||
self.ripple_pos = ripple_pos = (touch.x - x, touch.y - y)
|
||||
else:
|
||||
self.ripple_pos = ripple_pos = (touch.x, touch.y)
|
||||
rc = self.ripple_color
|
||||
ripple_rad = self.ripple_rad
|
||||
self.ripple_color = [rc[0], rc[1], rc[2], self.ripple_fade_from_alpha]
|
||||
with self.ripple_pane:
|
||||
ScissorPush(
|
||||
x=int(round(x)),
|
||||
y=int(round(y)),
|
||||
width=int(round(width)),
|
||||
height=int(round(height))
|
||||
)
|
||||
self.ripple_col_instruction = Color(rgba=self.ripple_color)
|
||||
self.ripple_ellipse = Ellipse(
|
||||
size=(ripple_rad, ripple_rad),
|
||||
pos=(
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
)
|
||||
ScissorPop()
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
t=self.ripple_func_in,
|
||||
ripple_color=[rc[0], rc[1], rc[2], self.ripple_fade_to_alpha],
|
||||
duration=self.ripple_duration_in
|
||||
)
|
||||
anim.start(self)
|
||||
|
||||
def ripple_fade(self):
|
||||
'''Finish ripple animation on current widget.
|
||||
'''
|
||||
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
||||
width, height = self.size
|
||||
rc = self.ripple_color
|
||||
duration = self.ripple_duration_out
|
||||
anim = Animation(
|
||||
ripple_rad=max(width, height) * self.ripple_scale,
|
||||
ripple_color=[rc[0], rc[1], rc[2], 0.],
|
||||
t=self.ripple_func_out,
|
||||
duration=duration
|
||||
)
|
||||
anim.bind(on_complete=self._ripple_anim_complete)
|
||||
anim.start(self)
|
||||
|
||||
def _ripple_set_ellipse(self, instance, value):
|
||||
ellipse = self.ripple_ellipse
|
||||
if not ellipse:
|
||||
return
|
||||
ripple_pos = self.ripple_pos
|
||||
ripple_rad = self.ripple_rad
|
||||
ellipse.size = (ripple_rad, ripple_rad)
|
||||
ellipse.pos = (
|
||||
ripple_pos[0] - ripple_rad / 2.,
|
||||
ripple_pos[1] - ripple_rad / 2.
|
||||
)
|
||||
|
||||
def _ripple_set_color(self, instance, value):
|
||||
if not self.ripple_col_instruction:
|
||||
return
|
||||
self.ripple_col_instruction.rgba = value
|
||||
|
||||
def _ripple_anim_complete(self, anim, instance):
|
||||
self._ripple_reset_pane()
|
||||
|
||||
def _ripple_reset_pane(self):
|
||||
self.ripple_rad = self.ripple_rad_default
|
||||
self.ripple_pane.clear()
|
||||
|
||||
|
||||
class TouchRippleButtonBehavior(TouchRippleBehavior):
|
||||
'''
|
||||
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
||||
a similar behavior to :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
||||
but provides touch ripple animation instead of button pressed/released as
|
||||
visual effect.
|
||||
|
||||
:Events:
|
||||
`on_press`
|
||||
Fired when the button is pressed.
|
||||
`on_release`
|
||||
Fired when the button is released (i.e. the touch/click that
|
||||
pressed the button goes away).
|
||||
'''
|
||||
|
||||
last_touch = ObjectProperty(None)
|
||||
'''Contains the last relevant touch received by the Button. This can
|
||||
be used in `on_press` or `on_release` in order to know which touch
|
||||
dispatched the event.
|
||||
|
||||
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to `None`.
|
||||
'''
|
||||
|
||||
always_release = BooleanProperty(False)
|
||||
'''This determines whether or not the widget fires an `on_release` event if
|
||||
the touch_up is outside the widget.
|
||||
|
||||
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `False`.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_press')
|
||||
self.register_event_type('on_release')
|
||||
super(TouchRippleButtonBehavior, self).__init__(**kwargs)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_down(touch):
|
||||
return True
|
||||
if touch.is_mouse_scrolling:
|
||||
return False
|
||||
if not self.collide_point(touch.x, touch.y):
|
||||
return False
|
||||
if self in touch.ud:
|
||||
return False
|
||||
touch.grab(self)
|
||||
touch.ud[self] = True
|
||||
self.last_touch = touch
|
||||
self.ripple_show(touch)
|
||||
self.dispatch('on_press')
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is self:
|
||||
return True
|
||||
if super(TouchRippleButtonBehavior, self).on_touch_move(touch):
|
||||
return True
|
||||
return self in touch.ud
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return super(TouchRippleButtonBehavior, self).on_touch_up(touch)
|
||||
assert self in touch.ud
|
||||
touch.ungrab(self)
|
||||
self.last_touch = touch
|
||||
if self.disabled:
|
||||
return
|
||||
self.ripple_fade()
|
||||
if not self.always_release and not self.collide_point(*touch.pos):
|
||||
return
|
||||
|
||||
# defer on_release until ripple_fade has completed
|
||||
def defer_release(dt):
|
||||
self.dispatch('on_release')
|
||||
Clock.schedule_once(defer_release, self.ripple_duration_out)
|
||||
return True
|
||||
|
||||
def on_disabled(self, instance, value):
|
||||
# ensure ripple animation completes if disabled gets set to True
|
||||
if value:
|
||||
self.ripple_fade()
|
||||
return super(TouchRippleButtonBehavior, self).on_disabled(
|
||||
instance, value)
|
||||
|
||||
def on_press(self):
|
||||
pass
|
||||
|
||||
def on_release(self):
|
||||
pass
|
331
kivy_venv/lib/python3.11/site-packages/kivy/uix/boxlayout.py
Normal file
331
kivy_venv/lib/python3.11/site-packages/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)
|
576
kivy_venv/lib/python3.11/site-packages/kivy/uix/bubble.py
Normal file
576
kivy_venv/lib/python3.11/site-packages/kivy/uix/bubble.py
Normal file
|
@ -0,0 +1,576 @@
|
|||
'''
|
||||
Bubble
|
||||
======
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
.. image:: images/bubble.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`Bubble` widget is a form of menu or a small popup with an arrow
|
||||
arranged on one side of it's content.
|
||||
|
||||
The :class:`Bubble` contains an arrow attached to the content
|
||||
(e.g., :class:`BubbleContent`) pointing in the direction you choose. It can
|
||||
be placed either at a predefined location or flexibly by specifying a relative
|
||||
position on the border of the widget.
|
||||
|
||||
The :class:`BubbleContent` is a styled BoxLayout and is thought to be added to
|
||||
the :class:`Bubble` as a child widget. The :class:`Bubble` will then arrange
|
||||
an arrow around the content as desired. Instead of the class:`BubbleContent`,
|
||||
you can theoretically use any other :class:`Widget` as well as long as it
|
||||
supports the 'bind' and 'unbind' function of the :class:`EventDispatcher` and
|
||||
is compatible with Kivy to be placed inside a :class:`BoxLayout`.
|
||||
|
||||
The :class:`BubbleButton`is a styled Button. It suits to the style of
|
||||
:class:`Bubble` and :class:`BubbleContent`. Feel free to place other Widgets
|
||||
inside the 'content' of the :class:`Bubble`.
|
||||
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
The properties :attr:`background_image`, :attr:`background_color`,
|
||||
:attr:`border` and :attr:`border_auto_scale` were removed from :class:`Bubble`.
|
||||
These properties had only been used by the content widget that now uses it's
|
||||
own properties instead. The color of the arrow is now changed with
|
||||
:attr:`arrow_color` instead of :attr:`background_color`.
|
||||
These changes makes the :class:`Bubble` transparent to use with other layouts
|
||||
as content without any side-effects due to property inheritance.
|
||||
|
||||
The property :attr:`flex_arrow_pos` has been added to allow further
|
||||
customization of the arrow positioning.
|
||||
|
||||
The properties :attr:`arrow_margin`, :attr:`arrow_margin_x`,
|
||||
:attr:`arrow_margin_y`, :attr:`content_size`, :attr:`content_width` and
|
||||
:attr:`content_height` have been added to ease proper sizing of a
|
||||
:class:`Bubble` e.g., based on it's content size.
|
||||
|
||||
BubbleContent
|
||||
=============
|
||||
|
||||
The :class:`BubbleContent` is a styled BoxLayout that can be used to
|
||||
add e.g., :class:`BubbleButtons` as menu items.
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
The properties :attr:`background_image`, :attr:`background_color`,
|
||||
:attr:`border` and :attr:`border_auto_scale` were added to the
|
||||
:class:`BubbleContent`. The :class:`BubbleContent` does no longer rely on these
|
||||
properties being present in the parent class.
|
||||
|
||||
BubbleButton
|
||||
============
|
||||
|
||||
The :class:`BubbleButton` is a styled :class:`Button` that can be used to be
|
||||
added to the :class:`BubbleContent`.
|
||||
|
||||
Simple example
|
||||
--------------
|
||||
|
||||
.. include:: ../../examples/widgets/bubble_test.py
|
||||
:literal:
|
||||
|
||||
Customize the Bubble
|
||||
--------------------
|
||||
|
||||
You can choose the direction in which the arrow points::
|
||||
|
||||
Bubble(arrow_pos='top_mid')
|
||||
or
|
||||
Bubble(size=(200, 40), flex_arrow_pos=(175, 40))
|
||||
|
||||
Similarly, the corresponding properties in the '.kv' language can be used
|
||||
as well.
|
||||
|
||||
You can change the appearance of the bubble::
|
||||
|
||||
Bubble(
|
||||
arrow_image='/path/to/arrow/image',
|
||||
arrow_color=(1, 0, 0, .5)),
|
||||
)
|
||||
BubbleContent(
|
||||
background_image='/path/to/background/image',
|
||||
background_color=(1, 0, 0, .5), # 50% translucent red
|
||||
border=(0,0,0,0),
|
||||
)
|
||||
|
||||
Similarly, the corresponding properties in the '.kv' language can be used
|
||||
as well.
|
||||
|
||||
-----------------------------
|
||||
'''
|
||||
|
||||
__all__ = ('Bubble', 'BubbleButton', 'BubbleContent')
|
||||
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.scatter import Scatter
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.properties import StringProperty
|
||||
from kivy.properties import OptionProperty
|
||||
from kivy.properties import ListProperty
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.properties import ColorProperty
|
||||
from kivy.properties import NumericProperty
|
||||
from kivy.properties import ReferenceListProperty
|
||||
from kivy.base import EventLoop
|
||||
from kivy.metrics import dp
|
||||
|
||||
|
||||
class BubbleException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BubbleButton(Button):
|
||||
'''A button intended for use in a BubbleContent widget.
|
||||
You can use a "normal" button class, but it will not look good unless the
|
||||
background is changed.
|
||||
|
||||
Rather use this BubbleButton widget that is already defined and provides a
|
||||
suitable background for you.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class BubbleContent(BoxLayout):
|
||||
'''A styled BoxLayout that can be used as the content widget of a Bubble.
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
The graphical appearance of :class:`BubbleContent` is now based on it's
|
||||
own properties :attr:`background_image`, :attr:`background_color`,
|
||||
:attr:`border` and :attr:`border_auto_scale`. The parent widget properties
|
||||
are no longer considered. This makes the BubbleContent a standalone themed
|
||||
BoxLayout.
|
||||
'''
|
||||
|
||||
background_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Background color, in the format (r, g, b, a). To use it you have to set
|
||||
:attr:`background_image` first.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
'''
|
||||
|
||||
background_image = StringProperty('atlas://data/images/defaulttheme/bubble')
|
||||
'''Background image of the bubble.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/bubble'.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used with the :attr:`background_image`.
|
||||
It should be used when using custom backgrounds.
|
||||
|
||||
It must be a list of 4 values: (bottom, right, top, left). Read the
|
||||
BorderImage instructions for more information about how to use it.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16)
|
||||
'''
|
||||
|
||||
border_auto_scale = OptionProperty(
|
||||
'both_lower',
|
||||
options=[
|
||||
'off', 'both', 'x_only', 'y_only', 'y_full_x_lower',
|
||||
'x_full_y_lower', 'both_lower'
|
||||
]
|
||||
)
|
||||
'''Specifies the :attr:`kivy.graphics.BorderImage.auto_scale`
|
||||
value on the background BorderImage.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`border_auto_scale` is a
|
||||
:class:`~kivy.properties.OptionProperty` and defaults to
|
||||
'both_lower'.
|
||||
'''
|
||||
|
||||
|
||||
class Bubble(BoxLayout):
|
||||
'''Bubble class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
content = ObjectProperty(allownone=True)
|
||||
'''This is the object where the main content of the bubble is held.
|
||||
|
||||
The content of the Bubble set by 'add_widget' and removed with
|
||||
'remove_widget' similarly to the :class:`ActionView` which is placed into
|
||||
a class:`ActionBar`
|
||||
|
||||
:attr:`content` is a :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
arrow_image = StringProperty(
|
||||
'atlas://data/images/defaulttheme/bubble_arrow'
|
||||
)
|
||||
''' Image of the arrow pointing to the bubble.
|
||||
|
||||
:attr:`arrow_image` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/bubble_arrow'.
|
||||
'''
|
||||
|
||||
arrow_color = ColorProperty([1, 1, 1, 1])
|
||||
'''Arrow color, in the format (r, g, b, a). To use it you have to set
|
||||
:attr:`arrow_image` first.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [1, 1, 1, 1].
|
||||
'''
|
||||
|
||||
show_arrow = BooleanProperty(True)
|
||||
''' Indicates whether to show arrow.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`show_arrow` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to `True`.
|
||||
'''
|
||||
|
||||
arrow_pos = OptionProperty(
|
||||
'bottom_mid',
|
||||
options=(
|
||||
'left_top', 'left_mid', 'left_bottom',
|
||||
'top_left', 'top_mid', 'top_right',
|
||||
'right_top', 'right_mid', 'right_bottom',
|
||||
'bottom_left', 'bottom_mid', 'bottom_right',
|
||||
)
|
||||
)
|
||||
'''Specifies the position of the arrow as predefined relative position to
|
||||
the bubble.
|
||||
Can be one of: left_top, left_mid, left_bottom top_left, top_mid, top_right
|
||||
right_top, right_mid, right_bottom bottom_left, bottom_mid, bottom_right.
|
||||
|
||||
:attr:`arrow_pos` is a :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'bottom_mid'.
|
||||
'''
|
||||
|
||||
flex_arrow_pos = ListProperty(None)
|
||||
'''Specifies the position of the arrow as flex coordinate around the
|
||||
border of the :class:`Bubble` Widget.
|
||||
If this property is set to a proper position (relative pixel coordinates
|
||||
within the :class:`Bubble` widget, it overwrites the setting
|
||||
:attr:`arrow_pos`.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`flex_arrow_pos` is a :class:`~kivy.properties.ListProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
limit_to = ObjectProperty(None, allownone=True)
|
||||
'''Specifies the widget to which the bubbles position is restricted.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
:attr:`limit_to` is a :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to 'None'.
|
||||
'''
|
||||
|
||||
arrow_margin_x = NumericProperty(0)
|
||||
'''Automatically computed margin in x direction that the arrow widget
|
||||
occupies in pixel.
|
||||
|
||||
In combination with the :attr:`content_width`, this property can be used
|
||||
to determine the correct width of the Bubble to exactly enclose the
|
||||
arrow + content by adding :attr:`content_width` and :attr:`arrow_margin_x`
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_margin_x` is a :class:`~kivy.properties.NumericProperty` and
|
||||
represents the added margin in x direction due to the arrow widget.
|
||||
It defaults to 0 and is read only.
|
||||
'''
|
||||
|
||||
arrow_margin_y = NumericProperty(0)
|
||||
'''Automatically computed margin in y direction that the arrow widget
|
||||
occupies in pixel.
|
||||
|
||||
In combination with the :attr:`content_height`, this property can be used
|
||||
to determine the correct height of the Bubble to exactly enclose the
|
||||
arrow + content by adding :attr:`content_height` and :attr:`arrow_margin_y`
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_margin_y` is a :class:`~kivy.properties.NumericProperty` and
|
||||
represents the added margin in y direction due to the arrow widget.
|
||||
It defaults to 0 and is read only.
|
||||
'''
|
||||
|
||||
arrow_margin = ReferenceListProperty(arrow_margin_x, arrow_margin_y)
|
||||
'''Automatically computed margin that the arrow widget occupies in
|
||||
x and y direction in pixel.
|
||||
|
||||
Check the description of :attr:`arrow_margin_x` and :attr:`arrow_margin_y`.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`arrow_margin` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`arrow_margin_x`, :attr:`arrow_margin_y`) properties.
|
||||
It is read only.
|
||||
'''
|
||||
|
||||
content_width = NumericProperty(0)
|
||||
'''The width of the content Widget.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`content_width` is a :class:`~kivy.properties.NumericProperty` and
|
||||
is the same as self.content.width if content is not None, else it defaults
|
||||
to 0. It is read only.
|
||||
'''
|
||||
|
||||
content_height = NumericProperty(0)
|
||||
'''The height of the content Widget.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`content_height` is a :class:`~kivy.properties.NumericProperty` and
|
||||
is the same as self.content.height if content is not None, else it defaults
|
||||
to 0. It is read only.
|
||||
'''
|
||||
|
||||
content_size = ReferenceListProperty(content_width, content_height)
|
||||
''' The size of the content Widget.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
:attr:`content_size` is a :class:`~kivy.properties.ReferenceListProperty`
|
||||
of (:attr:`content_width`, :attr:`content_height`) properties.
|
||||
It is read only.
|
||||
'''
|
||||
|
||||
# Internal map that specifies the different parameters for fixed arrow
|
||||
# position layouts. The flex_arrow_pos uses these parameter sets
|
||||
# as a template.
|
||||
# 0: orientation of the children of Bubble ([content, arrow])
|
||||
# 1: order of widgets to add to the BoxLayout (default: [content, arrow])
|
||||
# 2: size_hint of _arrow_image_layout
|
||||
# 3: rotation of the _arrow_image
|
||||
# 4: pos_hint of the _arrow_image_layout
|
||||
ARROW_LAYOUTS = {
|
||||
"bottom_left": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "x": 0.05}), # noqa: E201,E241,E501
|
||||
"bottom_mid": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "center_x": 0.50}), # noqa: E201,E241,E501
|
||||
"bottom_right": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "right": 0.95}), # noqa: E201,E241,E501
|
||||
"right_bottom": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "y": 0.05}), # noqa: E201,E241,E501
|
||||
"right_mid": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "center_y": 0.50}), # noqa: E201,E241,E501
|
||||
"right_top": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "top": 0.95}), # noqa: E201,E241,E501
|
||||
"top_left": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "x": 0.05}), # noqa: E201,E241,E501
|
||||
"top_mid": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "center_x": 0.50}), # noqa: E201,E241,E501
|
||||
"top_right": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "right": 0.95}), # noqa: E201,E241,E501
|
||||
"left_bottom": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "y": 0.05}), # noqa: E201,E241,E501
|
||||
"left_mid": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "center_y": 0.50}), # noqa: E201,E241,E501
|
||||
"left_top": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "top": 0.95}), # noqa: E201,E241,E501
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.content = None
|
||||
|
||||
self._flex_arrow_layout_params = None
|
||||
self._temporarily_ignore_limits = False
|
||||
|
||||
self._arrow_image = Image(
|
||||
source=self.arrow_image,
|
||||
fit_mode="scale-down",
|
||||
color=self.arrow_color
|
||||
)
|
||||
self._arrow_image.width = self._arrow_image.texture_size[0]
|
||||
self._arrow_image.height = dp(self._arrow_image.texture_size[1])
|
||||
self._arrow_image_scatter = Scatter(
|
||||
size_hint=(None, None),
|
||||
do_scale=False,
|
||||
do_rotation=False,
|
||||
do_translation=False,
|
||||
)
|
||||
self._arrow_image_scatter.add_widget(self._arrow_image)
|
||||
self._arrow_image_scatter.size = self._arrow_image.texture_size
|
||||
self._arrow_image_scatter_wrapper = BoxLayout(
|
||||
size_hint=(None, None),
|
||||
)
|
||||
self._arrow_image_scatter_wrapper.add_widget(self._arrow_image_scatter)
|
||||
self._arrow_image_layout = RelativeLayout()
|
||||
self._arrow_image_layout.add_widget(self._arrow_image_scatter_wrapper)
|
||||
|
||||
self._arrow_layout = None
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
if self.content is None:
|
||||
self.content = widget
|
||||
self.content_size = widget.size
|
||||
self.content.bind(size=self.update_content_size)
|
||||
self.reposition_inner_widgets()
|
||||
else:
|
||||
raise BubbleException(
|
||||
"Bubble can only contain a single Widget or Layout"
|
||||
)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
if widget == self.content:
|
||||
self.content.unbind(size=self.update_content_size)
|
||||
self.content = None
|
||||
self.content_size = [0, 0]
|
||||
self.reposition_inner_widgets()
|
||||
return
|
||||
super().remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def on_content_size(self, instance, value):
|
||||
self.adjust_position()
|
||||
|
||||
def on_limit_to(self, instance, value):
|
||||
self.adjust_position()
|
||||
|
||||
def on_pos(self, instance, value):
|
||||
self.adjust_position()
|
||||
|
||||
def on_size(self, instance, value):
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def on_arrow_image(self, instance, value):
|
||||
self._arrow_image.source = self.arrow_image
|
||||
self._arrow_image.width = self._arrow_image.texture_size[0]
|
||||
self._arrow_image.height = dp(self._arrow_image.texture_size[1])
|
||||
self._arrow_image_scatter.size = self._arrow_image.texture_size
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def on_arrow_color(self, instance, value):
|
||||
self._arrow_image.color = self.arrow_color
|
||||
|
||||
def on_arrow_pos(self, instance, value):
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def on_flex_arrow_pos(self, instance, value):
|
||||
self._flex_arrow_layout_params = self.get_flex_arrow_layout_params()
|
||||
self.reposition_inner_widgets()
|
||||
|
||||
def get_flex_arrow_layout_params(self):
|
||||
pos = self.flex_arrow_pos
|
||||
|
||||
if pos is None:
|
||||
return None
|
||||
|
||||
x, y = pos
|
||||
if not (0 <= x <= self.width and 0 <= y <= self.height):
|
||||
return None
|
||||
|
||||
# the order of the following list defines the side that the arrow
|
||||
# will be attached to in case of ambiguity (same distances)
|
||||
base_layouts_map = [
|
||||
("bottom_mid", y),
|
||||
("top_mid", self.height - y),
|
||||
("left_mid", x),
|
||||
("right_mid", self.width - x),
|
||||
]
|
||||
base_layout_key = min(base_layouts_map, key=lambda val: val[1])[0]
|
||||
arrow_layout = list(Bubble.ARROW_LAYOUTS[base_layout_key])
|
||||
|
||||
arrow_width = self._arrow_image.width
|
||||
|
||||
# This function calculates the proper value for pos_hint, i.e., the
|
||||
# arrow texture does not 'overflow' and stays entirely connected to
|
||||
# the side of the content.
|
||||
def calc_x0(x, length):
|
||||
return x * (length - arrow_width) / (length * length)
|
||||
|
||||
if base_layout_key == "bottom_mid":
|
||||
arrow_layout[-1] = {"top": 1.0, "x": calc_x0(x, self.width)}
|
||||
elif base_layout_key == "top_mid":
|
||||
arrow_layout[-1] = {"bottom": 0.0, "x": calc_x0(x, self.width)}
|
||||
elif base_layout_key == "left_mid":
|
||||
arrow_layout[-1] = {"right": 1.0, "y": calc_x0(y, self.height)}
|
||||
elif base_layout_key == "right_mid":
|
||||
arrow_layout[-1] = {"left": 0.0, "y": calc_x0(y, self.height)}
|
||||
return arrow_layout
|
||||
|
||||
def update_content_size(self, instance, value):
|
||||
self.content_size = self.content.size
|
||||
|
||||
def adjust_position(self):
|
||||
if self.limit_to is not None and not self._temporarily_ignore_limits:
|
||||
if self.limit_to is EventLoop.window:
|
||||
lim_x, lim_y = 0, 0
|
||||
lim_top, lim_right = self.limit_to.size
|
||||
else:
|
||||
lim_x = self.limit_to.x
|
||||
lim_y = self.limit_to.y
|
||||
lim_top = self.limit_to.top
|
||||
lim_right = self.limit_to.right
|
||||
|
||||
self._temporarily_ignore_limits = True
|
||||
|
||||
if not (lim_x > self.x and lim_right < self.right):
|
||||
self.x = max(lim_x, min(lim_right - self.width, self.x))
|
||||
|
||||
if not (lim_y > self.y and lim_right < self.right):
|
||||
self.y = min(lim_top - self.height, max(lim_y, self.y))
|
||||
|
||||
self._temporarily_ignore_limits = False
|
||||
|
||||
def reposition_inner_widgets(self):
|
||||
arrow_image_layout = self._arrow_image_layout
|
||||
arrow_image_scatter = self._arrow_image_scatter
|
||||
arrow_image_scatter_wrapper = self._arrow_image_scatter_wrapper
|
||||
content = self.content
|
||||
|
||||
# Remove the children of the Bubble (BoxLayout) as a first step
|
||||
for child in list(self.children):
|
||||
super().remove_widget(child)
|
||||
|
||||
if self.canvas is None or content is None:
|
||||
return
|
||||
|
||||
# find the layout parameters that define a specific bubble setup
|
||||
if self._flex_arrow_layout_params is not None:
|
||||
layout_params = self._flex_arrow_layout_params
|
||||
else:
|
||||
layout_params = Bubble.ARROW_LAYOUTS[self.arrow_pos]
|
||||
(bubble_orientation,
|
||||
widget_order,
|
||||
arrow_size_hint,
|
||||
arrow_rotation,
|
||||
arrow_pos_hint) = layout_params
|
||||
|
||||
# rotate the arrow, place it at the right pos and setup the size
|
||||
# of the widget, so the BoxLayout can do the rest.
|
||||
arrow_image_scatter.rotation = arrow_rotation
|
||||
arrow_image_scatter_wrapper.size = arrow_image_scatter.bbox[1]
|
||||
arrow_image_scatter_wrapper.pos_hint = arrow_pos_hint
|
||||
arrow_image_layout.size_hint = arrow_size_hint
|
||||
arrow_image_layout.size = arrow_image_scatter.bbox[1]
|
||||
|
||||
# set the orientation of the Bubble (BoxLayout)
|
||||
self.orientation = bubble_orientation
|
||||
|
||||
# Add the updated children of the Bubble (BoxLayout) and update
|
||||
# properties
|
||||
widgets_to_add = [content, arrow_image_layout]
|
||||
|
||||
# Set the arrow_margin, so we can use this property for proper sizing
|
||||
# of the Bubble Widget.
|
||||
# Determine whether to add the arrow_image_layout to the
|
||||
# Bubble (BoxLayout) or not.
|
||||
arrow_margin_x, arrow_margin_y = (0, 0)
|
||||
if self.show_arrow:
|
||||
if bubble_orientation[0] == "h":
|
||||
arrow_margin_x = arrow_image_layout.width
|
||||
elif bubble_orientation[0] == "v":
|
||||
arrow_margin_y = arrow_image_layout.height
|
||||
else:
|
||||
widgets_to_add.pop(1)
|
||||
|
||||
for widget in widgets_to_add[::widget_order]:
|
||||
super().add_widget(widget)
|
||||
|
||||
self.arrow_margin = (arrow_margin_x, arrow_margin_y)
|
137
kivy_venv/lib/python3.11/site-packages/kivy/uix/button.py
Normal file
137
kivy_venv/lib/python3.11/site-packages/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 color. The default
|
||||
texture is grey, so just setting the background color will give
|
||||
a darker result. To set a plain color, set the
|
||||
:attr:`background_normal` to ``''``.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
The :attr:`background_color` is a
|
||||
:class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is not pressed.
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty`
|
||||
and defaults to 'atlas://data/images/defaulttheme/button'.
|
||||
'''
|
||||
|
||||
background_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_pressed')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is pressed.
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
|
||||
:attr:`background_down` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/button_pressed'.
|
||||
'''
|
||||
|
||||
background_disabled_normal = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_disabled')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is disabled and not pressed.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled_normal` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/button_disabled'.
|
||||
'''
|
||||
|
||||
background_disabled_down = StringProperty(
|
||||
'atlas://data/images/defaulttheme/button_disabled_pressed')
|
||||
'''Background image of the button used for the default graphical
|
||||
representation when the button is disabled and pressed.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`background_disabled_down` is a
|
||||
:class:`~kivy.properties.StringProperty` and defaults to
|
||||
'atlas://data/images/defaulttheme/button_disabled_pressed'.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used with :attr:`background_normal` and
|
||||
:attr:`background_down`. Can be used for custom backgrounds.
|
||||
|
||||
It must be a list of four values: (bottom, right, top, left). Read the
|
||||
BorderImage instruction for more information about how to use it.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16)
|
||||
'''
|
118
kivy_venv/lib/python3.11/site-packages/kivy/uix/camera.py
Normal file
118
kivy_venv/lib/python3.11/site-packages/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_venv/lib/python3.11/site-packages/kivy/uix/carousel.py
Normal file
695
kivy_venv/lib/python3.11/site-packages/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, fit_mode="contain")
|
||||
carousel.add_widget(image)
|
||||
return carousel
|
||||
|
||||
|
||||
CarouselApp().run()
|
||||
|
||||
|
||||
Kv Example::
|
||||
|
||||
Carousel:
|
||||
direction: 'right'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-1.png'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-2.png'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-3.png'
|
||||
AsyncImage:
|
||||
source: 'http://placehold.it/480x270.png&text=slide-4.png'
|
||||
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The carousel now supports active children, like the
|
||||
:class:`~kivy.uix.scrollview.ScrollView`. It will detect a swipe gesture
|
||||
according to the :attr:`Carousel.scroll_timeout` and
|
||||
:attr:`Carousel.scroll_distance` properties.
|
||||
|
||||
In addition, the slide container is no longer exposed by the API.
|
||||
The impacted properties are
|
||||
:attr:`Carousel.slides`, :attr:`Carousel.current_slide`,
|
||||
:attr:`Carousel.previous_slide` and :attr:`Carousel.next_slide`.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Carousel', )
|
||||
|
||||
from functools import partial
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.stencilview import StencilView
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.properties import BooleanProperty, OptionProperty, AliasProperty, \
|
||||
NumericProperty, ListProperty, ObjectProperty, StringProperty
|
||||
|
||||
|
||||
class Carousel(StencilView):
|
||||
'''Carousel class. See module documentation for more information.
|
||||
'''
|
||||
|
||||
slides = ListProperty([])
|
||||
'''List of slides inside the Carousel. The slides are the
|
||||
widgets added to the Carousel using the :attr:`add_widget` method.
|
||||
|
||||
:attr:`slides` is a :class:`~kivy.properties.ListProperty` and is
|
||||
read-only.
|
||||
'''
|
||||
|
||||
def _get_slides_container(self):
|
||||
return [x.parent for x in self.slides]
|
||||
|
||||
slides_container = AliasProperty(_get_slides_container, bind=('slides',))
|
||||
|
||||
direction = OptionProperty('right',
|
||||
options=('right', 'left', 'top', 'bottom'))
|
||||
'''Specifies the direction in which the slides are ordered. This
|
||||
corresponds to the direction from which the user swipes to go from one
|
||||
slide to the next. It
|
||||
can be `right`, `left`, `top`, or `bottom`. For example, with
|
||||
the default value of `right`, the second slide is to the right
|
||||
of the first and the user would swipe from the right towards the
|
||||
left to get to the second slide.
|
||||
|
||||
:attr:`direction` is an :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to 'right'.
|
||||
'''
|
||||
|
||||
min_move = NumericProperty(0.2)
|
||||
'''Defines the minimum distance to be covered before the touch is
|
||||
considered a swipe gesture and the Carousel content changed.
|
||||
This is a expressed as a fraction of the Carousel's width.
|
||||
If the movement doesn't reach this minimum value, the movement is
|
||||
cancelled and the content is restored to its original position.
|
||||
|
||||
:attr:`min_move` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.2.
|
||||
'''
|
||||
|
||||
anim_move_duration = NumericProperty(0.5)
|
||||
'''Defines the duration of the Carousel animation between pages.
|
||||
|
||||
:attr:`anim_move_duration` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.5.
|
||||
'''
|
||||
|
||||
anim_cancel_duration = NumericProperty(0.3)
|
||||
'''Defines the duration of the animation when a swipe movement is not
|
||||
accepted. This is generally when the user does not make a large enough
|
||||
swipe. See :attr:`min_move`.
|
||||
|
||||
:attr:`anim_cancel_duration` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to 0.3.
|
||||
'''
|
||||
|
||||
loop = BooleanProperty(False)
|
||||
'''Allow the Carousel to loop infinitely. If True, when the user tries to
|
||||
swipe beyond last page, it will return to the first. If False, it will
|
||||
remain on the last page.
|
||||
|
||||
:attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
def _get_index(self):
|
||||
if self.slides:
|
||||
return self._index % len(self.slides)
|
||||
return None
|
||||
|
||||
def _set_index(self, value):
|
||||
if self.slides:
|
||||
self._index = value % len(self.slides)
|
||||
else:
|
||||
self._index = None
|
||||
|
||||
index = AliasProperty(_get_index, _set_index,
|
||||
bind=('_index', 'slides'),
|
||||
cache=True)
|
||||
'''Get/Set the current slide based on the index.
|
||||
|
||||
:attr:`index` is an :class:`~kivy.properties.AliasProperty` and defaults
|
||||
to 0 (the first item).
|
||||
'''
|
||||
|
||||
def _prev_slide(self):
|
||||
slides = self.slides
|
||||
len_slides = len(slides)
|
||||
index = self.index
|
||||
if len_slides < 2: # None, or 1 slide
|
||||
return None
|
||||
if self.loop and index == 0:
|
||||
return slides[-1]
|
||||
if index > 0:
|
||||
return slides[index - 1]
|
||||
|
||||
previous_slide = AliasProperty(_prev_slide,
|
||||
bind=('slides', 'index', 'loop'),
|
||||
cache=True)
|
||||
'''The previous slide in the Carousel. It is None if the current slide is
|
||||
the first slide in the Carousel. This ordering reflects the order in which
|
||||
the slides are added: their presentation varies according to the
|
||||
:attr:`direction` property.
|
||||
|
||||
:attr:`previous_slide` is an :class:`~kivy.properties.AliasProperty`.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
This property no longer exposes the slides container. It returns
|
||||
the widget you have added.
|
||||
'''
|
||||
|
||||
def _curr_slide(self):
|
||||
if len(self.slides):
|
||||
return self.slides[self.index or 0]
|
||||
|
||||
current_slide = AliasProperty(_curr_slide,
|
||||
bind=('slides', 'index'),
|
||||
cache=True)
|
||||
'''The currently shown slide.
|
||||
|
||||
:attr:`current_slide` is an :class:`~kivy.properties.AliasProperty`.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The property no longer exposes the slides container. It returns
|
||||
the widget you have added.
|
||||
'''
|
||||
|
||||
def _next_slide(self):
|
||||
if len(self.slides) < 2: # None, or 1 slide
|
||||
return None
|
||||
if self.loop and self.index == len(self.slides) - 1:
|
||||
return self.slides[0]
|
||||
if self.index < len(self.slides) - 1:
|
||||
return self.slides[self.index + 1]
|
||||
|
||||
next_slide = AliasProperty(_next_slide,
|
||||
bind=('slides', 'index', 'loop'),
|
||||
cache=True)
|
||||
'''The next slide in the Carousel. It is None if the current slide is
|
||||
the last slide in the Carousel. This ordering reflects the order in which
|
||||
the slides are added: their presentation varies according to the
|
||||
:attr:`direction` property.
|
||||
|
||||
:attr:`next_slide` is an :class:`~kivy.properties.AliasProperty`.
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
The property no longer exposes the slides container.
|
||||
It returns the widget you have added.
|
||||
'''
|
||||
|
||||
scroll_timeout = NumericProperty(200)
|
||||
'''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
|
||||
If the user has not moved :attr:`scroll_distance` within the timeout,
|
||||
no scrolling will occur and the touch event will go to the children.
|
||||
|
||||
:attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 200 (milliseconds)
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
'''
|
||||
|
||||
scroll_distance = NumericProperty('20dp')
|
||||
'''Distance to move before scrolling the :class:`Carousel` in pixels. As
|
||||
soon as the distance has been traveled, the :class:`Carousel` will start
|
||||
to scroll, and no touch event will go to children.
|
||||
It is advisable that you base this value on the dpi of your target device's
|
||||
screen.
|
||||
|
||||
:attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 20dp.
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
'''
|
||||
|
||||
anim_type = StringProperty('out_quad')
|
||||
'''Type of animation to use while animating to the next/previous slide.
|
||||
This should be the name of an
|
||||
:class:`~kivy.animation.AnimationTransition` function.
|
||||
|
||||
:attr:`anim_type` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'out_quad'.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
'''
|
||||
|
||||
ignore_perpendicular_swipes = BooleanProperty(False)
|
||||
'''Ignore swipes on axis perpendicular to direction.
|
||||
|
||||
:attr:`ignore_perpendicular_swipes` is a
|
||||
:class:`~kivy.properties.BooleanProperty` and defaults to False.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
|
||||
# private properties, for internal use only ###
|
||||
_index = NumericProperty(0, allownone=True)
|
||||
_prev = ObjectProperty(None, allownone=True)
|
||||
_current = ObjectProperty(None, allownone=True)
|
||||
_next = ObjectProperty(None, allownone=True)
|
||||
_offset = NumericProperty(0)
|
||||
_touch = ObjectProperty(None, allownone=True)
|
||||
|
||||
_change_touch_mode_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._trigger_position_visible_slides = Clock.create_trigger(
|
||||
self._position_visible_slides, -1)
|
||||
super(Carousel, self).__init__(**kwargs)
|
||||
self._skip_slide = None
|
||||
self.touch_mode_change = False
|
||||
self._prioritize_next = False
|
||||
self.fbind('loop', lambda *args: self._insert_visible_slides())
|
||||
|
||||
def load_slide(self, slide):
|
||||
'''Animate to the slide that is passed as the argument.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
'''
|
||||
slides = self.slides
|
||||
start, stop = slides.index(self.current_slide), slides.index(slide)
|
||||
if start == stop:
|
||||
return
|
||||
|
||||
self._skip_slide = stop
|
||||
if stop > start:
|
||||
self._prioritize_next = True
|
||||
self._insert_visible_slides(_next_slide=slide)
|
||||
self.load_next()
|
||||
else:
|
||||
self._prioritize_next = False
|
||||
self._insert_visible_slides(_prev_slide=slide)
|
||||
self.load_previous()
|
||||
|
||||
def load_previous(self):
|
||||
'''Animate to the previous slide.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
'''
|
||||
self.load_next(mode='prev')
|
||||
|
||||
def load_next(self, mode='next'):
|
||||
'''Animate to the next slide.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
'''
|
||||
if self.index is not None:
|
||||
w, h = self.size
|
||||
_direction = {
|
||||
'top': -h / 2,
|
||||
'bottom': h / 2,
|
||||
'left': w / 2,
|
||||
'right': -w / 2}
|
||||
_offset = _direction[self.direction]
|
||||
if mode == 'prev':
|
||||
_offset = -_offset
|
||||
|
||||
self._start_animation(min_move=0, offset=_offset)
|
||||
|
||||
def get_slide_container(self, slide):
|
||||
return slide.parent
|
||||
|
||||
@property
|
||||
def _prev_equals_next(self):
|
||||
return self.loop and len(self.slides) == 2
|
||||
|
||||
def _insert_visible_slides(self, _next_slide=None, _prev_slide=None):
|
||||
get_slide_container = self.get_slide_container
|
||||
|
||||
previous_slide = _prev_slide if _prev_slide else self.previous_slide
|
||||
if previous_slide:
|
||||
self._prev = get_slide_container(previous_slide)
|
||||
else:
|
||||
self._prev = None
|
||||
|
||||
current_slide = self.current_slide
|
||||
if current_slide:
|
||||
self._current = get_slide_container(current_slide)
|
||||
else:
|
||||
self._current = None
|
||||
|
||||
next_slide = _next_slide if _next_slide else self.next_slide
|
||||
if next_slide:
|
||||
self._next = get_slide_container(next_slide)
|
||||
else:
|
||||
self._next = None
|
||||
|
||||
if self._prev_equals_next:
|
||||
setattr(self, '_prev' if self._prioritize_next else '_next', None)
|
||||
|
||||
super_remove = super(Carousel, self).remove_widget
|
||||
for container in self.slides_container:
|
||||
super_remove(container)
|
||||
|
||||
if self._prev and self._prev.parent is not self:
|
||||
super(Carousel, self).add_widget(self._prev)
|
||||
if self._next and self._next.parent is not self:
|
||||
super(Carousel, self).add_widget(self._next)
|
||||
if self._current:
|
||||
super(Carousel, self).add_widget(self._current)
|
||||
|
||||
def _position_visible_slides(self, *args):
|
||||
slides, index = self.slides, self.index
|
||||
no_of_slides = len(slides) - 1
|
||||
if not slides:
|
||||
return
|
||||
x, y, width, height = self.x, self.y, self.width, self.height
|
||||
_offset, direction = self._offset, self.direction[0]
|
||||
_prev, _next, _current = self._prev, self._next, self._current
|
||||
get_slide_container = self.get_slide_container
|
||||
last_slide = get_slide_container(slides[-1])
|
||||
first_slide = get_slide_container(slides[0])
|
||||
skip_next = False
|
||||
_loop = self.loop
|
||||
|
||||
if direction in 'rl':
|
||||
xoff = x + _offset
|
||||
x_prev = {'l': xoff + width, 'r': xoff - width}
|
||||
x_next = {'l': xoff - width, 'r': xoff + width}
|
||||
if _prev:
|
||||
_prev.pos = (x_prev[direction], y)
|
||||
elif _loop and _next and index == 0:
|
||||
# if first slide is moving to right with direction set to right
|
||||
# or toward left with direction set to left
|
||||
if ((_offset > 0 and direction == 'r') or
|
||||
(_offset < 0 and direction == 'l')):
|
||||
# put last_slide before first slide
|
||||
last_slide.pos = (x_prev[direction], y)
|
||||
skip_next = True
|
||||
if _current:
|
||||
_current.pos = (xoff, y)
|
||||
if skip_next:
|
||||
return
|
||||
if _next:
|
||||
_next.pos = (x_next[direction], y)
|
||||
elif _loop and _prev and index == no_of_slides:
|
||||
if ((_offset < 0 and direction == 'r') or
|
||||
(_offset > 0 and direction == 'l')):
|
||||
first_slide.pos = (x_next[direction], y)
|
||||
if direction in 'tb':
|
||||
yoff = y + _offset
|
||||
y_prev = {'t': yoff - height, 'b': yoff + height}
|
||||
y_next = {'t': yoff + height, 'b': yoff - height}
|
||||
if _prev:
|
||||
_prev.pos = (x, y_prev[direction])
|
||||
elif _loop and _next and index == 0:
|
||||
if ((_offset > 0 and direction == 't') or
|
||||
(_offset < 0 and direction == 'b')):
|
||||
last_slide.pos = (x, y_prev[direction])
|
||||
skip_next = True
|
||||
if _current:
|
||||
_current.pos = (x, yoff)
|
||||
if skip_next:
|
||||
return
|
||||
if _next:
|
||||
_next.pos = (x, y_next[direction])
|
||||
elif _loop and _prev and index == no_of_slides:
|
||||
if ((_offset < 0 and direction == 't') or
|
||||
(_offset > 0 and direction == 'b')):
|
||||
first_slide.pos = (x, y_next[direction])
|
||||
|
||||
def on_size(self, *args):
|
||||
size = self.size
|
||||
for slide in self.slides_container:
|
||||
slide.size = size
|
||||
self._trigger_position_visible_slides()
|
||||
|
||||
def on_pos(self, *args):
|
||||
self._trigger_position_visible_slides()
|
||||
|
||||
def on_index(self, *args):
|
||||
self._insert_visible_slides()
|
||||
self._trigger_position_visible_slides()
|
||||
self._offset = 0
|
||||
|
||||
def on_slides(self, *args):
|
||||
if self.slides:
|
||||
self.index = self.index % len(self.slides)
|
||||
self._insert_visible_slides()
|
||||
self._trigger_position_visible_slides()
|
||||
|
||||
def on__offset(self, *args):
|
||||
self._trigger_position_visible_slides()
|
||||
# if reached full offset, switch index to next or prev
|
||||
direction = self.direction[0]
|
||||
_offset = self._offset
|
||||
width = self.width
|
||||
height = self.height
|
||||
index = self.index
|
||||
if self._skip_slide is not None or index is None:
|
||||
return
|
||||
|
||||
# Move to next slide?
|
||||
if (direction == 'r' and _offset <= -width) or \
|
||||
(direction == 'l' and _offset >= width) or \
|
||||
(direction == 't' and _offset <= - height) or \
|
||||
(direction == 'b' and _offset >= height):
|
||||
if self.next_slide:
|
||||
self.index += 1
|
||||
|
||||
# Move to previous slide?
|
||||
elif (direction == 'r' and _offset >= width) or \
|
||||
(direction == 'l' and _offset <= -width) or \
|
||||
(direction == 't' and _offset >= height) or \
|
||||
(direction == 'b' and _offset <= -height):
|
||||
if self.previous_slide:
|
||||
self.index -= 1
|
||||
|
||||
elif self._prev_equals_next:
|
||||
new_value = (_offset < 0) is (direction in 'rt')
|
||||
if self._prioritize_next is not new_value:
|
||||
self._prioritize_next = new_value
|
||||
if new_value is (self._next is None):
|
||||
self._prev, self._next = self._next, self._prev
|
||||
|
||||
def _start_animation(self, *args, **kwargs):
|
||||
# compute target offset for ease back, next or prev
|
||||
new_offset = 0
|
||||
direction = kwargs.get('direction', self.direction)[0]
|
||||
is_horizontal = direction in 'rl'
|
||||
extent = self.width if is_horizontal else self.height
|
||||
min_move = kwargs.get('min_move', self.min_move)
|
||||
_offset = kwargs.get('offset', self._offset)
|
||||
|
||||
if _offset < min_move * -extent:
|
||||
new_offset = -extent
|
||||
elif _offset > min_move * extent:
|
||||
new_offset = extent
|
||||
|
||||
# if new_offset is 0, it wasn't enough to go next/prev
|
||||
dur = self.anim_move_duration
|
||||
if new_offset == 0:
|
||||
dur = self.anim_cancel_duration
|
||||
|
||||
# detect edge cases if not looping
|
||||
len_slides = len(self.slides)
|
||||
index = self.index
|
||||
if not self.loop or len_slides == 1:
|
||||
is_first = (index == 0)
|
||||
is_last = (index == len_slides - 1)
|
||||
if direction in 'rt':
|
||||
towards_prev = (new_offset > 0)
|
||||
towards_next = (new_offset < 0)
|
||||
else:
|
||||
towards_prev = (new_offset < 0)
|
||||
towards_next = (new_offset > 0)
|
||||
if (is_first and towards_prev) or (is_last and towards_next):
|
||||
new_offset = 0
|
||||
|
||||
anim = Animation(_offset=new_offset, d=dur, t=self.anim_type)
|
||||
anim.cancel_all(self)
|
||||
|
||||
def _cmp(*l):
|
||||
if self._skip_slide is not None:
|
||||
self.index = self._skip_slide
|
||||
self._skip_slide = None
|
||||
|
||||
anim.bind(on_complete=_cmp)
|
||||
anim.start(self)
|
||||
|
||||
def _get_uid(self, prefix='sv'):
|
||||
return '{0}.{1}'.format(prefix, self.uid)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
touch.ud[self._get_uid('cavoid')] = True
|
||||
return
|
||||
if self.disabled:
|
||||
return True
|
||||
if self._touch:
|
||||
return super(Carousel, self).on_touch_down(touch)
|
||||
Animation.cancel_all(self)
|
||||
self._touch = touch
|
||||
uid = self._get_uid()
|
||||
touch.grab(self)
|
||||
touch.ud[uid] = {
|
||||
'mode': 'unknown',
|
||||
'time': touch.time_start}
|
||||
self._change_touch_mode_ev = Clock.schedule_once(
|
||||
self._change_touch_mode, self.scroll_timeout / 1000.)
|
||||
self.touch_mode_change = False
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if not self.touch_mode_change:
|
||||
if self.ignore_perpendicular_swipes and \
|
||||
self.direction in ('top', 'bottom'):
|
||||
if abs(touch.oy - touch.y) < self.scroll_distance:
|
||||
if abs(touch.ox - touch.x) > self.scroll_distance:
|
||||
self._change_touch_mode()
|
||||
self.touch_mode_change = True
|
||||
elif self.ignore_perpendicular_swipes and \
|
||||
self.direction in ('right', 'left'):
|
||||
if abs(touch.ox - touch.x) < self.scroll_distance:
|
||||
if abs(touch.oy - touch.y) > self.scroll_distance:
|
||||
self._change_touch_mode()
|
||||
self.touch_mode_change = True
|
||||
|
||||
if self._get_uid('cavoid') in touch.ud:
|
||||
return
|
||||
if self._touch is not touch:
|
||||
super(Carousel, self).on_touch_move(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
if touch.grab_current is not self:
|
||||
return True
|
||||
ud = touch.ud[self._get_uid()]
|
||||
direction = self.direction[0]
|
||||
if ud['mode'] == 'unknown':
|
||||
if direction in 'rl':
|
||||
distance = abs(touch.ox - touch.x)
|
||||
else:
|
||||
distance = abs(touch.oy - touch.y)
|
||||
if distance > self.scroll_distance:
|
||||
ev = self._change_touch_mode_ev
|
||||
if ev is not None:
|
||||
ev.cancel()
|
||||
ud['mode'] = 'scroll'
|
||||
else:
|
||||
if direction in 'rl':
|
||||
self._offset += touch.dx
|
||||
if direction in 'tb':
|
||||
self._offset += touch.dy
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self._get_uid('cavoid') in touch.ud:
|
||||
return
|
||||
if self in [x() for x in touch.grab_list]:
|
||||
touch.ungrab(self)
|
||||
self._touch = None
|
||||
ud = touch.ud[self._get_uid()]
|
||||
if ud['mode'] == 'unknown':
|
||||
ev = self._change_touch_mode_ev
|
||||
if ev is not None:
|
||||
ev.cancel()
|
||||
super(Carousel, self).on_touch_down(touch)
|
||||
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
|
||||
else:
|
||||
self._start_animation()
|
||||
|
||||
else:
|
||||
if self._touch is not touch and self.uid not in touch.ud:
|
||||
super(Carousel, self).on_touch_up(touch)
|
||||
return self._get_uid() in touch.ud
|
||||
|
||||
def _do_touch_up(self, touch, *largs):
|
||||
super(Carousel, self).on_touch_up(touch)
|
||||
# don't forget about grab event!
|
||||
for x in touch.grab_list[:]:
|
||||
touch.grab_list.remove(x)
|
||||
x = x()
|
||||
if not x:
|
||||
continue
|
||||
touch.grab_current = x
|
||||
super(Carousel, self).on_touch_up(touch)
|
||||
touch.grab_current = None
|
||||
|
||||
def _change_touch_mode(self, *largs):
|
||||
if not self._touch:
|
||||
return
|
||||
self._start_animation()
|
||||
uid = self._get_uid()
|
||||
touch = self._touch
|
||||
ud = touch.ud[uid]
|
||||
if ud['mode'] == 'unknown':
|
||||
touch.ungrab(self)
|
||||
self._touch = None
|
||||
super(Carousel, self).on_touch_down(touch)
|
||||
return
|
||||
|
||||
def add_widget(self, widget, index=0, *args, **kwargs):
|
||||
container = RelativeLayout(
|
||||
size=self.size, x=self.x - self.width, y=self.y)
|
||||
container.add_widget(widget)
|
||||
super(Carousel, self).add_widget(container, index, *args, **kwargs)
|
||||
if index != 0:
|
||||
self.slides.insert(index - len(self.slides), widget)
|
||||
else:
|
||||
self.slides.append(widget)
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
# XXX be careful, the widget.parent refer to the RelativeLayout
|
||||
# added in add_widget(). But it will break if RelativeLayout
|
||||
# implementation change.
|
||||
# if we passed the real widget
|
||||
slides = self.slides
|
||||
if widget in slides:
|
||||
if self.index >= slides.index(widget):
|
||||
self.index = max(0, self.index - 1)
|
||||
container = widget.parent
|
||||
slides.remove(widget)
|
||||
super(Carousel, self).remove_widget(container, *args, **kwargs)
|
||||
container.remove_widget(widget)
|
||||
return
|
||||
super(Carousel, self).remove_widget(widget, *args, **kwargs)
|
||||
|
||||
def clear_widgets(self, children=None, *args, **kwargs):
|
||||
# `children` must be a list of slides or None
|
||||
if children is None:
|
||||
children = self.slides[:]
|
||||
remove_widget = self.remove_widget
|
||||
for widget in children:
|
||||
remove_widget(widget)
|
||||
super(Carousel, self).clear_widgets()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.app import App
|
||||
|
||||
class Example1(App):
|
||||
|
||||
def build(self):
|
||||
carousel = Carousel(direction='left',
|
||||
loop=True)
|
||||
for i in range(4):
|
||||
src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i
|
||||
image = Factory.AsyncImage(source=src, fit_mode="contain")
|
||||
carousel.add_widget(image)
|
||||
return carousel
|
||||
|
||||
Example1().run()
|
197
kivy_venv/lib/python3.11/site-packages/kivy/uix/checkbox.py
Normal file
197
kivy_venv/lib/python3.11/site-packages/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_venv/lib/python3.11/site-packages/kivy/uix/codeinput.py
Normal file
238
kivy_venv/lib/python3.11/site-packages/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()
|
486
kivy_venv/lib/python3.11/site-packages/kivy/uix/colorpicker.py
Normal file
486
kivy_venv/lib/python3.11/site-packages/kivy/uix/colorpicker.py
Normal file
|
@ -0,0 +1,486 @@
|
|||
'''
|
||||
Color Picker
|
||||
============
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
|
||||
.. warning::
|
||||
|
||||
This widget is experimental. Its use and API can change at any time until
|
||||
this warning is removed.
|
||||
|
||||
.. image:: images/colorpicker.png
|
||||
:align: right
|
||||
|
||||
The ColorPicker widget allows a user to select a color from a chromatic
|
||||
wheel where pinch and zoom can be used to change the wheel's saturation.
|
||||
Sliders and TextInputs are also provided for entering the RGBA/HSV/HEX values
|
||||
directly.
|
||||
|
||||
Usage::
|
||||
|
||||
clr_picker = ColorPicker()
|
||||
parent.add_widget(clr_picker)
|
||||
|
||||
# To monitor changes, we can bind to color property changes
|
||||
def on_color(instance, value):
|
||||
print("RGBA = ", str(value)) # or instance.color
|
||||
print("HSV = ", str(instance.hsv))
|
||||
print("HEX = ", str(instance.hex_color))
|
||||
|
||||
clr_picker.bind(color=on_color)
|
||||
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ColorPicker', 'ColorWheel')
|
||||
|
||||
from math import cos, sin, pi, sqrt, atan
|
||||
from colorsys import rgb_to_hsv, hsv_to_rgb
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import Mesh, InstructionGroup, Color
|
||||
from kivy.logger import Logger
|
||||
from kivy.properties import (NumericProperty, BoundedNumericProperty,
|
||||
ListProperty, ObjectProperty,
|
||||
ReferenceListProperty, StringProperty,
|
||||
AliasProperty)
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.utils import get_color_from_hex, get_hex_from_color
|
||||
|
||||
|
||||
def distance(pt1, pt2):
|
||||
return sqrt((pt1[0] - pt2[0]) ** 2. + (pt1[1] - pt2[1]) ** 2.)
|
||||
|
||||
|
||||
def polar_to_rect(origin, r, theta):
|
||||
return origin[0] + r * cos(theta), origin[1] + r * sin(theta)
|
||||
|
||||
|
||||
def rect_to_polar(origin, x, y):
|
||||
if x == origin[0]:
|
||||
if y == origin[1]:
|
||||
return 0, 0
|
||||
elif y > origin[1]:
|
||||
return y - origin[1], pi / 2
|
||||
else:
|
||||
return origin[1] - y, 3 * pi / 2
|
||||
t = atan(float((y - origin[1])) / (x - origin[0]))
|
||||
if x - origin[0] < 0:
|
||||
t += pi
|
||||
|
||||
if t < 0:
|
||||
t += 2 * pi
|
||||
|
||||
return distance((x, y), origin), t
|
||||
|
||||
|
||||
class ColorWheel(Widget):
|
||||
'''Chromatic wheel for the ColorPicker.
|
||||
|
||||
.. versionchanged:: 1.7.1
|
||||
`font_size`, `font_name` and `foreground_color` have been removed. The
|
||||
sizing is now the same as others widget, based on 'sp'. Orientation is
|
||||
also automatically determined according to the width/height ratio.
|
||||
|
||||
'''
|
||||
|
||||
r = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Red value of the color currently selected.
|
||||
|
||||
:attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and
|
||||
can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
g = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Green value of the color currently selected.
|
||||
|
||||
:attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty`
|
||||
and can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
b = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Blue value of the color currently selected.
|
||||
|
||||
:attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and
|
||||
can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
a = BoundedNumericProperty(0, min=0, max=1)
|
||||
'''The Alpha value of the color currently selected.
|
||||
|
||||
:attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and
|
||||
can be a value from 0 to 1. It defaults to 0.
|
||||
'''
|
||||
|
||||
color = ReferenceListProperty(r, g, b, a)
|
||||
'''The holds the color currently selected.
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and
|
||||
contains a list of `r`, `g`, `b`, `a` values. It defaults to `[0, 0, 0, 0]`.
|
||||
'''
|
||||
|
||||
_origin = ListProperty((100, 100))
|
||||
_radius = NumericProperty(100)
|
||||
|
||||
_piece_divisions = NumericProperty(10)
|
||||
_pieces_of_pie = NumericProperty(16)
|
||||
|
||||
_inertia_slowdown = 1.25
|
||||
_inertia_cutoff = .25
|
||||
|
||||
_num_touches = 0
|
||||
_pinch_flag = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.arcs = []
|
||||
self.sv_idx = 0
|
||||
|
||||
pdv = self._piece_divisions
|
||||
self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [
|
||||
(1, float(y) / pdv) for y in reversed(range(pdv))]
|
||||
|
||||
super(ColorWheel, self).__init__(**kwargs)
|
||||
|
||||
def on__origin(self, _instance, _value):
|
||||
self._reset_canvas()
|
||||
|
||||
def on__radius(self, _instance, _value):
|
||||
self._reset_canvas()
|
||||
|
||||
def _reset_canvas(self):
|
||||
# initialize list to hold all meshes
|
||||
self.canvas.clear()
|
||||
self.arcs = []
|
||||
self.sv_idx = 0
|
||||
pdv = self._piece_divisions
|
||||
ppie = self._pieces_of_pie
|
||||
|
||||
for r in range(pdv):
|
||||
for t in range(ppie):
|
||||
self.arcs.append(
|
||||
_ColorArc(
|
||||
self._radius * (float(r) / float(pdv)),
|
||||
self._radius * (float(r + 1) / float(pdv)),
|
||||
2 * pi * (float(t) / float(ppie)),
|
||||
2 * pi * (float(t + 1) / float(ppie)),
|
||||
origin=self._origin,
|
||||
color=(float(t) / ppie,
|
||||
self.sv_s[self.sv_idx + r][0],
|
||||
self.sv_s[self.sv_idx + r][1],
|
||||
1)))
|
||||
|
||||
self.canvas.add(self.arcs[-1])
|
||||
|
||||
def recolor_wheel(self):
|
||||
ppie = self._pieces_of_pie
|
||||
for idx, segment in enumerate(self.arcs):
|
||||
segment.change_color(
|
||||
sv=self.sv_s[int(self.sv_idx + idx / ppie)])
|
||||
|
||||
def change_alpha(self, val):
|
||||
for idx, segment in enumerate(self.arcs):
|
||||
segment.change_color(a=val)
|
||||
|
||||
def inertial_incr_sv_idx(self, dt):
|
||||
# if its already zoomed all the way out, cancel the inertial zoom
|
||||
if self.sv_idx == len(self.sv_s) - self._piece_divisions:
|
||||
return False
|
||||
|
||||
self.sv_idx += 1
|
||||
self.recolor_wheel()
|
||||
if dt * self._inertia_slowdown > self._inertia_cutoff:
|
||||
return False
|
||||
else:
|
||||
Clock.schedule_once(self.inertial_incr_sv_idx,
|
||||
dt * self._inertia_slowdown)
|
||||
|
||||
def inertial_decr_sv_idx(self, dt):
|
||||
# if its already zoomed all the way in, cancel the inertial zoom
|
||||
if self.sv_idx == 0:
|
||||
return False
|
||||
self.sv_idx -= 1
|
||||
self.recolor_wheel()
|
||||
if dt * self._inertia_slowdown > self._inertia_cutoff:
|
||||
return False
|
||||
else:
|
||||
Clock.schedule_once(self.inertial_decr_sv_idx,
|
||||
dt * self._inertia_slowdown)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
r = self._get_touch_r(touch.pos)
|
||||
if r > self._radius:
|
||||
return False
|
||||
|
||||
# code is still set up to allow pinch to zoom, but this is
|
||||
# disabled for now since it was fiddly with small wheels.
|
||||
# Comment out these lines and adjust on_touch_move to reenable
|
||||
# this.
|
||||
if self._num_touches != 0:
|
||||
return False
|
||||
|
||||
touch.grab(self)
|
||||
self._num_touches += 1
|
||||
touch.ud['anchor_r'] = r
|
||||
touch.ud['orig_sv_idx'] = self.sv_idx
|
||||
touch.ud['orig_time'] = Clock.get_time()
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
r = self._get_touch_r(touch.pos)
|
||||
goal_sv_idx = (touch.ud['orig_sv_idx'] -
|
||||
int((r - touch.ud['anchor_r']) /
|
||||
(float(self._radius) / self._piece_divisions)))
|
||||
|
||||
if (
|
||||
goal_sv_idx != self.sv_idx and
|
||||
0 <= goal_sv_idx <= len(self.sv_s) - self._piece_divisions
|
||||
):
|
||||
# this is a pinch to zoom
|
||||
self._pinch_flag = True
|
||||
self.sv_idx = goal_sv_idx
|
||||
self.recolor_wheel()
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self._num_touches -= 1
|
||||
if self._pinch_flag:
|
||||
if self._num_touches == 0:
|
||||
# user was pinching, and now both fingers are up. Return
|
||||
# to normal
|
||||
if self.sv_idx > touch.ud['orig_sv_idx']:
|
||||
Clock.schedule_once(
|
||||
self.inertial_incr_sv_idx,
|
||||
(Clock.get_time() - touch.ud['orig_time']) /
|
||||
(self.sv_idx - touch.ud['orig_sv_idx']))
|
||||
|
||||
if self.sv_idx < touch.ud['orig_sv_idx']:
|
||||
Clock.schedule_once(
|
||||
self.inertial_decr_sv_idx,
|
||||
(Clock.get_time() - touch.ud['orig_time']) /
|
||||
(self.sv_idx - touch.ud['orig_sv_idx']))
|
||||
|
||||
self._pinch_flag = False
|
||||
return
|
||||
else:
|
||||
# user was pinching, and at least one finger remains. We
|
||||
# don't want to treat the remaining fingers as touches
|
||||
return
|
||||
else:
|
||||
r, theta = rect_to_polar(self._origin, *touch.pos)
|
||||
# if touch up is outside the wheel, ignore
|
||||
if r >= self._radius:
|
||||
return
|
||||
# compute which ColorArc is being touched (they aren't
|
||||
# widgets so we don't get collide_point) and set
|
||||
# _hsv based on the selected ColorArc
|
||||
piece = int((theta / (2 * pi)) * self._pieces_of_pie)
|
||||
division = int((r / self._radius) * self._piece_divisions)
|
||||
hsva = list(
|
||||
self.arcs[self._pieces_of_pie * division + piece].color)
|
||||
self.color = list(hsv_to_rgb(*hsva[:3])) + hsva[-1:]
|
||||
|
||||
def _get_touch_r(self, pos):
|
||||
return distance(pos, self._origin)
|
||||
|
||||
|
||||
class _ColorArc(InstructionGroup):
|
||||
def __init__(self, r_min, r_max, theta_min, theta_max,
|
||||
color=(0, 0, 1, 1), origin=(0, 0), **kwargs):
|
||||
super(_ColorArc, self).__init__(**kwargs)
|
||||
self.origin = origin
|
||||
self.r_min = r_min
|
||||
self.r_max = r_max
|
||||
self.theta_min = theta_min
|
||||
self.theta_max = theta_max
|
||||
self.color = color
|
||||
self.color_instr = Color(*color, mode='hsv')
|
||||
self.add(self.color_instr)
|
||||
self.mesh = self.get_mesh()
|
||||
self.add(self.mesh)
|
||||
|
||||
def __str__(self):
|
||||
return "r_min: %s r_max: %s theta_min: %s theta_max: %s color: %s" % (
|
||||
self.r_min, self.r_max, self.theta_min, self.theta_max, self.color
|
||||
)
|
||||
|
||||
def get_mesh(self):
|
||||
v = []
|
||||
# first calculate the distance between endpoints of the outer
|
||||
# arc, so we know how many steps to use when calculating
|
||||
# vertices
|
||||
theta_step_outer = 0.1
|
||||
theta = self.theta_max - self.theta_min
|
||||
d_outer = int(theta / theta_step_outer)
|
||||
theta_step_outer = theta / d_outer
|
||||
|
||||
if self.r_min == 0:
|
||||
for x in range(0, d_outer, 2):
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + x * theta_step_outer
|
||||
) * 2)
|
||||
v += polar_to_rect(self.origin, 0, 0) * 2
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + (x + 1) * theta_step_outer
|
||||
) * 2)
|
||||
if not d_outer & 1: # add a last point if d_outer is even
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + d_outer * theta_step_outer
|
||||
) * 2)
|
||||
else:
|
||||
for x in range(d_outer + 1):
|
||||
v += (polar_to_rect(self.origin, self.r_min,
|
||||
self.theta_min + x * theta_step_outer
|
||||
) * 2)
|
||||
v += (polar_to_rect(self.origin, self.r_max,
|
||||
self.theta_min + x * theta_step_outer
|
||||
) * 2)
|
||||
|
||||
return Mesh(vertices=v, indices=range(int(len(v) / 4)),
|
||||
mode='triangle_strip')
|
||||
|
||||
def change_color(self, color=None, color_delta=None, sv=None, a=None):
|
||||
self.remove(self.color_instr)
|
||||
if color is not None:
|
||||
self.color = color
|
||||
elif color_delta is not None:
|
||||
self.color = [self.color[i] + color_delta[i] for i in range(4)]
|
||||
elif sv is not None:
|
||||
self.color = (self.color[0], sv[0], sv[1], self.color[3])
|
||||
elif a is not None:
|
||||
self.color = (self.color[0], self.color[1], self.color[2], a)
|
||||
self.color_instr = Color(*self.color, mode='hsv')
|
||||
self.insert(0, self.color_instr)
|
||||
|
||||
|
||||
class ColorPicker(RelativeLayout):
|
||||
'''
|
||||
See module documentation.
|
||||
'''
|
||||
|
||||
font_name = StringProperty('data/fonts/RobotoMono-Regular.ttf')
|
||||
'''Specifies the font used on the ColorPicker.
|
||||
|
||||
:attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'data/fonts/RobotoMono-Regular.ttf'.
|
||||
'''
|
||||
|
||||
color = ListProperty((1, 1, 1, 1))
|
||||
'''The :attr:`color` holds the color currently selected in rgba format.
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(1, 1, 1, 1).
|
||||
'''
|
||||
|
||||
def _get_hsv(self):
|
||||
return rgb_to_hsv(*self.color[:3])
|
||||
|
||||
def _set_hsv(self, value):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self.set_color(value)
|
||||
|
||||
hsv = AliasProperty(_get_hsv, _set_hsv, bind=('color', ))
|
||||
'''The :attr:`hsv` holds the color currently selected in hsv format.
|
||||
|
||||
:attr:`hsv` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(1, 1, 1).
|
||||
'''
|
||||
def _get_hex(self):
|
||||
return get_hex_from_color(self.color)
|
||||
|
||||
def _set_hex(self, value):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self.set_color(get_color_from_hex(value)[:4])
|
||||
|
||||
hex_color = AliasProperty(_get_hex, _set_hex, bind=('color',), cache=True)
|
||||
'''The :attr:`hex_color` holds the currently selected color in hex.
|
||||
|
||||
:attr:`hex_color` is an :class:`~kivy.properties.AliasProperty` and
|
||||
defaults to `#ffffffff`.
|
||||
'''
|
||||
|
||||
wheel = ObjectProperty(None)
|
||||
'''The :attr:`wheel` holds the color wheel.
|
||||
|
||||
:attr:`wheel` is an :class:`~kivy.properties.ObjectProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
_update_clr_ev = _update_hex_ev = None
|
||||
|
||||
# now used only internally.
|
||||
foreground_color = ListProperty((1, 1, 1, 1))
|
||||
|
||||
def _trigger_update_clr(self, mode, clr_idx, text):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self._updating_clr = True
|
||||
self._upd_clr_list = mode, clr_idx, text
|
||||
ev = self._update_clr_ev
|
||||
if ev is None:
|
||||
ev = self._update_clr_ev = Clock.create_trigger(self._update_clr)
|
||||
ev()
|
||||
|
||||
def _update_clr(self, dt):
|
||||
# to prevent interaction between hsv/rgba, we work internally using rgba
|
||||
mode, clr_idx, text = self._upd_clr_list
|
||||
try:
|
||||
text = min(255.0, max(0.0, float(text)))
|
||||
if mode == 'rgb':
|
||||
self.color[clr_idx] = text / 255
|
||||
else:
|
||||
hsv = list(self.hsv[:])
|
||||
hsv[clr_idx] = text / 255
|
||||
self.color[:3] = hsv_to_rgb(*hsv)
|
||||
except ValueError:
|
||||
Logger.warning('ColorPicker: invalid value : {}'.format(text))
|
||||
finally:
|
||||
self._updating_clr = False
|
||||
|
||||
def _update_hex(self, dt):
|
||||
try:
|
||||
if len(self._upd_hex_list) != 9:
|
||||
return
|
||||
self._updating_clr = False
|
||||
self.hex_color = self._upd_hex_list
|
||||
finally:
|
||||
self._updating_clr = False
|
||||
|
||||
def _trigger_update_hex(self, text):
|
||||
if self._updating_clr:
|
||||
return
|
||||
self._updating_clr = True
|
||||
self._upd_hex_list = text
|
||||
ev = self._update_hex_ev
|
||||
if ev is None:
|
||||
ev = self._update_hex_ev = Clock.create_trigger(self._update_hex)
|
||||
ev()
|
||||
|
||||
def set_color(self, color):
|
||||
self._updating_clr = True
|
||||
if len(color) == 3:
|
||||
self.color[:3] = color
|
||||
else:
|
||||
self.color = color
|
||||
self._updating_clr = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._updating_clr = False
|
||||
super(ColorPicker, self).__init__(**kwargs)
|
||||
|
||||
|
||||
if __name__ in ('__android__', '__main__'):
|
||||
from kivy.app import App
|
||||
|
||||
class ColorPickerApp(App):
|
||||
def build(self):
|
||||
cp = ColorPicker(pos_hint={'center_x': .5, 'center_y': .5},
|
||||
size_hint=(1, 1))
|
||||
return cp
|
||||
ColorPickerApp().run()
|
391
kivy_venv/lib/python3.11/site-packages/kivy/uix/dropdown.py
Normal file
391
kivy_venv/lib/python3.11/site-packages/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_venv/lib/python3.11/site-packages/kivy/uix/effectwidget.py
Normal file
772
kivy_venv/lib/python3.11/site-packages/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_venv/lib/python3.11/site-packages/kivy/uix/filechooser.py
Normal file
1134
kivy_venv/lib/python3.11/site-packages/kivy/uix/filechooser.py
Normal file
File diff suppressed because it is too large
Load diff
148
kivy_venv/lib/python3.11/site-packages/kivy/uix/floatlayout.py
Normal file
148
kivy_venv/lib/python3.11/site-packages/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)
|
|
@ -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_venv/lib/python3.11/site-packages/kivy/uix/gridlayout.py
Normal file
629
kivy_venv/lib/python3.11/site-packages/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)
|
528
kivy_venv/lib/python3.11/site-packages/kivy/uix/image.py
Normal file
528
kivy_venv/lib/python3.11/site-packages/kivy/uix/image.py
Normal file
|
@ -0,0 +1,528 @@
|
|||
'''
|
||||
Image
|
||||
=====
|
||||
|
||||
The :class:`Image` widget is used to display an image::
|
||||
|
||||
Example in python::
|
||||
|
||||
wimg = Image(source='mylogo.png')
|
||||
|
||||
Kv Example::
|
||||
|
||||
Image:
|
||||
source: 'mylogo.png'
|
||||
size: self.texture_size
|
||||
|
||||
|
||||
Asynchronous Loading
|
||||
--------------------
|
||||
|
||||
To load an image asynchronously (for example from an external webserver), use
|
||||
the :class:`AsyncImage` subclass::
|
||||
|
||||
aimg = AsyncImage(source='http://mywebsite.com/logo.png')
|
||||
|
||||
This can be useful as it prevents your application from waiting until the image
|
||||
is loaded. If you want to display large images or retrieve them from URL's,
|
||||
using :class:`AsyncImage` will allow these resources to be retrieved on a
|
||||
background thread without blocking your application.
|
||||
|
||||
Alignment
|
||||
---------
|
||||
|
||||
By default, the image is centered inside the widget bounding box.
|
||||
|
||||
Adjustment
|
||||
----------
|
||||
|
||||
To control how the image should be adjusted to fit inside the widget box, you
|
||||
should use the :attr:`~kivy.uix.image.Image.fit_mode` property. Available
|
||||
options include:
|
||||
|
||||
- ``"scale-down"``: maintains aspect ratio without stretching.
|
||||
- ``"fill"``: stretches to fill widget, may cause distortion.
|
||||
- ``"contain"``: maintains aspect ratio and resizes to fit inside widget.
|
||||
- ``"cover"``: maintains aspect ratio and stretches to fill widget, may clip
|
||||
image.
|
||||
|
||||
For more details, refer to the :attr:`~kivy.uix.image.Image.fit_mode`.
|
||||
|
||||
|
||||
You can also inherit from Image and create your own style. For example, if you
|
||||
want your image to be greater than the size of your widget, you could do::
|
||||
|
||||
class FullImage(Image):
|
||||
pass
|
||||
|
||||
And in your kivy language file::
|
||||
|
||||
<-FullImage>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: (1, 1, 1)
|
||||
Rectangle:
|
||||
texture: self.texture
|
||||
size: self.width + 20, self.height + 20
|
||||
pos: self.x - 10, self.y - 10
|
||||
|
||||
'''
|
||||
__all__ = ('Image', 'AsyncImage')
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.core.image import Image as CoreImage
|
||||
from kivy.resources import resource_find
|
||||
from kivy.properties import (
|
||||
StringProperty,
|
||||
ObjectProperty,
|
||||
ListProperty,
|
||||
AliasProperty,
|
||||
BooleanProperty,
|
||||
NumericProperty,
|
||||
ColorProperty,
|
||||
OptionProperty
|
||||
)
|
||||
from kivy.logger import Logger
|
||||
|
||||
# delayed imports
|
||||
Loader = None
|
||||
|
||||
|
||||
class Image(Widget):
|
||||
'''Image class, see module documentation for more information.'''
|
||||
|
||||
source = StringProperty(None)
|
||||
'''Filename / source of your image.
|
||||
|
||||
:attr:`source` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to None.
|
||||
'''
|
||||
|
||||
texture = ObjectProperty(None, allownone=True)
|
||||
'''Texture object of the image. The texture represents the original, loaded
|
||||
image texture. It is stretched and positioned during rendering according to
|
||||
the :attr:`fit_mode` property.
|
||||
|
||||
Depending of the texture creation, the value will be a
|
||||
:class:`~kivy.graphics.texture.Texture` or a
|
||||
:class:`~kivy.graphics.texture.TextureRegion` object.
|
||||
|
||||
:attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults
|
||||
to None.
|
||||
'''
|
||||
|
||||
texture_size = ListProperty([0, 0])
|
||||
'''Texture size of the image. This represents the original, loaded image
|
||||
texture size.
|
||||
|
||||
.. warning::
|
||||
|
||||
The texture size is set after the texture property. So if you listen to
|
||||
the change on :attr:`texture`, the property texture_size will not be
|
||||
up-to-date. Use self.texture.size instead.
|
||||
'''
|
||||
|
||||
def get_image_ratio(self):
|
||||
if self.texture:
|
||||
return self.texture.width / float(self.texture.height)
|
||||
return 1.0
|
||||
|
||||
mipmap = BooleanProperty(False)
|
||||
'''Indicate if you want OpenGL mipmapping to be applied to the texture.
|
||||
Read :ref:`mipmap` for more information.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
:attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
image_ratio = AliasProperty(get_image_ratio, bind=('texture',), cache=True)
|
||||
'''Ratio of the image (width / float(height).
|
||||
|
||||
:attr:`image_ratio` is an :class:`~kivy.properties.AliasProperty` and is
|
||||
read-only.
|
||||
'''
|
||||
|
||||
color = ColorProperty([1, 1, 1, 1])
|
||||
'''Image color, in the format (r, g, b, a). This attribute can be used to
|
||||
'tint' an image. Be careful: if the source image is not gray/white, the
|
||||
color will not really work as expected.
|
||||
|
||||
.. versionadded:: 1.0.6
|
||||
|
||||
:attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to
|
||||
[1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
allow_stretch = BooleanProperty(False, deprecated=True)
|
||||
'''If True, the normalized image size will be maximized to fit in the image
|
||||
box. Otherwise, if the box is too tall, the image will not be
|
||||
stretched more than 1:1 pixels.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
.. deprecated:: 2.2.0
|
||||
:attr:`allow_stretch` have been deprecated. Please use `fit_mode`
|
||||
instead.
|
||||
|
||||
:attr:`allow_stretch` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
keep_ratio = BooleanProperty(True, deprecated=True)
|
||||
'''If False along with allow_stretch being True, the normalized image
|
||||
size will be maximized to fit in the image box and ignores the aspect
|
||||
ratio of the image.
|
||||
Otherwise, if the box is too tall, the image will not be stretched more
|
||||
than 1:1 pixels.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
.. deprecated:: 2.2.0
|
||||
:attr:`keep_ratio` have been deprecated. Please use `fit_mode`
|
||||
instead.
|
||||
|
||||
:attr:`keep_ratio` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to True.
|
||||
'''
|
||||
|
||||
fit_mode = OptionProperty(
|
||||
"scale-down", options=["scale-down", "fill", "contain", "cover"]
|
||||
)
|
||||
'''If the size of the image is different than the size of the widget,
|
||||
determine how the image should be resized to fit inside the widget box.
|
||||
|
||||
Available options:
|
||||
|
||||
- ``"scale-down"``: the image will be scaled down to fit inside the widget
|
||||
box, **maintaining its aspect ratio and without stretching**. If the size
|
||||
of the image is smaller than the widget, it will be displayed at its
|
||||
original size. If the image has a different aspect ratio than the widget,
|
||||
there will be blank areas on the widget box.
|
||||
|
||||
- ``"fill"``: the image is stretched to fill the widget, **regardless of
|
||||
its aspect ratio or dimensions**. If the image has a different aspect ratio
|
||||
than the widget, this option can lead to distortion of the image.
|
||||
|
||||
- ``"contain"``: the image is resized to fit inside the widget box,
|
||||
**maintaining its aspect ratio**. If the image size is larger than the
|
||||
widget size, the behavior will be similar to ``"scale-down"``. However, if
|
||||
the size of the image is smaller than the widget size, unlike
|
||||
``"scale-down``, the image will be resized to fit inside the widget.
|
||||
If the image has a different aspect ratio than the widget, there will be
|
||||
blank areas on the widget box.
|
||||
|
||||
- ``"cover"``: the image will be stretched horizontally or vertically to
|
||||
fill the widget box, **maintaining its aspect ratio**. If the image has a
|
||||
different aspect ratio than the widget, then the image will be clipped to
|
||||
fit.
|
||||
|
||||
:attr:`fit_mode` is a :class:`~kivy.properties.OptionProperty` and
|
||||
defaults to ``"scale-down"``.
|
||||
'''
|
||||
|
||||
keep_data = BooleanProperty(False)
|
||||
'''If True, the underlying _coreimage will store the raw image data.
|
||||
This is useful when performing pixel based collision detection.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
:attr:`keep_data` is a :class:`~kivy.properties.BooleanProperty` and
|
||||
defaults to False.
|
||||
'''
|
||||
|
||||
anim_delay = NumericProperty(0.25)
|
||||
'''Delay the animation if the image is sequenced (like an animated gif).
|
||||
If anim_delay is set to -1, the animation will be stopped.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:attr:`anim_delay` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.25 (4 FPS).
|
||||
'''
|
||||
|
||||
anim_loop = NumericProperty(0)
|
||||
'''Number of loops to play then stop animating. 0 means keep animating.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
:attr:`anim_loop` is a :class:`~kivy.properties.NumericProperty` and
|
||||
defaults to 0.
|
||||
'''
|
||||
|
||||
nocache = BooleanProperty(False)
|
||||
'''If this property is set True, the image will not be added to the
|
||||
internal cache. The cache will simply ignore any calls trying to
|
||||
append the core image.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
|
||||
:attr:`nocache` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
||||
to False.
|
||||
'''
|
||||
|
||||
def get_norm_image_size(self):
|
||||
if not self.texture:
|
||||
return list(self.size)
|
||||
|
||||
ratio = self.image_ratio
|
||||
w, h = self.size
|
||||
tw, th = self.texture.size
|
||||
|
||||
if self.fit_mode == "cover":
|
||||
widget_ratio = w / max(1, h)
|
||||
if widget_ratio > ratio:
|
||||
return [w, (w * th) / tw]
|
||||
else:
|
||||
return [(h * tw) / th, h]
|
||||
elif self.fit_mode == "fill":
|
||||
return [w, h]
|
||||
elif self.fit_mode == "contain":
|
||||
iw = w
|
||||
else:
|
||||
iw = min(w, tw)
|
||||
|
||||
# calculate the appropriate height
|
||||
ih = iw / ratio
|
||||
# if the height is too higher, take the height of the container
|
||||
# and calculate appropriate width. no need to test further. :)
|
||||
if ih > h:
|
||||
if self.fit_mode == "contain":
|
||||
ih = h
|
||||
else:
|
||||
ih = min(h, th)
|
||||
iw = ih * ratio
|
||||
return [iw, ih]
|
||||
|
||||
norm_image_size = AliasProperty(
|
||||
get_norm_image_size,
|
||||
bind=(
|
||||
'texture',
|
||||
'size',
|
||||
'image_ratio',
|
||||
'fit_mode',
|
||||
),
|
||||
cache=True,
|
||||
)
|
||||
'''Normalized image size within the widget box.
|
||||
|
||||
This size will always fit the widget size and will preserve the image
|
||||
ratio.
|
||||
|
||||
:attr:`norm_image_size` is an :class:`~kivy.properties.AliasProperty` and
|
||||
is read-only.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._coreimage = None
|
||||
self._loops = 0
|
||||
update = self.texture_update
|
||||
fbind = self.fbind
|
||||
fbind('source', update)
|
||||
fbind('mipmap', update)
|
||||
|
||||
# NOTE: Compatibility code due to deprecated properties.
|
||||
fbind('keep_ratio', self._update_fit_mode)
|
||||
fbind('allow_stretch', self._update_fit_mode)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _update_fit_mode(self, *args):
|
||||
keep_ratio = self.keep_ratio
|
||||
allow_stretch = self.allow_stretch
|
||||
if (
|
||||
not keep_ratio and not allow_stretch
|
||||
or keep_ratio and not allow_stretch
|
||||
):
|
||||
self.fit_mode = "scale-down"
|
||||
elif not keep_ratio and allow_stretch:
|
||||
self.fit_mode = "fill"
|
||||
elif keep_ratio and allow_stretch:
|
||||
self.fit_mode = "contain"
|
||||
|
||||
def texture_update(self, *largs):
|
||||
self.set_texture_from_resource(self.source)
|
||||
|
||||
def set_texture_from_resource(self, resource):
|
||||
if not resource:
|
||||
self._clear_core_image()
|
||||
return
|
||||
source = resource_find(resource)
|
||||
if not source:
|
||||
Logger.error('Image: Not found <%s>' % resource)
|
||||
self._clear_core_image()
|
||||
return
|
||||
if self._coreimage:
|
||||
self._coreimage.unbind(on_texture=self._on_tex_change)
|
||||
try:
|
||||
self._coreimage = image = CoreImage(
|
||||
source,
|
||||
mipmap=self.mipmap,
|
||||
anim_delay=self.anim_delay,
|
||||
keep_data=self.keep_data,
|
||||
nocache=self.nocache
|
||||
)
|
||||
except Exception:
|
||||
Logger.error('Image: Error loading <%s>' % resource)
|
||||
self._clear_core_image()
|
||||
image = self._coreimage
|
||||
if image:
|
||||
image.bind(on_texture=self._on_tex_change)
|
||||
self.texture = image.texture
|
||||
|
||||
def on_anim_delay(self, instance, value):
|
||||
if self._coreimage is None:
|
||||
return
|
||||
self._coreimage.anim_delay = value
|
||||
if value < 0:
|
||||
self._coreimage.anim_reset(False)
|
||||
|
||||
def on_texture(self, instance, value):
|
||||
self.texture_size = value.size if value else [0, 0]
|
||||
|
||||
def _clear_core_image(self):
|
||||
if self._coreimage:
|
||||
self._coreimage.unbind(on_texture=self._on_tex_change)
|
||||
self.texture = None
|
||||
self._coreimage = None
|
||||
self._loops = 0
|
||||
|
||||
def _on_tex_change(self, *largs):
|
||||
# update texture from core image
|
||||
self.texture = self._coreimage.texture
|
||||
ci = self._coreimage
|
||||
if self.anim_loop and ci._anim_index == len(ci._image.textures) - 1:
|
||||
self._loops += 1
|
||||
if self.anim_loop == self._loops:
|
||||
ci.anim_reset(False)
|
||||
self._loops = 0
|
||||
|
||||
def reload(self):
|
||||
'''Reload image from disk. This facilitates re-loading of
|
||||
images from disk in case the image content changes.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
Usage::
|
||||
|
||||
im = Image(source = '1.jpg')
|
||||
# -- do something --
|
||||
im.reload()
|
||||
# image will be re-loaded from disk
|
||||
|
||||
'''
|
||||
self.remove_from_cache()
|
||||
old_source = self.source
|
||||
self.source = ''
|
||||
self.source = old_source
|
||||
|
||||
def remove_from_cache(self):
|
||||
'''Remove image from cache.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
'''
|
||||
if self._coreimage:
|
||||
self._coreimage.remove_from_cache()
|
||||
|
||||
def on_nocache(self, *args):
|
||||
if self.nocache:
|
||||
self.remove_from_cache()
|
||||
if self._coreimage:
|
||||
self._coreimage._nocache = True
|
||||
|
||||
|
||||
class AsyncImage(Image):
|
||||
'''Asynchronous Image class. See the module documentation for more
|
||||
information.
|
||||
|
||||
.. note::
|
||||
|
||||
The AsyncImage is a specialized form of the Image class. You may
|
||||
want to refer to the :mod:`~kivy.loader` documentation and in
|
||||
particular, the :class:`~kivy.loader.ProxyImage` for more detail
|
||||
on how to handle events around asynchronous image loading.
|
||||
|
||||
.. note::
|
||||
|
||||
AsyncImage currently does not support properties
|
||||
:attr:`anim_loop` and :attr:`mipmap` and setting those properties will
|
||||
have no effect.
|
||||
'''
|
||||
|
||||
__events__ = ('on_error', 'on_load')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._found_source = None
|
||||
self._coreimage = None
|
||||
global Loader
|
||||
if not Loader:
|
||||
from kivy.loader import Loader
|
||||
self.fbind('source', self._load_source)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _load_source(self, *args):
|
||||
source = self.source
|
||||
if not source:
|
||||
self._clear_core_image()
|
||||
return
|
||||
if not self.is_uri(source):
|
||||
source = resource_find(source)
|
||||
if not source:
|
||||
Logger.error('AsyncImage: Not found <%s>' % self.source)
|
||||
self._clear_core_image()
|
||||
return
|
||||
self._found_source = source
|
||||
self._coreimage = image = Loader.image(
|
||||
source,
|
||||
nocache=self.nocache,
|
||||
mipmap=self.mipmap,
|
||||
anim_delay=self.anim_delay
|
||||
)
|
||||
image.bind(
|
||||
on_load=self._on_source_load,
|
||||
on_error=self._on_source_error,
|
||||
on_texture=self._on_tex_change
|
||||
)
|
||||
self.texture = image.texture
|
||||
|
||||
def _on_source_load(self, value):
|
||||
image = self._coreimage.image
|
||||
if not image:
|
||||
return
|
||||
self.texture = image.texture
|
||||
self.dispatch('on_load')
|
||||
|
||||
def _on_source_error(self, instance, error=None):
|
||||
self.dispatch('on_error', error)
|
||||
|
||||
def on_error(self, error):
|
||||
pass
|
||||
|
||||
def on_load(self, *args):
|
||||
pass
|
||||
|
||||
def is_uri(self, filename):
|
||||
proto = filename.split('://', 1)[0]
|
||||
return proto in ('http', 'https', 'ftp', 'smb')
|
||||
|
||||
def _clear_core_image(self):
|
||||
if self._coreimage:
|
||||
self._coreimage.unbind(on_load=self._on_source_load)
|
||||
super()._clear_core_image()
|
||||
self._found_source = None
|
||||
|
||||
def _on_tex_change(self, *largs):
|
||||
if self._coreimage:
|
||||
self.texture = self._coreimage.texture
|
||||
|
||||
def texture_update(self, *largs):
|
||||
pass
|
||||
|
||||
def remove_from_cache(self):
|
||||
if self._found_source:
|
||||
Loader.remove_from_cache(self._found_source)
|
||||
super().remove_from_cache()
|
1196
kivy_venv/lib/python3.11/site-packages/kivy/uix/label.py
Normal file
1196
kivy_venv/lib/python3.11/site-packages/kivy/uix/label.py
Normal file
File diff suppressed because it is too large
Load diff
322
kivy_venv/lib/python3.11/site-packages/kivy/uix/layout.py
Normal file
322
kivy_venv/lib/python3.11/site-packages/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_venv/lib/python3.11/site-packages/kivy/uix/modalview.py
Normal file
339
kivy_venv/lib/python3.11/site-packages/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 color. The default
|
||||
texture is grey, so just setting the background color will give
|
||||
a darker result. To set a plain color, set the
|
||||
:attr:`background_normal` to ``''``.
|
||||
|
||||
The :attr:`background_color` is a
|
||||
:class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1].
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Changed behavior to affect the background of the widget itself, not
|
||||
the overlay dimming.
|
||||
Changed from :class:`~kivy.properties.ListProperty` to
|
||||
:class:`~kivy.properties.ColorProperty`.
|
||||
'''
|
||||
|
||||
background = StringProperty(
|
||||
'atlas://data/images/defaulttheme/modalview-background')
|
||||
'''Background image of the view used for the view background.
|
||||
|
||||
:attr:`background` is a :class:`~kivy.properties.StringProperty` and
|
||||
defaults to 'atlas://data/images/defaulttheme/modalview-background'.
|
||||
'''
|
||||
|
||||
border = ListProperty([16, 16, 16, 16])
|
||||
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
||||
graphics instruction. Used for the :attr:`background_normal` and the
|
||||
:attr:`background_down` properties. Can be used when using custom
|
||||
backgrounds.
|
||||
|
||||
It must be a list of four values: (bottom, right, top, left). Read the
|
||||
BorderImage instructions for more information about how to use it.
|
||||
|
||||
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
|
||||
(16, 16, 16, 16).
|
||||
'''
|
||||
|
||||
overlay_color = ColorProperty([0, 0, 0, .7])
|
||||
'''Overlay color in the format (r, g, b, a).
|
||||
Used for dimming the window behind the modal view.
|
||||
|
||||
:attr:`overlay_color` is a :class:`~kivy.properties.ColorProperty` and
|
||||
defaults to [0, 0, 0, .7].
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
'''
|
||||
|
||||
# Internals properties used for graphical representation.
|
||||
|
||||
_anim_alpha = NumericProperty(0)
|
||||
|
||||
_anim_duration = NumericProperty(.1)
|
||||
|
||||
_window = ObjectProperty(allownone=True, rebind=True)
|
||||
|
||||
_is_open = BooleanProperty(False)
|
||||
|
||||
_touch_started_inside = None
|
||||
|
||||
__events__ = ('on_pre_open', 'on_open', 'on_pre_dismiss', 'on_dismiss')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._parent = None
|
||||
super(ModalView, self).__init__(**kwargs)
|
||||
|
||||
def open(self, *_args, **kwargs):
|
||||
"""Display the modal in the Window.
|
||||
|
||||
When the view is opened, it will be faded in with an animation. If you
|
||||
don't want the animation, use::
|
||||
|
||||
view.open(animation=False)
|
||||
|
||||
"""
|
||||
from kivy.core.window import Window
|
||||
if self._is_open:
|
||||
return
|
||||
self._window = Window
|
||||
self._is_open = True
|
||||
self.dispatch('on_pre_open')
|
||||
Window.add_widget(self)
|
||||
Window.bind(
|
||||
on_resize=self._align_center,
|
||||
on_keyboard=self._handle_keyboard)
|
||||
self.center = Window.center
|
||||
self.fbind('center', self._align_center)
|
||||
self.fbind('size', self._align_center)
|
||||
if kwargs.get('animation', True):
|
||||
ani = Animation(_anim_alpha=1., d=self._anim_duration)
|
||||
ani.bind(on_complete=lambda *_args: self.dispatch('on_open'))
|
||||
ani.start(self)
|
||||
else:
|
||||
self._anim_alpha = 1.
|
||||
self.dispatch('on_open')
|
||||
|
||||
def dismiss(self, *_args, **kwargs):
|
||||
""" Close the view if it is open.
|
||||
|
||||
If you really want to close the view, whatever the on_dismiss
|
||||
event returns, you can use the *force* keyword argument::
|
||||
|
||||
view = ModalView()
|
||||
view.dismiss(force=True)
|
||||
|
||||
When the view is dismissed, it will be faded out before being
|
||||
removed from the parent. If you don't want this animation, use::
|
||||
|
||||
view.dismiss(animation=False)
|
||||
|
||||
"""
|
||||
if not self._is_open:
|
||||
return
|
||||
self.dispatch('on_pre_dismiss')
|
||||
if self.dispatch('on_dismiss') is True:
|
||||
if kwargs.get('force', False) is not True:
|
||||
return
|
||||
if kwargs.get('animation', True):
|
||||
Animation(_anim_alpha=0., d=self._anim_duration).start(self)
|
||||
else:
|
||||
self._anim_alpha = 0
|
||||
self._real_remove_widget()
|
||||
|
||||
def _align_center(self, *_args):
|
||||
if self._is_open:
|
||||
self.center = self._window.center
|
||||
|
||||
def on_motion(self, etype, me):
|
||||
super().on_motion(etype, me)
|
||||
return True
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
""" touch down event handler. """
|
||||
self._touch_started_inside = self.collide_point(*touch.pos)
|
||||
if not self.auto_dismiss or self._touch_started_inside:
|
||||
super().on_touch_down(touch)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
""" touch moved event handler. """
|
||||
if not self.auto_dismiss or self._touch_started_inside:
|
||||
super().on_touch_move(touch)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
""" touch up event handler. """
|
||||
# Explicitly test for False as None occurs when shown by on_touch_down
|
||||
if self.auto_dismiss and self._touch_started_inside is False:
|
||||
self.dismiss()
|
||||
else:
|
||||
super().on_touch_up(touch)
|
||||
self._touch_started_inside = None
|
||||
return True
|
||||
|
||||
def on__anim_alpha(self, _instance, value):
|
||||
""" animation progress callback. """
|
||||
if value == 0 and self._is_open:
|
||||
self._real_remove_widget()
|
||||
|
||||
def _real_remove_widget(self):
|
||||
if not self._is_open:
|
||||
return
|
||||
self._window.remove_widget(self)
|
||||
self._window.unbind(
|
||||
on_resize=self._align_center,
|
||||
on_keyboard=self._handle_keyboard)
|
||||
self._is_open = False
|
||||
self._window = None
|
||||
|
||||
def on_pre_open(self):
|
||||
""" default pre-open event handler. """
|
||||
|
||||
def on_open(self):
|
||||
""" default open event handler. """
|
||||
|
||||
def on_pre_dismiss(self):
|
||||
""" default pre-dismiss event handler. """
|
||||
|
||||
def on_dismiss(self):
|
||||
""" default dismiss event handler. """
|
||||
|
||||
def _handle_keyboard(self, _window, key, *_args):
|
||||
if key == 27 and self.auto_dismiss:
|
||||
self.dismiss()
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.uix.button import Button
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
|
||||
# add view
|
||||
content = GridLayout(cols=1)
|
||||
content.add_widget(Label(text='This is a hello world'))
|
||||
view = ModalView(size_hint=(None, None), size=(256, 256))
|
||||
view.add_widget(content)
|
||||
|
||||
layout = GridLayout(cols=3)
|
||||
for x in range(9):
|
||||
btn = Button(text=f"click me {x}")
|
||||
btn.bind(on_release=view.open)
|
||||
layout.add_widget(btn)
|
||||
Window.add_widget(layout)
|
||||
|
||||
view.open()
|
||||
runTouchApp()
|
233
kivy_venv/lib/python3.11/site-packages/kivy/uix/pagelayout.py
Normal file
233
kivy_venv/lib/python3.11/site-packages/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_venv/lib/python3.11/site-packages/kivy/uix/popup.py
Normal file
266
kivy_venv/lib/python3.11/site-packages/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()
|
|
@ -0,0 +1,95 @@
|
|||
'''
|
||||
Progress Bar
|
||||
============
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
.. image:: images/progressbar.jpg
|
||||
:align: right
|
||||
|
||||
The :class:`ProgressBar` widget is used to visualize the progress of some task.
|
||||
Only the horizontal mode is currently supported: the vertical mode is not
|
||||
yet available.
|
||||
|
||||
The progress bar has no interactive elements and is a display-only widget.
|
||||
|
||||
To use it, simply assign a value to indicate the current progress::
|
||||
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
pb = ProgressBar(max=1000)
|
||||
|
||||
# this will update the graphics automatically (75% done)
|
||||
pb.value = 750
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('ProgressBar', )
|
||||
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.properties import NumericProperty, AliasProperty
|
||||
|
||||
|
||||
class ProgressBar(Widget):
|
||||
'''Class for creating a progress bar widget.
|
||||
|
||||
See module documentation for more details.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._value = 0.
|
||||
super(ProgressBar, self).__init__(**kwargs)
|
||||
|
||||
def _get_value(self):
|
||||
return self._value
|
||||
|
||||
def _set_value(self, value):
|
||||
value = max(0, min(self.max, value))
|
||||
if value != self._value:
|
||||
self._value = value
|
||||
return True
|
||||
|
||||
value = AliasProperty(_get_value, _set_value)
|
||||
'''Current value used for the slider.
|
||||
|
||||
:attr:`value` is an :class:`~kivy.properties.AliasProperty` that
|
||||
returns the value of the progress bar. If the value is < 0 or >
|
||||
:attr:`max`, it will be normalized to those boundaries.
|
||||
|
||||
.. versionchanged:: 1.6.0
|
||||
The value is now limited to between 0 and :attr:`max`.
|
||||
'''
|
||||
|
||||
def get_norm_value(self):
|
||||
d = self.max
|
||||
if d == 0:
|
||||
return 0
|
||||
return self.value / float(d)
|
||||
|
||||
def set_norm_value(self, value):
|
||||
self.value = value * self.max
|
||||
|
||||
value_normalized = AliasProperty(get_norm_value, set_norm_value,
|
||||
bind=('value', 'max'), cache=True)
|
||||
'''Normalized value inside the range 0-1::
|
||||
|
||||
>>> pb = ProgressBar(value=50, max=100)
|
||||
>>> pb.value
|
||||
50
|
||||
>>> pb.value_normalized
|
||||
0.5
|
||||
|
||||
:attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`.
|
||||
'''
|
||||
|
||||
max = NumericProperty(100.)
|
||||
'''Maximum value allowed for :attr:`value`.
|
||||
|
||||
:attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
||||
100.
|
||||
'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
from kivy.base import runTouchApp
|
||||
runTouchApp(ProgressBar(value=50))
|
|
@ -0,0 +1,183 @@
|
|||
"""
|
||||
RecycleBoxLayout
|
||||
================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
|
||||
The RecycleBoxLayout is designed to provide a
|
||||
:class:`~kivy.uix.boxlayout.BoxLayout` type layout when used with the
|
||||
:class:`~kivy.uix.recycleview.RecycleView` widget. Please refer to the
|
||||
:mod:`~kivy.uix.recycleview` module documentation for more information.
|
||||
|
||||
"""
|
||||
|
||||
from kivy.uix.recyclelayout import RecycleLayout
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
|
||||
__all__ = ('RecycleBoxLayout', )
|
||||
|
||||
|
||||
class RecycleBoxLayout(RecycleLayout, BoxLayout):
|
||||
|
||||
_rv_positions = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RecycleBoxLayout, self).__init__(**kwargs)
|
||||
self.funbind('children', self._trigger_layout)
|
||||
|
||||
def _update_sizes(self, changed):
|
||||
horizontal = self.orientation == 'horizontal'
|
||||
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
||||
padding_x = padding_left + padding_right
|
||||
padding_y = padding_top + padding_bottom
|
||||
selfw = self.width
|
||||
selfh = self.height
|
||||
layout_w = max(0, selfw - padding_x)
|
||||
layout_h = max(0, selfh - padding_y)
|
||||
cx = self.x + padding_left
|
||||
cy = self.y + padding_bottom
|
||||
view_opts = self.view_opts
|
||||
remove_view = self.remove_view
|
||||
|
||||
for (index, widget, (w, h), (wn, hn), (shw, shh), (shnw, shnh),
|
||||
(shw_min, shh_min), (shwn_min, shhn_min), (shw_max, shh_max),
|
||||
(shwn_max, shhn_max), ph, phn) in changed:
|
||||
if (horizontal and
|
||||
(shw != shnw or w != wn or shw_min != shwn_min or
|
||||
shw_max != shwn_max) or
|
||||
not horizontal and
|
||||
(shh != shnh or h != hn or shh_min != shhn_min or
|
||||
shh_max != shhn_max)):
|
||||
return True
|
||||
|
||||
remove_view(widget, index)
|
||||
opt = view_opts[index]
|
||||
if horizontal:
|
||||
wo, ho = opt['size']
|
||||
if shnh is not None:
|
||||
_, h = opt['size'] = [wo, shnh * layout_h]
|
||||
else:
|
||||
h = ho
|
||||
|
||||
xo, yo = opt['pos']
|
||||
for key, value in phn.items():
|
||||
posy = value * layout_h
|
||||
if key == 'y':
|
||||
yo = posy + cy
|
||||
elif key == 'top':
|
||||
yo = posy - h
|
||||
elif key == 'center_y':
|
||||
yo = posy - (h / 2.)
|
||||
opt['pos'] = [xo, yo]
|
||||
else:
|
||||
wo, ho = opt['size']
|
||||
if shnw is not None:
|
||||
w, _ = opt['size'] = [shnw * layout_w, ho]
|
||||
else:
|
||||
w = wo
|
||||
|
||||
xo, yo = opt['pos']
|
||||
for key, value in phn.items():
|
||||
posx = value * layout_w
|
||||
if key == 'x':
|
||||
xo = posx + cx
|
||||
elif key == 'right':
|
||||
xo = posx - w
|
||||
elif key == 'center_x':
|
||||
xo = posx - (w / 2.)
|
||||
opt['pos'] = [xo, yo]
|
||||
|
||||
return False
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
super(RecycleBoxLayout, self).compute_layout(data, flags)
|
||||
|
||||
changed = self._changed_views
|
||||
if (changed is None or
|
||||
changed and not self._update_sizes(changed)):
|
||||
return
|
||||
|
||||
self.clear_layout()
|
||||
self._rv_positions = None
|
||||
if not data:
|
||||
l, t, r, b = self.padding
|
||||
self.minimum_size = l + r, t + b
|
||||
return
|
||||
|
||||
view_opts = self.view_opts
|
||||
n = len(view_opts)
|
||||
for i, x, y, w, h in self._iterate_layout(
|
||||
[(opt['size'], opt['size_hint'], opt['pos_hint'],
|
||||
opt['size_hint_min'], opt['size_hint_max']) for
|
||||
opt in reversed(view_opts)]):
|
||||
opt = view_opts[n - i - 1]
|
||||
shw, shh = opt['size_hint']
|
||||
opt['pos'] = x, y
|
||||
wo, ho = opt['size']
|
||||
# layout won't/shouldn't change previous size if size_hint is None
|
||||
# which is what w/h being None means.
|
||||
opt['size'] = [(wo if shw is None else w),
|
||||
(ho if shh is None else h)]
|
||||
|
||||
spacing = self.spacing
|
||||
pos = self._rv_positions = [None, ] * len(data)
|
||||
|
||||
if self.orientation == 'horizontal':
|
||||
pos[0] = self.x
|
||||
last = pos[0] + self.padding[0] + view_opts[0]['size'][0] + \
|
||||
spacing / 2.
|
||||
for i, val in enumerate(view_opts[1:], 1):
|
||||
pos[i] = last
|
||||
last += val['size'][0] + spacing
|
||||
else:
|
||||
last = pos[-1] = \
|
||||
self.y + self.height - self.padding[1] - \
|
||||
view_opts[0]['size'][1] - spacing / 2.
|
||||
n = len(view_opts)
|
||||
for i, val in enumerate(view_opts[1:], 1):
|
||||
last -= spacing + val['size'][1]
|
||||
pos[n - 1 - i] = last
|
||||
|
||||
def get_view_index_at(self, pos):
|
||||
calc_pos = self._rv_positions
|
||||
if not calc_pos:
|
||||
return 0
|
||||
|
||||
x, y = pos
|
||||
|
||||
if self.orientation == 'horizontal':
|
||||
if x >= calc_pos[-1] or len(calc_pos) == 1:
|
||||
return len(calc_pos) - 1
|
||||
|
||||
ix = 0
|
||||
for val in calc_pos[1:]:
|
||||
if x < val:
|
||||
return ix
|
||||
ix += 1
|
||||
else:
|
||||
if y >= calc_pos[-1] or len(calc_pos) == 1:
|
||||
return 0
|
||||
|
||||
iy = 0
|
||||
for val in calc_pos[1:]:
|
||||
if y < val:
|
||||
return len(calc_pos) - iy - 1
|
||||
iy += 1
|
||||
|
||||
assert False
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
if self._rv_positions is None or not data:
|
||||
return []
|
||||
|
||||
x, y, w, h = viewport
|
||||
at_idx = self.get_view_index_at
|
||||
if self.orientation == 'horizontal':
|
||||
a, b = at_idx((x, y)), at_idx((x + w, y))
|
||||
else:
|
||||
a, b = at_idx((x, y + h)), at_idx((x, y))
|
||||
return list(range(a, b + 1))
|
|
@ -0,0 +1,255 @@
|
|||
"""
|
||||
RecycleGridLayout
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
|
||||
The RecycleGridLayout is designed to provide a
|
||||
:class:`~kivy.uix.gridlayout.GridLayout` type layout when used with the
|
||||
:class:`~kivy.uix.recycleview.RecycleView` widget. Please refer to the
|
||||
:mod:`~kivy.uix.recycleview` module documentation for more information.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
chain_from_iterable = itertools.chain.from_iterable
|
||||
from kivy.uix.recyclelayout import RecycleLayout
|
||||
from kivy.uix.gridlayout import GridLayout, GridLayoutException, nmax, nmin
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = ('RecycleGridLayout', )
|
||||
|
||||
|
||||
class RecycleGridLayout(RecycleLayout, GridLayout):
|
||||
|
||||
_cols_pos = None
|
||||
_rows_pos = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RecycleGridLayout, self).__init__(**kwargs)
|
||||
self.funbind('children', self._trigger_layout)
|
||||
|
||||
def on_children(self, instance, value):
|
||||
pass
|
||||
|
||||
def _fill_rows_cols_sizes(self):
|
||||
cols, rows = self._cols, self._rows
|
||||
cols_sh, rows_sh = self._cols_sh, self._rows_sh
|
||||
cols_sh_min, rows_sh_min = self._cols_sh_min, self._rows_sh_min
|
||||
cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max
|
||||
self._cols_count = cols_count = [defaultdict(int) for _ in cols]
|
||||
self._rows_count = rows_count = [defaultdict(int) for _ in rows]
|
||||
|
||||
# calculate minimum size for each columns and rows
|
||||
idx_iter = self._create_idx_iter(len(cols), len(rows))
|
||||
has_bound_y = has_bound_x = False
|
||||
for opt, (col, row) in zip(self.view_opts, idx_iter):
|
||||
(shw, shh), (w, h) = opt['size_hint'], opt['size']
|
||||
shw_min, shh_min = opt['size_hint_min']
|
||||
shw_max, shh_max = opt['size_hint_max']
|
||||
|
||||
if shw is None:
|
||||
cols_count[col][w] += 1
|
||||
if shh is None:
|
||||
rows_count[row][h] += 1
|
||||
|
||||
# compute minimum size / maximum stretch needed
|
||||
if shw is None:
|
||||
cols[col] = nmax(cols[col], w)
|
||||
else:
|
||||
cols_sh[col] = nmax(cols_sh[col], shw)
|
||||
if shw_min is not None:
|
||||
has_bound_x = True
|
||||
cols_sh_min[col] = nmax(cols_sh_min[col], shw_min)
|
||||
if shw_max is not None:
|
||||
has_bound_x = True
|
||||
cols_sh_max[col] = nmin(cols_sh_max[col], shw_max)
|
||||
|
||||
if shh is None:
|
||||
rows[row] = nmax(rows[row], h)
|
||||
else:
|
||||
rows_sh[row] = nmax(rows_sh[row], shh)
|
||||
if shh_min is not None:
|
||||
has_bound_y = True
|
||||
rows_sh_min[row] = nmax(rows_sh_min[row], shh_min)
|
||||
if shh_max is not None:
|
||||
has_bound_y = True
|
||||
rows_sh_max[row] = nmin(rows_sh_max[row], shh_max)
|
||||
self._has_hint_bound_x = has_bound_x
|
||||
self._has_hint_bound_y = has_bound_y
|
||||
|
||||
def _update_rows_cols_sizes(self, changed):
|
||||
cols_count, rows_count = self._cols_count, self._rows_count
|
||||
cols, rows = self._cols, self._rows
|
||||
remove_view = self.remove_view
|
||||
n_cols = len(cols)
|
||||
n_rows = len(rows)
|
||||
orientation = self.orientation
|
||||
|
||||
# this can be further improved to reduce re-comp, but whatever...
|
||||
for index, widget, (w, h), (wn, hn), sh, shn, sh_min, shn_min, \
|
||||
sh_max, shn_max, _, _ in changed:
|
||||
if sh != shn or sh_min != shn_min or sh_max != shn_max:
|
||||
return True
|
||||
elif (sh[0] is not None and w != wn and
|
||||
(h == hn or sh[1] is not None) or
|
||||
sh[1] is not None and h != hn and
|
||||
(w == wn or sh[0] is not None)):
|
||||
remove_view(widget, index)
|
||||
else: # size hint is None, so check if it can be resized inplace
|
||||
col, row = self._calculate_idx_from_a_view_idx(
|
||||
n_cols, n_rows, index)
|
||||
if w != wn:
|
||||
col_w = cols[col]
|
||||
cols_count[col][w] -= 1
|
||||
cols_count[col][wn] += 1
|
||||
was_last_w = cols_count[col][w] <= 0
|
||||
if was_last_w and col_w == w or wn > col_w:
|
||||
return True
|
||||
if was_last_w:
|
||||
del cols_count[col][w]
|
||||
|
||||
if h != hn:
|
||||
row_h = rows[row]
|
||||
rows_count[row][h] -= 1
|
||||
rows_count[row][hn] += 1
|
||||
was_last_h = rows_count[row][h] <= 0
|
||||
if was_last_h and row_h == h or hn > row_h:
|
||||
return True
|
||||
if was_last_h:
|
||||
del rows_count[row][h]
|
||||
|
||||
return False
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
super(RecycleGridLayout, self).compute_layout(data, flags)
|
||||
|
||||
n = len(data)
|
||||
smax = self.get_max_widgets()
|
||||
if smax and n > smax:
|
||||
raise GridLayoutException(
|
||||
'Too many children ({}) in GridLayout. Increase rows/cols!'.
|
||||
format(n))
|
||||
|
||||
changed = self._changed_views
|
||||
if (changed is None or
|
||||
changed and not self._update_rows_cols_sizes(changed)):
|
||||
return
|
||||
|
||||
self.clear_layout()
|
||||
if not self._init_rows_cols_sizes(n):
|
||||
self._cols_pos = None
|
||||
l, t, r, b = self.padding
|
||||
self.minimum_size = l + r, t + b
|
||||
return
|
||||
self._fill_rows_cols_sizes()
|
||||
self._update_minimum_size()
|
||||
self._finalize_rows_cols_sizes()
|
||||
|
||||
view_opts = self.view_opts
|
||||
for widget, x, y, w, h in self._iterate_layout(n):
|
||||
opt = view_opts[n - widget - 1]
|
||||
shw, shh = opt['size_hint']
|
||||
opt['pos'] = x, y
|
||||
wo, ho = opt['size']
|
||||
# layout won't/shouldn't change previous size if size_hint is None
|
||||
# which is what w/h being None means.
|
||||
opt['size'] = [(wo if shw is None else w),
|
||||
(ho if shh is None else h)]
|
||||
|
||||
spacing_x, spacing_y = self.spacing
|
||||
cols, rows = self._cols, self._rows
|
||||
|
||||
cols_pos = self._cols_pos = [None, ] * len(cols)
|
||||
rows_pos = self._rows_pos = [None, ] * len(rows)
|
||||
|
||||
cols_pos[0] = self.x
|
||||
last = cols_pos[0] + self.padding[0] + cols[0] + spacing_x / 2.
|
||||
for i, val in enumerate(cols[1:], 1):
|
||||
cols_pos[i] = last
|
||||
last += val + spacing_x
|
||||
|
||||
last = rows_pos[-1] = \
|
||||
self.y + self.height - self.padding[1] - rows[0] - spacing_y / 2.
|
||||
n = len(rows)
|
||||
for i, val in enumerate(rows[1:], 1):
|
||||
last -= spacing_y + val
|
||||
rows_pos[n - 1 - i] = last
|
||||
|
||||
def get_view_index_at(self, pos):
|
||||
if self._cols_pos is None:
|
||||
return 0
|
||||
|
||||
x, y = pos
|
||||
col_pos = self._cols_pos
|
||||
row_pos = self._rows_pos
|
||||
cols, rows = self._cols, self._rows
|
||||
if not col_pos or not row_pos:
|
||||
return 0
|
||||
|
||||
if x >= col_pos[-1]:
|
||||
ix = len(cols) - 1
|
||||
else:
|
||||
ix = 0
|
||||
for val in col_pos[1:]:
|
||||
if x < val:
|
||||
break
|
||||
ix += 1
|
||||
|
||||
if y >= row_pos[-1]:
|
||||
iy = len(rows) - 1
|
||||
else:
|
||||
iy = 0
|
||||
for val in row_pos[1:]:
|
||||
if y < val:
|
||||
break
|
||||
iy += 1
|
||||
|
||||
if not self._fills_from_left_to_right:
|
||||
ix = len(cols) - ix - 1
|
||||
if self._fills_from_top_to_bottom:
|
||||
iy = len(rows) - iy - 1
|
||||
return (iy * len(cols) + ix) if self._fills_row_first else \
|
||||
(ix * len(rows) + iy)
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
if self._cols_pos is None:
|
||||
return []
|
||||
x, y, w, h = viewport
|
||||
right = x + w
|
||||
top = y + h
|
||||
at_idx = self.get_view_index_at
|
||||
tl, tr, bl, br = sorted((
|
||||
at_idx((x, y)),
|
||||
at_idx((right, y)),
|
||||
at_idx((x, top)),
|
||||
at_idx((right, top)),
|
||||
))
|
||||
|
||||
n = len(data)
|
||||
if len({tl, tr, bl, br}) < 4:
|
||||
# visible area is one row/column
|
||||
return range(min(n, tl), min(n, br + 1))
|
||||
indices = []
|
||||
stride = len(self._cols) if self._fills_row_first else len(self._rows)
|
||||
if stride:
|
||||
x_slice = br - bl + 1
|
||||
indices = chain_from_iterable(
|
||||
range(min(s, n), min(n, s + x_slice))
|
||||
for s in range(tl, bl + 1, stride))
|
||||
return indices
|
||||
|
||||
def _calculate_idx_from_a_view_idx(self, n_cols, n_rows, view_idx):
|
||||
'''returns a tuple of (column-index, row-index) from a view-index'''
|
||||
if self._fills_row_first:
|
||||
row_idx, col_idx = divmod(view_idx, n_cols)
|
||||
else:
|
||||
col_idx, row_idx = divmod(view_idx, n_rows)
|
||||
if not self._fills_from_left_to_right:
|
||||
col_idx = n_cols - col_idx - 1
|
||||
if not self._fills_from_top_to_bottom:
|
||||
row_idx = n_rows - row_idx - 1
|
||||
return (col_idx, row_idx, )
|
446
kivy_venv/lib/python3.11/site-packages/kivy/uix/recyclelayout.py
Normal file
446
kivy_venv/lib/python3.11/site-packages/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
Loading…
Add table
Add a link
Reference in a new issue