""" Components/SegmentedButton ========================== .. versionadded:: 1.2.0 .. seealso:: `Material Design spec, Segmented buttons `_ `Segmented control `_ .. rubric:: Segmented buttons help people select options, switch views, or sort elements. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-preview.png :align: center Usage ----- .. code-block:: kv MDScreen: MDSegmentedButton: MDSegmentedButtonItem: icon: ... text: ... MDSegmentedButtonItem: icon: ... text: ... MDSegmentedButtonItem: icon: ... text: ... Example ------- .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp KV = ''' MDScreen: MDSegmentedButton: pos_hint: {"center_x": .5, "center_y": .5} MDSegmentedButtonItem: text: "Walking" MDSegmentedButtonItem: text: "Transit" MDSegmentedButtonItem: text: "Driving" ''' class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) Example().run() By default, segmented buttons support single marking of elements: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-false.gif :align: center For multiple marking of elements, use the :attr:`kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton.multiselect` parameter: .. code-block:: kv MDSegmentedButton: multiselect: True .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-true.gif :align: center Control width ------------- The width of the panel of segmented buttons will be equal to the width of the texture of the widest button multiplied by the number of buttons: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-by-default.png :align: center But you can use the `size_hint_x` parameter to specify the relative width: .. code-block:: kv MDSegmentedButton: size_hint_x: .9 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-size-hint-x.png :align: center Customization ------------- You can see below in the documentation from which classes the :class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton` and :class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButtonItem` classes are inherited and use all their attributes such as `md_bg_color`, `md_bg_color` etc. for additional customization of segments. Events ------ - on_marked The method is called when a segment is marked. - on_unmarked The method is called when a segment is unmarked. .. code-block:: kv MDSegmentedButton: on_marked: app.on_marked(*args) .. code-block:: python def on_marked( self, segment_button: MDSegmentedButton, segment_item: MDSegmentedButtonItem, marked: bool, ) -> None: print(segment_button) print(segment_item) print(marked) A practical example ------------------- .. code-block:: python import os from faker import Faker from kivy.clock import Clock from kivy.lang import Builder from kivy.properties import StringProperty from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.segmentedbutton import MDSegmentedButton, MDSegmentedButtonItem from kivymd.utils import asynckivy KV = ''' adaptive_height: True md_bg_color: "#343930" radius: 16 TwoLineAvatarListItem: id: item divider: None _no_ripple_effect: True text: root.name secondary_text: root.path_to_file theme_text_color: "Custom" text_color: "#8A8D79" secondary_theme_text_color: self.theme_text_color secondary_text_color: self.text_color on_size: self.ids._left_container.size = (item.height, item.height) self.ids._left_container.x = dp(6) self._txt_right_pad = item.height + dp(12) ImageLeftWidget: source: root.album radius: root.radius MDScreen: md_bg_color: "#151514" MDBoxLayout: orientation: "vertical" padding: "12dp" spacing: "12dp" MDLabel: adaptive_height: True text: "Your downloads" font_style: "H5" theme_text_color: "Custom" text_color: "#8A8D79" MDSegmentedButton: size_hint_x: 1 selected_color: "#303A29" line_color: "#343930" on_marked: app.on_marked(*args) MDSegmentedButtonItem: text: "Songs" active: True MDSegmentedButtonItem: text: "Albums" MDSegmentedButtonItem: text: "Podcasts" RecycleView: id: card_list viewclass: "UserCard" bar_width: 0 RecycleBoxLayout: orientation: 'vertical' spacing: "16dp" padding: "16dp" default_size: None, dp(72) default_size_hint: 1, None size_hint_y: None height: self.minimum_height ''' class UserCard(MDBoxLayout): name = StringProperty() path_to_file = StringProperty() album = StringProperty() class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) def on_marked( self, segment_button: MDSegmentedButton, segment_item: MDSegmentedButtonItem, marked: bool, ) -> None: self.generate_card() def generate_card(self): async def generate_card(): for i in range(10): await asynckivy.sleep(0) self.root.ids.card_list.data.append( { "name": fake.name(), "path_to_file": f"{os.path.splitext(fake.file_path())[0]}.mp3", "album": fake.image_url(), } ) fake = Faker() self.root.ids.card_list.data = [] Clock.schedule_once(lambda x: asynckivy.start(generate_card())) Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-practical-example.gif :align: center """ from __future__ import annotations __all__ = ("MDSegmentedButton", "MDSegmentedButtonItem") import os from kivy.animation import Animation from kivy.clock import Clock from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ( BooleanProperty, ColorProperty, ListProperty, NumericProperty, StringProperty, VariableListProperty, ) from kivy.uix.behaviors import ButtonBehavior from kivymd import uix_path from kivymd.uix.behaviors import RectangularRippleBehavior, ScaleBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.label import MDIcon with open( os.path.join(uix_path, "segmentedbutton", "segmentedbutton.kv"), encoding="utf-8", ) as kv_file: Builder.load_string(kv_file.read()) class MDSegmentedButtonItem( RectangularRippleBehavior, ButtonBehavior, MDFloatLayout ): """ Segment button item. For more information, see in the :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. """ icon = StringProperty() """ Icon segment. :attr:`icon` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ text = StringProperty() """ Text segment. :attr:`text` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ active = BooleanProperty(False) """ Background color of an disabled segment. :attr:`active` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ disabled_color = ColorProperty(None) """ Is active segment. :attr:`active` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ _no_ripple_effect = BooleanProperty(True) _current_icon = "" _current_md_bg_color = None def on_disabled(self, instance, value: bool) -> None: def on_disabled(*args): if value: if not self._current_md_bg_color: self._current_md_bg_color = self.md_bg_color self.md_bg_color = ( self.theme_cls.disabled_hint_text_color if not self.disabled_color else self.disabled_color ) else: if self._current_md_bg_color: self.md_bg_color = self._current_md_bg_color self._current_md_bg_color = None Clock.schedule_once(on_disabled) def on_icon(self, instance, icon_name: str): if icon_name != "check": self._current_icon = icon_name # TODO: # Add the feature to use both text and icons in segments - # https://m3.material.io/components/segmented-buttons/guidelines#26abac1c-c6bd-44c1-a969-8c910c880b98 # Icons: optional check icon to indicate selected state - # https://m3.material.io/components/segmented-buttons/overview#7b80f313-7d3a-4865-b26c-1f7ec98ba694 # Hovered: add a color for the hovered segment - # https://m3.material.io/components/segmented-buttons/specs#d730b3ba-c59e-4ef8-b652-20979fe20b67 # Density: Each step down in density removes 4dp from the height - # https://m3.material.io/components/segmented-buttons/specs#2d5cab36-1deb-40bd-9e37-bc2bb1657009 class MDSegmentedButton(MDBoxLayout): """ Segment button panel. For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. :Events: `on_marked` The method is called when a segment is marked. `on_unmarked` The method is called when a segment is unmarked. """ radius = VariableListProperty([20], length=4) """ Panel radius. :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[20, 20, 20, 20]`. """ multiselect = BooleanProperty(False) """ Do I allow multiple segment selection. :attr:`multiselect` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ hiding_icon_transition = StringProperty("linear") """ Name of the transition hiding the current icon. :attr:`hiding_icon_transition` is a :class:`~kivy.properties.StringProperty` and defaults to `'linear'`. """ hiding_icon_duration = NumericProperty(0.05) """ Duration of hiding the current icon. :attr:`hiding_icon_duration` is a :class:`~kivy.properties.NumericProperty` and defaults to `0.05`. """ opening_icon_transition = StringProperty("linear") """ The name of the transition that opens a new icon of the "marked" type. :attr:`opening_icon_transition` is a :class:`~kivy.properties.StringProperty` and defaults to `'linear'`. """ opening_icon_duration = NumericProperty(0.05) """ The duration of opening a new icon of the "marked" type. :attr:`opening_icon_duration` is a :class:`~kivy.properties.NumericProperty` and defaults to `0.05`. """ selected_items = ListProperty() """ The list of :class:`~MDSegmentedButtonItem` objects that are currently marked. :attr:`selected_items` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ selected_color = ColorProperty(None) """ Color of the marked segment. :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.register_event_type("on_marked") self.register_event_type("on_unmarked") Clock.schedule_once(self.mark_segment) Clock.schedule_once(self.adjust_segment_radius) Clock.schedule_once(self.adjust_segment_panel_width, 2) def mark_segment(self, *args) -> None: """Programmatically marks a segment.""" for widget in self.children: if widget.active: widget.active = False widget.dispatch("on_release") if not self.multiselect: break def adjust_segment_radius(self, *args) -> None: """Rounds off the first and last elements.""" if self.children[0].radius == [0, 0, 0, 0]: self.children[0].radius = (0, self.height / 2, self.height / 2, 0) if self.children[-1].radius == [0, 0, 0, 0]: self.children[-1].radius = (self.height / 2, 0, 0, self.height / 2) def adjust_segment_panel_width(self, *args) -> None: """ Sets the width of all segments and the width of the panel by the widest segment. """ if not self.size_hint_x: width_list = [ widget.ids.label_text.texture_size[0] + (dp(72) if widget.icon else dp(48)) for widget in self.children ] max_width = max(width_list) self.width = max_width * len(width_list) else: max_width = self.width / len(self.children) for widget in self.children: widget.width = max_width self.opacity = 1 for widget in self.children: if widget.active: widget.dispatch("on_release") def shift_segment_text(self, segment_item: MDSegmentedButtonItem) -> None: """ Shifts the segment text to the right, thus freeing up space for the icon (when the segment is marked). """ Animation( x=( segment_item.ids.label_text.x + ( dp(16) if not segment_item.icon and not segment_item.active else 0 ) ) if not segment_item.active else ( segment_item.ids.label_text.x - ( dp(16) if not segment_item.icon and segment_item.active else 0 ) ), d=0.2, ).start(segment_item.ids.label_text) def show_icon_marked_segment( self, segment_item: MDSegmentedButtonItem ) -> None: """ Sets the icon for the marked segment and changes the icon scale to the normal scale. """ segment_item.ids.scale_icon.icon = "check" if segment_item.ids.scale_icon.icon == "check" and segment_item.active: segment_item.ids.scale_icon.icon = segment_item._current_icon Animation( scale_value_x=1, scale_value_y=1, d=self.opening_icon_duration, t=self.opening_icon_transition, ).start(segment_item.ids.scale_icon) self.shift_segment_text(segment_item) self.set_selected_segment_list(segment_item) self.set_bg_marked_segment(segment_item) def hide_icon_marked_segment( self, segment_item: MDSegmentedButtonItem ) -> None: """Changes the scale of the icon of the marked segment to zero.""" anim = Animation( scale_value_x=0, scale_value_y=0, d=self.hiding_icon_duration, t=self.hiding_icon_transition, ) anim.bind( on_complete=lambda x, y: self.show_icon_marked_segment(segment_item) ) anim.start(segment_item.ids.scale_icon) def restore_bg_segment(self, segment_item) -> None: Animation(md_bg_color=self.md_bg_color, d=0.2).start(segment_item) def set_bg_marked_segment(self, segment_item) -> None: if segment_item.active: Animation( md_bg_color=self.selected_color if self.selected_color else self.theme_cls.primary_color, d=0.2, ).start(segment_item) def set_selected_segment_list(self, segment_item) -> None: segment_item.active = not segment_item.active if segment_item.active: self.selected_items.append(segment_item) self.dispatch("on_marked", segment_item, segment_item.active) else: if segment_item in self.selected_items: self.selected_items.remove(segment_item) self.dispatch("on_unmarked", segment_item, segment_item.active) def mark_item(self, segment_item: MDSegmentedButtonItem) -> None: if segment_item.active and not self.multiselect: return if not self.multiselect and self.selected_items: self.uncheck_item() else: if segment_item.active: self.restore_bg_segment(segment_item) self.hide_icon_marked_segment(segment_item) def uncheck_item(self) -> None: for item in self.children: if item.active: self.hide_icon_marked_segment(item) self.restore_bg_segment(item) break def add_widget(self, widget, *args, **kwargs): if isinstance(widget, MDSegmentedButtonItem): widget.bind(on_release=self.mark_item) return super().add_widget(widget) def on_size(self, instance_segment_button, size: list) -> None: """Called when the root screen is resized.""" if self.size_hint_x: max_width = size[0] / len(self.children) for widget in self.children: widget.width = max_width def on_marked(self, *args): """The method is called when a segment is marked.""" def on_unmarked(self, *args): """The method is called when a segment is unmarked.""" class SegmentButtonIcon(MDIcon, ScaleBehavior): """Implements an icon with scaling behavior."""