Sideband/sbapp/kivymd/uix/selection/selection.py

665 lines
21 KiB
Python
Raw Normal View History

2022-07-07 14:16:10 -06:00
"""
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 theyre 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 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 = "#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.utils.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.center_x - 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()