# -*- coding: utf-8 -*- ''' Lists ===== `Material Design spec, Lists page `_ `Material Design spec, Lists: Controls page `_ The class :class:`MDList` in combination with a ListItem like :class:`OneLineListItem` will create a list that expands as items are added to it, working nicely with Kivy's :class:`~kivy.uix.scrollview.ScrollView`. Simple examples --------------- Kv Lang: .. code-block:: python ScrollView: do_scroll_x: False # Important for MD compliance MDList: OneLineListItem: text: "Single-line item" TwoLineListItem: text: "Two-line item" secondary_text: "Secondary text here" ThreeLineListItem: text: "Three-line item" secondary_text: "This is a multi-line label where you can fit more text than usual" Python: .. code-block:: python # Sets up ScrollView with MDList, as normally used in Android: sv = ScrollView() ml = MDList() sv.add_widget(ml) contacts = ["Paula", "John", "Kate", "Vlad"] for c in contacts: ml.add_widget( OneLineListItem( text=c ) ) Advanced usage -------------- Due to the variety in sizes and controls in the MD spec, this module suffers from a certain level of complexity to keep the widgets compliant, flexible and performant. For this KivyMD provides ListItems that try to cover the most common usecases, when those are insufficient, there's a base class called :class:`ListItem` which you can use to create your own ListItems. This documentation will only cover the provided ones, for custom implementations please refer to this module's source code. Text only ListItems ------------------- - :class:`~OneLineListItem` - :class:`~TwoLineListItem` - :class:`~ThreeLineListItem` These are the simplest ones. The :attr:`~ListItem.text` attribute changes the text in the most prominent line, while :attr:`~ListItem.secondary_text` changes the second and third line. If there are only two lines, :attr:`~ListItem.secondary_text` will shorten the text to fit in case it is too long; if a third line is available, it will instead wrap the text to make use of it. ListItems with widget containers -------------------------------- - :class:`~OneLineAvatarListItem` - :class:`~TwoLineAvatarListItem` - :class:`~ThreeLineAvatarListItem` - :class:`~OneLineIconListItem` - :class:`~TwoLineIconListItem` - :class:`~ThreeLineIconListItem` - :class:`~OneLineAvatarIconListItem` - :class:`~TwoLineAvatarIconListItem` - :class:`~ThreeLineAvatarIconListItem` These widgets will take other widgets that inherit from :class:`~ILeftBody`, :class:`ILeftBodyTouch`, :class:`~IRightBody` or :class:`~IRightBodyTouch` and put them in their corresponding container. As the name implies, :class:`~ILeftBody` and :class:`~IRightBody` will signal that the widget goes into the left or right container, respectively. :class:`~ILeftBodyTouch` and :class:`~IRightBodyTouch` do the same thing, except these widgets will also receive touch events that occur within their surfaces. Python example: .. code-block:: python class ContactPhoto(ILeftBody, AsyncImage): pass class MessageButton(IRightBodyTouch, MDIconButton): phone_number = StringProperty() def on_release(self): # sample code: Dialer.send_sms(phone_number, "Hey! What's up?") pass # Sets up ScrollView with MDList, as normally used in Android: sv = ScrollView() ml = MDList() sv.add_widget(ml) contacts = [ ["Annie", "555-24235", "http://myphotos.com/annie.png"], ["Bob", "555-15423", "http://myphotos.com/bob.png"], ["Claire", "555-66098", "http://myphotos.com/claire.png"] ] for c in contacts: item = TwoLineAvatarIconListItem( text=c[0], secondary_text=c[1] ) item.add_widget(ContactPhoto(source=c[2])) item.add_widget(MessageButton(phone_number=c[1]) ml.add_widget(item) API --- ''' from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ObjectProperty, StringProperty, NumericProperty, \ ListProperty, OptionProperty from kivy.uix.behaviors import ButtonBehavior from kivy.uix.floatlayout import FloatLayout from kivy.uix.gridlayout import GridLayout import kivymd.material_resources as m_res from kivymd.ripplebehavior import RectangularRippleBehavior from kivymd.theming import ThemableBehavior Builder.load_string(''' #:import m_res kivymd.material_resources cols: 1 size_hint_y: None height: self._min_list_height padding: 0, self._list_vertical_padding size_hint_y: None canvas: Color: rgba: self.theme_cls.divider_color Line: points: root.x,root.y, root.x+self.width,root.y BoxLayout: id: _text_container orientation: 'vertical' pos: root.pos padding: root._txt_left_pad, root._txt_top_pad, root._txt_right_pad, root._txt_bot_pad MDLabel: id: _lbl_primary text: root.text font_style: root.font_style theme_text_color: root.theme_text_color text_color: root.text_color size_hint_y: None height: self.texture_size[1] MDLabel: id: _lbl_secondary text: '' if root._num_lines == 1 else root.secondary_text font_style: root.secondary_font_style theme_text_color: root.secondary_theme_text_color text_color: root.secondary_text_color size_hint_y: None height: 0 if root._num_lines == 1 else self.texture_size[1] shorten: True if root._num_lines == 2 else False BoxLayout: id: _left_container size_hint: None, None x: root.x + dp(16) y: root.y + root.height/2 - self.height/2 size: dp(40), dp(40) BoxLayout: id: _left_container size_hint: None, None x: root.x + dp(16) y: root.y + root.height - root._txt_top_pad - self.height - dp(5) size: dp(40), dp(40) BoxLayout: id: _left_container size_hint: None, None x: root.x + dp(16) y: root.y + root.height/2 - self.height/2 size: dp(48), dp(48) BoxLayout: id: _left_container size_hint: None, None x: root.x + dp(16) y: root.y + root.height - root._txt_top_pad - self.height - dp(5) size: dp(48), dp(48) BoxLayout: id: _right_container size_hint: None, None x: root.x + root.width - m_res.HORIZ_MARGINS - self.width y: root.y + root.height/2 - self.height/2 size: dp(48), dp(48) BoxLayout: id: _right_container size_hint: None, None x: root.x + root.width - m_res.HORIZ_MARGINS - self.width y: root.y + root.height/2 - self.height/2 size: dp(48), dp(48) BoxLayout: id: _right_container size_hint: None, None x: root.x + root.width - m_res.HORIZ_MARGINS - self.width y: root.y + root.height/2 - self.height/2 size: dp(48), dp(48) BoxLayout: id: _right_container size_hint: None, None x: root.x + root.width - m_res.HORIZ_MARGINS - self.width y: root.y + root.height/2 - self.height/2 size: dp(48), dp(48) BoxLayout: id: _right_container size_hint: None, None x: root.x + root.width - m_res.HORIZ_MARGINS - self.width y: root.y + root.height - root._txt_top_pad - self.height - dp(5) size: dp(48), dp(48) ''') class MDList(GridLayout): '''ListItem container. Best used in conjunction with a :class:`kivy.uix.ScrollView`. When adding (or removing) a widget, it will resize itself to fit its children, plus top and bottom paddings as described by the MD spec. ''' selected = ObjectProperty() _min_list_height = dp(16) _list_vertical_padding = dp(8) icon = StringProperty() def add_widget(self, widget, index=0): super(MDList, self).add_widget(widget, index) self.height += widget.height def remove_widget(self, widget): super(MDList, self).remove_widget(widget) self.height -= widget.height class BaseListItem(ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, FloatLayout): '''Base class to all ListItems. Not supposed to be instantiated on its own. ''' text = StringProperty() '''Text shown in the first line. :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to "". ''' text_color = ListProperty(None) ''' Text color used if theme_text_color is set to 'Custom' ''' font_style = OptionProperty( 'Subhead', options=['Body1', 'Body2', 'Caption', 'Subhead', 'Title', 'Headline', 'Display1', 'Display2', 'Display3', 'Display4', 'Button', 'Icon']) theme_text_color = StringProperty('Primary',allownone=True) ''' Theme text color for primary text ''' secondary_text = StringProperty() '''Text shown in the second and potentially third line. The text will wrap into the third line if the ListItem's type is set to \'one-line\'. It can be forced into the third line by adding a \\n escape sequence. :attr:`secondary_text` is a :class:`~kivy.properties.StringProperty` and defaults to "". ''' secondary_text_color = ListProperty(None) ''' Text color used for secondary text if secondary_theme_text_color is set to 'Custom' ''' secondary_theme_text_color = StringProperty('Secondary',allownone=True) ''' Theme text color for secondary primary text ''' secondary_font_style = OptionProperty( 'Body1', options=['Body1', 'Body2', 'Caption', 'Subhead', 'Title', 'Headline', 'Display1', 'Display2', 'Display3', 'Display4', 'Button', 'Icon']) _txt_left_pad = NumericProperty(dp(16)) _txt_top_pad = NumericProperty() _txt_bot_pad = NumericProperty() _txt_right_pad = NumericProperty(m_res.HORIZ_MARGINS) _num_lines = 2 class ILeftBody: '''Pseudo-interface for widgets that go in the left container for ListItems that support it. Implements nothing and requires no implementation, for annotation only. ''' pass class ILeftBodyTouch: '''Same as :class:`~ILeftBody`, but allows the widget to receive touch events instead of triggering the ListItem's ripple effect ''' pass class IRightBody: '''Pseudo-interface for widgets that go in the right container for ListItems that support it. Implements nothing and requires no implementation, for annotation only. ''' pass class IRightBodyTouch: '''Same as :class:`~IRightBody`, but allows the widget to receive touch events instead of triggering the ListItem's ripple effect ''' pass class ContainerSupport: '''Overrides add_widget in a ListItem to include support for I*Body widgets when the appropiate containers are present. ''' _touchable_widgets = ListProperty() def add_widget(self, widget, index=0): if issubclass(widget.__class__, ILeftBody): self.ids['_left_container'].add_widget(widget) elif issubclass(widget.__class__, ILeftBodyTouch): self.ids['_left_container'].add_widget(widget) self._touchable_widgets.append(widget) elif issubclass(widget.__class__, IRightBody): self.ids['_right_container'].add_widget(widget) elif issubclass(widget.__class__, IRightBodyTouch): self.ids['_right_container'].add_widget(widget) self._touchable_widgets.append(widget) else: return super(BaseListItem, self).add_widget(widget,index) def remove_widget(self, widget): super(BaseListItem, self).remove_widget(widget) if widget in self._touchable_widgets: self._touchable_widgets.remove(widget) def on_touch_down(self, touch): if self.propagate_touch_to_touchable_widgets(touch, 'down'): return super(BaseListItem, self).on_touch_down(touch) def on_touch_move(self, touch, *args): if self.propagate_touch_to_touchable_widgets(touch, 'move', *args): return super(BaseListItem, self).on_touch_move(touch, *args) def on_touch_up(self, touch): if self.propagate_touch_to_touchable_widgets(touch, 'up'): return super(BaseListItem, self).on_touch_up(touch) def propagate_touch_to_touchable_widgets(self, touch, touch_event, *args): triggered = False for i in self._touchable_widgets: if i.collide_point(touch.x, touch.y): triggered = True if touch_event == 'down': i.on_touch_down(touch) elif touch_event == 'move': i.on_touch_move(touch, *args) elif touch_event == 'up': i.on_touch_up(touch) return triggered class OneLineListItem(BaseListItem): _txt_top_pad = NumericProperty(dp(16)) _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) _num_lines = 1 def __init__(self, **kwargs): super(OneLineListItem, self).__init__(**kwargs) self.height = dp(48) class TwoLineListItem(BaseListItem): _txt_top_pad = NumericProperty(dp(20)) _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) def __init__(self, **kwargs): super(TwoLineListItem, self).__init__(**kwargs) self.height = dp(72) class ThreeLineListItem(BaseListItem): _txt_top_pad = NumericProperty(dp(16)) _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) _num_lines = 3 def __init__(self, **kwargs): super(ThreeLineListItem, self).__init__(**kwargs) self.height = dp(88) class OneLineAvatarListItem(ContainerSupport, BaseListItem): _txt_left_pad = NumericProperty(dp(72)) _txt_top_pad = NumericProperty(dp(20)) _txt_bot_pad = NumericProperty(dp(19)) # dp(24) - dp(5) _num_lines = 1 def __init__(self, **kwargs): super(OneLineAvatarListItem, self).__init__(**kwargs) self.height = dp(56) class TwoLineAvatarListItem(OneLineAvatarListItem): _txt_top_pad = NumericProperty(dp(20)) _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) _num_lines = 2 def __init__(self, **kwargs): super(BaseListItem, self).__init__(**kwargs) self.height = dp(72) class ThreeLineAvatarListItem(ContainerSupport, ThreeLineListItem): _txt_left_pad = NumericProperty(dp(72)) class OneLineIconListItem(ContainerSupport, OneLineListItem): _txt_left_pad = NumericProperty(dp(72)) class TwoLineIconListItem(OneLineIconListItem): _txt_top_pad = NumericProperty(dp(20)) _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) _num_lines = 2 def __init__(self, **kwargs): super(BaseListItem, self).__init__(**kwargs) self.height = dp(72) class ThreeLineIconListItem(ContainerSupport, ThreeLineListItem): _txt_left_pad = NumericProperty(dp(72)) class OneLineRightIconListItem(ContainerSupport, OneLineListItem): # dp(40) = dp(16) + dp(24): _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) class TwoLineRightIconListItem(OneLineRightIconListItem): _txt_top_pad = NumericProperty(dp(20)) _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) _num_lines = 2 def __init__(self, **kwargs): super(BaseListItem, self).__init__(**kwargs) self.height = dp(72) class ThreeLineRightIconListitem(ContainerSupport, ThreeLineListItem): # dp(40) = dp(16) + dp(24): _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) class OneLineAvatarIconListItem(OneLineAvatarListItem): # dp(40) = dp(16) + dp(24): _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) class TwoLineAvatarIconListItem(TwoLineAvatarListItem): # dp(40) = dp(16) + dp(24): _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) class ThreeLineAvatarIconListItem(ThreeLineAvatarListItem): # dp(40) = dp(16) + dp(24): _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS)