666 lines
21 KiB
Python
666 lines
21 KiB
Python
"""
|
||
Components/Selection
|
||
====================
|
||
|
||
.. seealso::
|
||
|
||
`Material Design spec, Banner <https://material.io/design/interaction/selection.html>`_
|
||
|
||
.. rubric:: Selection refers to how users indicate specific items they intend to take action on.
|
||
|
||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-previous.png
|
||
:align: center
|
||
|
||
Entering selection mode
|
||
-----------------------
|
||
|
||
To select an item and enter selection mode, long press the item:
|
||
|
||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/enter-selection-mode.gif
|
||
:align: center
|
||
|
||
Exiting selection mode
|
||
----------------------
|
||
|
||
To exit selection mode, tap each selected item until they’re all deselected:
|
||
|
||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/exit-selection-mode.gif
|
||
:align: center
|
||
|
||
Larger selections
|
||
-----------------
|
||
|
||
.. note:: This feature is missing yet.
|
||
|
||
Events
|
||
------
|
||
|
||
.. code-block:: python
|
||
|
||
def on_selected(self, instance_selection_list, instance_selection_item):
|
||
'''Called when a list item is selected.'''
|
||
|
||
def on_unselected(self, instance_selection_list, instance_selection_item):
|
||
'''Called when a list item is unselected.'''
|
||
|
||
Example with TwoLineAvatarListItem
|
||
----------------------------------
|
||
|
||
.. code-block:: python
|
||
|
||
from kivy.animation import Animation
|
||
from kivy.lang import Builder
|
||
from kivy.utils import get_color_from_hex
|
||
|
||
from kivymd.app import MDApp
|
||
from kivymd.uix.list import TwoLineAvatarListItem
|
||
|
||
KV = '''
|
||
<MyItem>
|
||
text: "Two-line item with avatar"
|
||
secondary_text: "Secondary text here"
|
||
_no_ripple_effect: True
|
||
|
||
ImageLeftWidget:
|
||
source: "data/logo/kivy-icon-256.png"
|
||
|
||
|
||
MDBoxLayout:
|
||
orientation: "vertical"
|
||
|
||
MDTopAppBar:
|
||
id: toolbar
|
||
title: "Inbox"
|
||
left_action_items: [["menu"]]
|
||
right_action_items: [["magnify"], ["dots-vertical"]]
|
||
md_bg_color: 0, 0, 0, 1
|
||
|
||
MDBoxLayout:
|
||
padding: "24dp", "8dp", 0, "8dp"
|
||
adaptive_size: True
|
||
|
||
MDLabel:
|
||
text: "Today"
|
||
adaptive_size: True
|
||
|
||
ScrollView:
|
||
|
||
MDSelectionList:
|
||
id: selection_list
|
||
spacing: "12dp"
|
||
overlay_color: app.overlay_color[:-1] + [.2]
|
||
icon_bg_color: app.overlay_color
|
||
on_selected: app.on_selected(*args)
|
||
on_unselected: app.on_unselected(*args)
|
||
on_selected_mode: app.set_selection_mode(*args)
|
||
'''
|
||
|
||
|
||
class MyItem(TwoLineAvatarListItem):
|
||
pass
|
||
|
||
|
||
class Example(MDApp):
|
||
overlay_color = get_color_from_hex("#6042e4")
|
||
|
||
def build(self):
|
||
return Builder.load_string(KV)
|
||
|
||
def on_start(self):
|
||
for i in range(10):
|
||
self.root.ids.selection_list.add_widget(MyItem())
|
||
|
||
def set_selection_mode(self, instance_selection_list, mode):
|
||
if mode:
|
||
md_bg_color = self.overlay_color
|
||
left_action_items = [
|
||
[
|
||
"close",
|
||
lambda x: self.root.ids.selection_list.unselected_all(),
|
||
]
|
||
]
|
||
right_action_items = [["trash-can"], ["dots-vertical"]]
|
||
else:
|
||
md_bg_color = (0, 0, 0, 1)
|
||
left_action_items = [["menu"]]
|
||
right_action_items = [["magnify"], ["dots-vertical"]]
|
||
self.root.ids.toolbar.title = "Inbox"
|
||
|
||
Animation(md_bg_color=md_bg_color, d=0.2).start(self.root.ids.toolbar)
|
||
self.root.ids.toolbar.left_action_items = left_action_items
|
||
self.root.ids.toolbar.right_action_items = right_action_items
|
||
|
||
def on_selected(self, instance_selection_list, instance_selection_item):
|
||
self.root.ids.toolbar.title = str(
|
||
len(instance_selection_list.get_selected_list_items())
|
||
)
|
||
|
||
def on_unselected(self, instance_selection_list, instance_selection_item):
|
||
if instance_selection_list.get_selected_list_items():
|
||
self.root.ids.toolbar.title = str(
|
||
len(instance_selection_list.get_selected_list_items())
|
||
)
|
||
|
||
|
||
Example().run()
|
||
|
||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-example-with-listItem.gif
|
||
:align: center
|
||
|
||
Example with FitImage
|
||
---------------------
|
||
|
||
.. code-block:: python
|
||
|
||
from kivy.animation import Animation
|
||
from kivy.lang import Builder
|
||
from kivy.properties import ColorProperty
|
||
|
||
from kivymd.app import MDApp
|
||
from kivymd.uix.fitimage import FitImage
|
||
|
||
KV = '''
|
||
MDBoxLayout:
|
||
orientation: "vertical"
|
||
md_bg_color: app.theme_cls.bg_light
|
||
|
||
MDTopAppBar:
|
||
id: toolbar
|
||
title: "Inbox"
|
||
left_action_items: [["menu"]]
|
||
right_action_items: [["magnify"], ["dots-vertical"]]
|
||
md_bg_color: app.theme_cls.bg_light
|
||
specific_text_color: 0, 0, 0, 1
|
||
|
||
MDBoxLayout:
|
||
padding: "24dp", "8dp", 0, "8dp"
|
||
adaptive_size: True
|
||
|
||
MDLabel:
|
||
text: "Today"
|
||
adaptive_size: True
|
||
|
||
ScrollView:
|
||
|
||
MDSelectionList:
|
||
id: selection_list
|
||
padding: "24dp", 0, "24dp", "24dp"
|
||
cols: 3
|
||
spacing: "12dp"
|
||
overlay_color: app.overlay_color[:-1] + [.2]
|
||
icon_bg_color: app.overlay_color
|
||
progress_round_color: app.progress_round_color
|
||
on_selected: app.on_selected(*args)
|
||
on_unselected: app.on_unselected(*args)
|
||
on_selected_mode: app.set_selection_mode(*args)
|
||
'''
|
||
|
||
|
||
class Example(MDApp):
|
||
overlay_color = ColorProperty("#6042e4")
|
||
progress_round_color = "#ef514b"
|
||
|
||
def build(self):
|
||
return Builder.load_string(KV)
|
||
|
||
def on_start(self):
|
||
for i in range(10):
|
||
self.root.ids.selection_list.add_widget(
|
||
FitImage(
|
||
source="image.png",
|
||
size_hint_y=None,
|
||
height="240dp",
|
||
)
|
||
)
|
||
|
||
def set_selection_mode(self, instance_selection_list, mode):
|
||
if mode:
|
||
md_bg_color = self.overlay_color
|
||
left_action_items = [
|
||
[
|
||
"close",
|
||
lambda x: self.root.ids.selection_list.unselected_all(),
|
||
]
|
||
]
|
||
right_action_items = [["trash-can"], ["dots-vertical"]]
|
||
else:
|
||
md_bg_color = (1, 1, 1, 1)
|
||
left_action_items = [["menu"]]
|
||
right_action_items = [["magnify"], ["dots-vertical"]]
|
||
self.root.ids.toolbar.title = "Inbox"
|
||
|
||
Animation(md_bg_color=md_bg_color, d=0.2).start(self.root.ids.toolbar)
|
||
self.root.ids.toolbar.left_action_items = left_action_items
|
||
self.root.ids.toolbar.right_action_items = right_action_items
|
||
|
||
def on_selected(self, instance_selection_list, instance_selection_item):
|
||
self.root.ids.toolbar.title = str(
|
||
len(instance_selection_list.get_selected_list_items())
|
||
)
|
||
|
||
def on_unselected(self, instance_selection_list, instance_selection_item):
|
||
if instance_selection_list.get_selected_list_items():
|
||
self.root.ids.toolbar.title = str(
|
||
len(instance_selection_list.get_selected_list_items())
|
||
)
|
||
|
||
|
||
Example().run()
|
||
|
||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-example-with-fitimage.gif
|
||
:align: center
|
||
"""
|
||
|
||
__all__ = ("MDSelectionList",)
|
||
|
||
import os
|
||
from typing import Union
|
||
|
||
from kivy.animation import Animation
|
||
from kivy.clock import Clock
|
||
from kivy.graphics.context_instructions import Color
|
||
from kivy.graphics.vertex_instructions import (
|
||
Ellipse,
|
||
RoundedRectangle,
|
||
SmoothLine,
|
||
)
|
||
from kivy.lang import Builder
|
||
from kivy.metrics import dp
|
||
from kivy.properties import (
|
||
BooleanProperty,
|
||
ColorProperty,
|
||
ListProperty,
|
||
NumericProperty,
|
||
ObjectProperty,
|
||
StringProperty,
|
||
)
|
||
|
||
from kivymd import uix_path
|
||
from kivymd.theming import ThemableBehavior
|
||
from kivymd.uix.behaviors import TouchBehavior
|
||
from kivymd.uix.button import MDIconButton
|
||
from kivymd.uix.list import MDList
|
||
from kivymd.uix.relativelayout import MDRelativeLayout
|
||
|
||
with open(
|
||
os.path.join(uix_path, "selection", "selection.kv"), encoding="utf-8"
|
||
) as kv_file:
|
||
Builder.load_string(kv_file.read())
|
||
|
||
|
||
class SelectionIconCheck(MDIconButton):
|
||
"""Implements the icon for the checked item."""
|
||
|
||
scale = NumericProperty(0)
|
||
icon_check_color = ColorProperty([0, 0, 0, 1])
|
||
|
||
|
||
class SelectionItem(ThemableBehavior, MDRelativeLayout, TouchBehavior):
|
||
selected = BooleanProperty(False)
|
||
"""
|
||
Whether or not an item is checked.
|
||
|
||
:attr:`selected` is an :class:`~kivy.properties.BooleanProperty`
|
||
and defaults to `False`.
|
||
"""
|
||
|
||
owner = ObjectProperty()
|
||
"""
|
||
Instance of :class:`~kivymd.uix.selection.MDSelectionList` class.
|
||
|
||
:attr:`owner` is an :class:`~kivy.properties.ObjectProperty`
|
||
and defaults to `None`.
|
||
"""
|
||
|
||
instance_item = ObjectProperty()
|
||
"""
|
||
User item. Must be a Kivy or KivyMD widget.
|
||
|
||
:attr:`instance_item` is an :class:`~kivy.properties.ObjectProperty`
|
||
and defaults to `None`.
|
||
"""
|
||
|
||
instance_icon = ObjectProperty()
|
||
"""
|
||
Instance of :class:`~kivymd.uix.selection.SelectionIconCheck` class.
|
||
|
||
:attr:`instance_icon` is an :class:`~kivy.properties.ObjectProperty`
|
||
and defaults to `None`.
|
||
"""
|
||
|
||
overlay_color = ColorProperty([0, 0, 0, 0.2])
|
||
"""See :attr:`~MDSelectionList.overlay_color`."""
|
||
|
||
progress_round_size = NumericProperty(dp(46))
|
||
"""See :attr:`~MDSelectionList.progress_round_size`."""
|
||
|
||
progress_round_color = ColorProperty(None)
|
||
"""See :attr:`~MDSelectionList.progress_round_color`."""
|
||
|
||
_progress_round = NumericProperty(0)
|
||
_progress_line_end = NumericProperty(0)
|
||
_progress_animation = BooleanProperty(False)
|
||
_touch_long = BooleanProperty(False)
|
||
_instance_progress_inner_circle_color = ObjectProperty()
|
||
_instance_progress_inner_circle_ellipse = ObjectProperty()
|
||
_instance_progress_inner_outer_color = ObjectProperty()
|
||
_instance_progress_inner_outer_line = ObjectProperty()
|
||
_instance_overlay_color = ObjectProperty()
|
||
_instance_overlay_rounded_rec = ObjectProperty()
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
Clock.schedule_once(self.set_progress_round)
|
||
|
||
def set_progress_round(self, interval: Union[int, float]) -> None:
|
||
with self.canvas.after:
|
||
self._instance_progress_inner_circle_color = Color(
|
||
rgba=(0, 0, 0, 0)
|
||
)
|
||
self._instance_progress_inner_circle_ellipse = Ellipse(
|
||
size=self.get_progress_round_size(),
|
||
pos=self.get_progress_round_pos(),
|
||
)
|
||
self.bind(
|
||
pos=self.update_progress_inner_circle_ellipse,
|
||
size=self.update_progress_inner_circle_ellipse,
|
||
)
|
||
# FIXME: Radius value is not displayed.
|
||
self._instance_overlay_color = Color(rgba=(0, 0, 0, 0))
|
||
self._instance_overlay_rounded_rec = RoundedRectangle(
|
||
size=self.size,
|
||
pos=self.pos,
|
||
radius=self.instance_item.radius
|
||
if hasattr(self.instance_item, "radius")
|
||
else [
|
||
0,
|
||
],
|
||
)
|
||
self.bind(
|
||
pos=self.update_overlay_rounded_rec,
|
||
size=self.update_overlay_rounded_rec,
|
||
)
|
||
self._instance_progress_inner_outer_color = Color(rgba=(0, 0, 0, 0))
|
||
self._instance_progress_inner_outer_line = SmoothLine(
|
||
width=dp(4),
|
||
circle=[
|
||
self.center_x,
|
||
self.center_y,
|
||
self.progress_round_size * 0.58,
|
||
0,
|
||
0,
|
||
],
|
||
)
|
||
|
||
def do_selected_item(self, *args) -> None:
|
||
Animation(scale=1, d=0.2).start(self.instance_icon)
|
||
self.selected = True
|
||
self._progress_animation = False
|
||
self._instance_overlay_color.rgba = self.get_overlay_color()
|
||
self.owner.dispatch("on_selected", self)
|
||
|
||
def do_unselected_item(self) -> None:
|
||
Animation(scale=0, d=0.2).start(self.instance_icon)
|
||
self.selected = False
|
||
self._instance_overlay_color.rgba = self.get_overlay_color()
|
||
self.owner.dispatch("on_unselected", self)
|
||
|
||
def do_animation_progress_line(
|
||
self, animation: Animation, instance_selection_item, value: float
|
||
) -> None:
|
||
self._instance_progress_inner_outer_line.circle = (
|
||
self.center_x,
|
||
self.center_y,
|
||
self.progress_round_size * 0.58,
|
||
0,
|
||
360 * value,
|
||
)
|
||
|
||
def update_overlay_rounded_rec(self, *args) -> None:
|
||
self._instance_overlay_rounded_rec.size = self.size
|
||
self._instance_overlay_rounded_rec.pos = self.pos
|
||
|
||
def update_progress_inner_circle_ellipse(self, *args) -> None:
|
||
self._instance_progress_inner_circle_ellipse.size = (
|
||
self.get_progress_round_size()
|
||
)
|
||
self._instance_progress_inner_circle_ellipse.pos = (
|
||
self.get_progress_round_pos()
|
||
)
|
||
|
||
def reset_progress_animation(self) -> None:
|
||
Animation.cancel_all(self)
|
||
self._progress_animation = False
|
||
self._instance_progress_inner_circle_color.rgba = (0, 0, 0, 0)
|
||
self._instance_progress_inner_outer_color.rgba = (0, 0, 0, 0)
|
||
self._instance_progress_inner_outer_line.circle = [
|
||
self.center_x,
|
||
self.center_y,
|
||
self.progress_round_size * 0.58,
|
||
0,
|
||
0,
|
||
]
|
||
self._progress_line_end = 0
|
||
|
||
def get_overlay_color(self) -> list:
|
||
return self.overlay_color if self.selected else (0, 0, 0, 0)
|
||
|
||
def get_progress_round_pos(self) -> tuple:
|
||
return (
|
||
(self.pos[0] + self.width / 2) - self.progress_round_size / 2,
|
||
self.center_y - self.progress_round_size / 2,
|
||
)
|
||
|
||
def get_progress_round_size(self) -> tuple:
|
||
return self.progress_round_size, self.progress_round_size
|
||
|
||
def get_progress_round_color(self) -> tuple:
|
||
return (
|
||
self.theme_cls.primary_color
|
||
if not self.progress_round_color
|
||
else self.progress_round_color
|
||
)
|
||
|
||
def get_progress_line_color(self) -> tuple:
|
||
return (
|
||
self.theme_cls.primary_color[:-1] + [0.5]
|
||
if not self.progress_round_color
|
||
else self.progress_round_color[:-1] + [0.5]
|
||
)
|
||
|
||
def on_long_touch(self, *args) -> None:
|
||
if not self.owner.get_selected():
|
||
self._touch_long = True
|
||
self._progress_animation = True
|
||
|
||
def on_touch_up(self, touch):
|
||
if self.collide_point(*touch.pos):
|
||
if self._touch_long:
|
||
self._touch_long = False
|
||
return super().on_touch_up(touch)
|
||
|
||
def on_touch_down(self, touch):
|
||
if self.collide_point(*touch.pos):
|
||
if self.selected:
|
||
self.do_unselected_item()
|
||
else:
|
||
if self.owner.selected_mode:
|
||
self.do_selected_item()
|
||
return super().on_touch_down(touch)
|
||
|
||
def on__touch_long(self, instance_selection_tem, touch_value: bool) -> None:
|
||
if not touch_value:
|
||
self.reset_progress_animation()
|
||
|
||
def on__progress_animation(
|
||
self, instance_selection_tem, touch_value: bool
|
||
) -> None:
|
||
if touch_value:
|
||
anim = Animation(_progress_line_end=360, d=1, t="in_out_quad")
|
||
anim.bind(
|
||
on_progress=self.do_animation_progress_line,
|
||
on_complete=self.do_selected_item,
|
||
)
|
||
anim.start(self)
|
||
self._instance_progress_inner_outer_color.rgba = (
|
||
self.get_progress_line_color()
|
||
)
|
||
self._instance_progress_inner_circle_color.rgba = (
|
||
self.get_progress_round_color()
|
||
)
|
||
else:
|
||
self.reset_progress_animation()
|
||
|
||
|
||
class MDSelectionList(MDList):
|
||
"""
|
||
:Events:
|
||
`on_selected`
|
||
Called when a list item is selected.
|
||
`on_unselected`
|
||
Called when a list item is unselected.
|
||
"""
|
||
|
||
selected_mode = BooleanProperty(False)
|
||
"""
|
||
List item selection mode. If `True` when clicking on a list item, it will
|
||
be selected.
|
||
|
||
:attr:`selected_mode` is an :class:`~kivy.properties.BooleanProperty`
|
||
and defaults to `False`.
|
||
"""
|
||
|
||
icon = StringProperty("check")
|
||
"""
|
||
Name of the icon with which the selected list item will be marked.
|
||
|
||
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
|
||
and defaults to `'check'`.
|
||
"""
|
||
|
||
icon_pos = ListProperty()
|
||
"""
|
||
The position of the icon that will mark the selected list item.
|
||
|
||
:attr:`icon_pos` is an :class:`~kivy.properties.ListProperty`
|
||
and defaults to `[]`.
|
||
"""
|
||
|
||
icon_bg_color = ColorProperty([1, 1, 1, 1])
|
||
"""
|
||
Background color of the icon that will mark the selected list item.
|
||
|
||
:attr:`icon_bg_color` is an :class:`~kivy.properties.ColorProperty`
|
||
and defaults to `[1, 1, 1, 1]`.
|
||
"""
|
||
|
||
icon_check_color = ColorProperty([0, 0, 0, 1])
|
||
"""
|
||
Color of the icon that will mark the selected list item.
|
||
|
||
:attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty`
|
||
and defaults to `[1, 1, 1, 1]`.
|
||
"""
|
||
|
||
overlay_color = ColorProperty([0, 0, 0, 0.2])
|
||
"""
|
||
The overlay color of the selected list item..
|
||
|
||
:attr:`overlay_color` is an :class:`~kivy.properties.ColorProperty`
|
||
and defaults to `[0, 0, 0, 0.2]]`.
|
||
"""
|
||
|
||
progress_round_size = NumericProperty(dp(46))
|
||
"""
|
||
Size of the spinner for switching of `selected_mode` mode.
|
||
|
||
:attr:`progress_round_size` is an :class:`~kivy.properties.NumericProperty`
|
||
and defaults to `dp(46)`.
|
||
"""
|
||
|
||
progress_round_color = ColorProperty(None)
|
||
"""
|
||
Color of the spinner for switching of `selected_mode` mode.
|
||
|
||
:attr:`progress_round_color` is an :class:`~kivy.properties.NumericProperty`
|
||
and defaults to `None`.
|
||
"""
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
self.register_event_type("on_selected")
|
||
self.register_event_type("on_unselected")
|
||
|
||
def add_widget(self, widget, index=0, canvas=None):
|
||
selection_icon = SelectionIconCheck(
|
||
icon=self.icon,
|
||
md_bg_color=self.icon_bg_color,
|
||
icon_check_color=self.icon_check_color,
|
||
)
|
||
container = SelectionItem(
|
||
size_hint=(1, None),
|
||
height=widget.height,
|
||
instance_item=widget,
|
||
instance_icon=selection_icon,
|
||
overlay_color=self.overlay_color,
|
||
progress_round_size=self.progress_round_size,
|
||
progress_round_color=self.progress_round_color,
|
||
owner=self,
|
||
)
|
||
container.add_widget(widget)
|
||
|
||
if not self.icon_pos:
|
||
pos = (
|
||
dp(12),
|
||
container.height / 2 - selection_icon.height / 2,
|
||
)
|
||
else:
|
||
pos = self.icon_pos
|
||
selection_icon.pos = pos
|
||
container.add_widget(selection_icon)
|
||
return super().add_widget(container, index, canvas)
|
||
|
||
def get_selected(self) -> bool:
|
||
"""Returns ``True`` if at least one item in the list is checked."""
|
||
|
||
selected = False
|
||
for item in self.children:
|
||
if item.selected:
|
||
selected = True
|
||
break
|
||
return selected
|
||
|
||
def get_selected_list_items(self) -> list:
|
||
"""
|
||
Returns a list of marked objects:
|
||
|
||
[<kivymd.uix.selection.SelectionItem object>, ...]
|
||
"""
|
||
|
||
selected_list_items = []
|
||
for item in self.children:
|
||
if item.selected:
|
||
selected_list_items.append(item)
|
||
return selected_list_items
|
||
|
||
def unselected_all(self) -> None:
|
||
for item in self.children:
|
||
item.do_unselected_item()
|
||
self.selected_mode = False
|
||
|
||
def selected_all(self) -> None:
|
||
for item in self.children:
|
||
item.do_selected_item()
|
||
self.selected_mode = True
|
||
|
||
def on_selected(self, *args):
|
||
"""Called when a list item is selected."""
|
||
|
||
if not self.selected_mode:
|
||
self.selected_mode = True
|
||
|
||
def on_unselected(self, *args):
|
||
"""Called when a list item is unselected."""
|
||
|
||
self.selected_mode = self.get_selected()
|