""" Components/Selection ==================== .. seealso:: `Material Design spec, Banner `_ .. 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 = ''' 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.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(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): """ Selection list class. For more information, see in the :class:`~kivymd.uix.list.MDList` classes documentation. :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 in (r, g, b, a) or string format 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 in (r, g, b, a) or string format 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 in (r, g, b, a) or string format 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 in (r, g, b, a) or string format 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: [, ...] """ 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()