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

666 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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