""" Components/Tabs =============== .. seealso:: `Material Design spec, Tabs `_ .. rubric:: Tabs organize content across different screens, data sets, and other interactions. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs.png :align: center .. Note:: Module provides tabs in the form of icons or text. Usage ----- To create a tab, you must create a new class that inherits from the :class:`~MDTabsBase` class and the `Kivy` container, in which you will create content for the tab. .. code-block:: python class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' .. code-block:: kv MDLabel: text: root.content_text pos_hint: {"center_x": .5, "center_y": .5} All tabs must be contained inside a :class:`~MDTabs` widget: .. code-block:: kv Root: MDTabs: Tab: title: "Tab 1" content_text: f"This is an example text for {self.title}" Tab: title: "Tab 2" content_text: f"This is an example text for {self.title}" ... Example with tab icon --------------------- .. tabs:: .. tab:: Declarative KV and imperative python styles .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.tab import MDTabsBase from kivymd.uix.floatlayout import MDFloatLayout from kivymd.icon_definitions import md_icons KV = ''' MDBoxLayout: orientation: "vertical" MDTopAppBar: title: "Example Tabs" MDTabs: id: tabs on_tab_switch: app.on_tab_switch(*args) MDIconButton: id: icon icon: root.icon icon_size: "48sp" pos_hint: {"center_x": .5, "center_y": .5} ''' class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): icons = list(md_icons.keys())[15:30] def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def on_start(self): for tab_name in self.icons: self.root.ids.tabs.add_widget(Tab(icon=tab_name)) def on_tab_switch( self, instance_tabs, instance_tab, instance_tab_label, tab_text ): ''' Called when switching tabs. :type instance_tabs: ; :param instance_tab: <__main__.Tab object>; :param instance_tab_label: ; :param tab_text: text or name icon of tab; ''' count_icon = instance_tab.icon # get the tab icon print(f"Welcome to {count_icon}' tab'") Example().run() .. tab:: Declarative python styles .. code-block:: python from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDIconButton from kivymd.uix.tab import MDTabsBase, MDTabs from kivymd.uix.floatlayout import MDFloatLayout from kivymd.icon_definitions import md_icons from kivymd.uix.toolbar import MDTopAppBar class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): icons = list(md_icons.keys())[15:30] def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return ( MDBoxLayout( MDTopAppBar(title="Example Tabs"), MDTabs(id="tabs"), orientation="vertical", ) ) def on_start(self): self.root.ids.tabs.bind(on_tab_switch=self.on_tab_switch) for tab_name in self.icons: self.root.ids.tabs.add_widget( Tab( MDIconButton( icon=tab_name, icon_size="48sp", pos_hint={"center_x": .5, "center_y": .5}, ), icon=tab_name, ) ) def on_tab_switch( self, instance_tabs, instance_tab, instance_tab_label, tab_text ): ''' Called when switching tabs. :type instance_tabs: ; :param instance_tab: <__main__.Tab object>; :param instance_tab_label: ; :param tab_text: text or name icon of tab; ''' count_icon = instance_tab.icon # get the tab icon print(f"Welcome to {count_icon}' tab'") Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example.gif :align: center Example with tab text --------------------- .. Note:: The :class:`~MDTabsBase` class has an icon parameter and, by default, tries to find the name of the icon in the file ``kivymd/icon_definitions.py``. If the name of the icon is not found, the class will send a message stating that the icon could not be found. if the tab has no icon, title or tab_label_text, the class will raise a ValueError. .. tabs:: .. tab:: Declarative KV and imperative python styles .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.tab import MDTabsBase KV = ''' MDBoxLayout: orientation: "vertical" MDTopAppBar: title: "Example Tabs" MDTabs: id: tabs on_tab_switch: app.on_tab_switch(*args) MDLabel: id: label text: "Tab 0" halign: "center" ''' class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def on_start(self): for i in range(20): self.root.ids.tabs.add_widget(Tab(title=f"Tab {i}")) def on_tab_switch( self, instance_tabs, instance_tab, instance_tab_label, tab_text ): '''Called when switching tabs. :type instance_tabs: ; :param instance_tab: <__main__.Tab object>; :param instance_tab_label: ; :param tab_text: text or name icon of tab; ''' instance_tab.ids.label.text = tab_text Example().run() .. tab:: Declarative python style .. code-block:: python from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.label import MDLabel from kivymd.uix.tab import MDTabsBase, MDTabs from kivymd.uix.toolbar import MDTopAppBar class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return ( MDBoxLayout( MDTopAppBar(title="Example Tabs"), MDTabs(id="tabs"), orientation="vertical", ) ) def on_start(self): self.root.ids.tabs.bind(on_tab_switch=self.on_tab_switch) for i in range(20): self.root.ids.tabs.add_widget( Tab( MDLabel(id="label", text="Tab 0", halign="center"), title=f"Tab {i}", ) ) def on_tab_switch( self, instance_tabs, instance_tab, instance_tab_label, tab_text ): ''' Called when switching tabs. :type instance_tabs: ; :param instance_tab: <__main__.Tab object>; :param instance_tab_label: ; :param tab_text: text or name icon of tab; ''' instance_tab.ids.label.text = tab_text Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-text.gif :align: center Example with tab icon and text ------------------------------ .. tabs:: .. tab:: Declarative KV and imperative python styles .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.tab import MDTabsBase from kivymd.uix.floatlayout import MDFloatLayout from kivymd.icon_definitions import md_icons KV = ''' MDBoxLayout: orientation: "vertical" MDTopAppBar: title: "Example Tabs" MDTabs: id: tabs ''' class Tab(MDFloatLayout, MDTabsBase): pass class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def on_start(self): for name_tab in list(md_icons.keys())[15:30]: self.root.ids.tabs.add_widget(Tab(icon=name_tab, title=name_tab)) Example().run() .. tab:: Declarative python style .. code-block:: python from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.tab import MDTabsBase, MDTabs from kivymd.uix.floatlayout import MDFloatLayout from kivymd.icon_definitions import md_icons from kivymd.uix.toolbar import MDTopAppBar class Tab(MDFloatLayout, MDTabsBase): pass class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return ( MDBoxLayout( MDTopAppBar(title="Example Tabs"), MDTabs(id="tabs"), orientation="vertical", ) ) def on_start(self): for name_tab in list(md_icons.keys())[15:30]: self.root.ids.tabs.add_widget(Tab(icon=name_tab, title=name_tab)) Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-icon-text.gif :align: center Dynamic tab management ---------------------- .. tabs:: .. tab:: Declarative KV and imperative python styles .. code-block:: python from kivy.lang import Builder from kivymd.uix.scrollview import MDScrollView from kivymd.app import MDApp from kivymd.uix.tab import MDTabsBase KV = ''' MDBoxLayout: orientation: "vertical" MDTopAppBar: title: "Example Tabs" MDTabs: id: tabs MDList: MDBoxLayout: adaptive_height: True MDFlatButton: text: "ADD TAB" on_release: app.add_tab() MDFlatButton: text: "REMOVE LAST TAB" on_release: app.remove_tab() MDFlatButton: text: "GET TAB LIST" on_release: app.get_tab_list() ''' class Tab(MDScrollView, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): index = 0 def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def on_start(self): self.add_tab() def get_tab_list(self): '''Prints a list of tab objects.''' print(self.root.ids.tabs.get_tab_list()) def add_tab(self): self.index += 1 self.root.ids.tabs.add_widget(Tab(title=f"{self.index} tab")) def remove_tab(self): if self.index > 1: self.index -= 1 self.root.ids.tabs.remove_widget( self.root.ids.tabs.get_tab_list()[-1] ) Example().run() .. tab:: Declarative python style .. code-block:: python from kivymd.uix.button import MDFlatButton from kivymd.uix.list import MDList from kivymd.uix.scrollview import MDScrollView from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.tab import MDTabsBase, MDTabs from kivymd.uix.toolbar import MDTopAppBar class Tab(MDScrollView, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): index = 0 def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return ( MDBoxLayout( MDTopAppBar(title="Example Tabs"), MDTabs(id="tabs"), orientation="vertical", ) ) def on_start(self): self.add_tab() def get_tab_list(self, *args): '''Prints a list of tab objects.''' print(self.root.ids.tabs.get_tab_list()) def add_tab(self, *args): self.index += 1 self.root.ids.tabs.add_widget( Tab( MDList( MDBoxLayout( MDFlatButton( text="ADD TAB", on_release=self.add_tab, ), MDFlatButton( text="REMOVE LAST TAB", on_release=self.remove_tab, ), MDFlatButton( text="GET TAB LIST", on_release=self.get_tab_list, ), adaptive_height=True, ), ), title=f"{self.index} tab", ) ) def remove_tab(self, *args): if self.index > 1: self.index -= 1 self.root.ids.tabs.remove_widget( self.root.ids.tabs.get_tab_list()[-1] ) Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-dynamic-managmant.gif :align: center Use on_ref_press method ----------------------- You can use markup for the text of the tabs and use the ``on_ref_press`` method accordingly: .. tabs:: .. tab:: Declarative KV and imperative python styles .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.floatlayout import MDFloatLayout from kivymd.font_definitions import fonts from kivymd.uix.tab import MDTabsBase from kivymd.icon_definitions import md_icons KV = ''' MDBoxLayout: orientation: "vertical" MDTopAppBar: title: "Example Tabs" MDTabs: id: tabs on_ref_press: app.on_ref_press(*args) MDIconButton: id: icon icon: app.icons[0] icon_size: "48sp" pos_hint: {"center_x": .5, "center_y": .5} ''' class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): icons = list(md_icons.keys())[15:30] def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def on_start(self): for name_tab in self.icons: self.root.ids.tabs.add_widget( Tab( title=f"[ref={name_tab}][font={fonts[-1]['fn_regular']}]{md_icons['close']}[/font][/ref] {name_tab}" ) ) def on_ref_press( self, instance_tabs, instance_tab_label, instance_tab, instance_tab_bar, instance_carousel, ): ''' The method will be called when the ``on_ref_press`` event occurs when you, for example, use markup text for tabs. :param instance_tabs: :param instance_tab_label: :param instance_tab: <__main__.Tab object> :param instance_tab_bar: :param instance_carousel: ''' # Removes a tab by clicking on the close icon on the left. for instance_tab in instance_carousel.slides: if instance_tab.title == instance_tab_label.text: instance_tabs.remove_widget(instance_tab_label) break Example().run() .. tab:: Declarative python style .. code-block:: python from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDIconButton from kivymd.uix.floatlayout import MDFloatLayout from kivymd.font_definitions import fonts from kivymd.uix.tab import MDTabsBase, MDTabs from kivymd.icon_definitions import md_icons from kivymd.uix.toolbar import MDTopAppBar class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): icons = list(md_icons.keys())[15:30] def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" return ( MDBoxLayout( MDTopAppBar(title="Example Tabs"), MDTabs(id="tabs"), orientation="vertical", ) ) def on_start(self): self.root.ids.tabs.bind(on_ref_press=self.on_ref_press) for name_tab in self.icons: self.root.ids.tabs.add_widget( Tab( MDIconButton( icon=self.icons[0], icon_size="48sp", pos_hint={"center_x": .5, "center_y": .5} ), title=( f"[ref={name_tab}][font={fonts[-1]['fn_regular']}]" f"{md_icons['close']}[/font][/ref] {name_tab}" ), ) ) def on_ref_press( self, instance_tabs, instance_tab_label, instance_tab, instance_tab_bar, instance_carousel, ): ''' The method will be called when the ``on_ref_press`` event occurs when you, for example, use markup text for tabs. :param instance_tabs: :param instance_tab_label: :param instance_tab: <__main__.Tab object> :param instance_tab_bar: :param instance_carousel: ''' # Removes a tab by clicking on the close icon on the left. for instance_tab in instance_carousel.slides: if instance_tab.title == instance_tab_label.text: instance_tabs.remove_widget(instance_tab_label) break Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-on-ref-press.gif :align: center Switching the tab by name ------------------------- .. tabs:: .. tab:: Declarative KV and imperative python styles .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp from kivymd.icon_definitions import md_icons from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.tab import MDTabsBase KV = ''' MDBoxLayout: orientation: "vertical" MDTopAppBar: title: "Example Tabs" MDTabs: id: tabs MDBoxLayout: orientation: "vertical" pos_hint: {"center_x": .5, "center_y": .5} adaptive_size: True spacing: dp(48) MDIconButton: id: icon icon: "arrow-right" icon_size: "48sp" on_release: app.switch_tab_by_name() MDIconButton: id: icon2 icon: "page-next" icon_size: "48sp" on_release: app.switch_tab_by_object() ''' class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): icons = list(md_icons.keys())[15:30] def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" self.iter_list_names = iter(list(self.icons)) return Builder.load_string(KV) def on_start(self): for name_tab in list(self.icons): self.root.ids.tabs.add_widget(Tab(tab_label_text=name_tab)) self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list())) def switch_tab_by_object(self): try: x = next(self.iter_list_objects) print(f"Switch slide by object, next element to show: [{x}]") self.root.ids.tabs.switch_tab(x) except StopIteration: # reset the iterator an begin again. self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list())) self.switch_tab_by_object() def switch_tab_by_name(self): '''Switching the tab by name.''' try: x = next(self.iter_list_names) print(f"Switch slide by name, next element to show: [{x}]") self.root.ids.tabs.switch_tab(x) except StopIteration: # Reset the iterator an begin again. self.iter_list_names = iter(list(self.icons)) self.switch_tab_by_name() Example().run() .. tab:: Declarative python style .. code-block:: python from kivy.metrics import dp from kivymd.app import MDApp from kivymd.icon_definitions import md_icons from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDIconButton from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.tab import MDTabsBase, MDTabs from kivymd.uix.toolbar import MDTopAppBar class Tab(MDFloatLayout, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): icons = list(md_icons.keys())[15:30] def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" self.iter_list_names = iter(list(self.icons)) return ( MDBoxLayout( MDTopAppBar(title="Example Tabs"), MDTabs(id="tabs"), orientation="vertical", ) ) def on_start(self): for name_tab in list(self.icons): self.root.ids.tabs.add_widget( Tab( MDBoxLayout( MDIconButton( id="icon", icon="arrow-right", icon_size="48sp", on_release=self.switch_tab_by_name, ), MDIconButton( id="icon2", icon="arrow-left", icon_size="48sp", on_release=self.switch_tab_by_object, ), orientation="vertical", pos_hint={"center_x": .5, "center_y": .5}, adaptive_size=True, spacing=dp(48), ), tab_label_text=name_tab, ) ) self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list())) def switch_tab_by_object(self, *args): try: x = next(self.iter_list_objects) print(f"Switch slide by object, next element to show: [{x}]") self.root.ids.tabs.switch_tab(x) except StopIteration: # reset the iterator an begin again. self.iter_list_objects = iter( list(self.root.ids.tabs.get_tab_list())) self.switch_tab_by_object() def switch_tab_by_name(self, *args): '''Switching the tab by name.''' try: x = next(self.iter_list_names) print(f"Switch slide by name, next element to show: [{x}]") self.root.ids.tabs.switch_tab(x) except StopIteration: # Reset the iterator an begin again. self.iter_list_names = iter(list(self.icons)) self.switch_tab_by_name() Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switching-tab-by-name.gif :align: center """ __all__ = ("MDTabs", "MDTabsBase") import os from typing import Union from kivy.clock import Clock from kivy.graphics.texture import Texture from kivy.lang import Builder from kivy.logger import Logger from kivy.metrics import dp from kivy.properties import ( AliasProperty, BooleanProperty, BoundedNumericProperty, ColorProperty, ListProperty, NumericProperty, ObjectProperty, OptionProperty, StringProperty, ) from kivy.uix.anchorlayout import AnchorLayout from kivy.uix.behaviors import ToggleButtonBehavior from kivy.uix.scrollview import ScrollView from kivy.utils import boundary from kivymd import uix_path from kivymd.font_definitions import fonts, theme_font_styles from kivymd.icon_definitions import md_icons from kivymd.theming import ThemableBehavior, ThemeManager from kivymd.uix.behaviors import ( DeclarativeBehavior, RectangularRippleBehavior, SpecificBackgroundColorBehavior, ) from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.card import MDCard from kivymd.uix.carousel import MDCarousel from kivymd.uix.label import MDLabel with open(os.path.join(uix_path, "tab", "tab.kv"), encoding="utf-8") as kv_file: Builder.load_string(kv_file.read()) class MDTabsException(Exception): pass class MDTabsLabel(ToggleButtonBehavior, RectangularRippleBehavior, MDLabel): """This class it represent the label of each tab.""" text_color_normal = ColorProperty(None) text_color_active = ColorProperty(None) tab = ObjectProperty() tab_bar = ObjectProperty() font_name = StringProperty("Roboto") def __init__(self, **kwargs): self.split_str = " ,-" super().__init__(**kwargs) self.max_lines = 2 self.size_hint_x = None self.size_hint_min_x = dp(90) self.min_space = dp(98) self.bind( text=self._update_text_size, ) def on_release(self) -> None: try: self.tab_bar.parent.dispatch( "on_tab_switch", self.tab, self, self.text ) # If the label is selected load the relative tab from carousel. if self.state == "down": self.tab_bar.parent.carousel.load_slide(self.tab) except KeyError: pass def on_texture(self, instance_tabs_label, texture: Texture) -> None: # Just save the minimum width of the label based of the content. if texture: max_width = dp(360) min_width = dp(90) if texture.width > max_width: self.width = max_width self.text_size = (max_width, None) elif texture.width < min_width: self.width = min_width else: self.width = texture.width def _update_text_size(self, *args): if not self.tab_bar: return if self.tab_bar.parent.allow_stretch is True: self.text_size = (None, None) else: self.width = self.tab_bar.parent.fixed_tab_label_width self.text_size = (self.width, None) Clock.schedule_once(self.tab_bar._label_request_indicator_update, 0) class MDTabsBase: """ This class allow you to create a tab. You must create a new class that inherits from MDTabsBase. In this way you have total control over the views of your tabbed panel. """ icon = StringProperty() """ This property will set the Tab's Label Icon. :attr:`icon` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"]) """ This property sets the mode in wich the tab's title and icon are shown. :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Lead'`. """ title = StringProperty() """ This property will set the Name of the tab. .. note:: As a side note. All tabs have set `markup = True`. Thanks to this, you can use the kivy markup language to set a colorful and fully customizable tabs titles. .. warning:: The material design requires that every title label is written in capital letters, because of this, the `string.upper()` will be applied to it's contents. :attr:`title` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ title_is_capital = BooleanProperty(False) """ This value controls wether if the title property should be converted to capital letters. :attr:`title_is_capital` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ tab_label_text = StringProperty() """ This property is the actual title's Label of the tab. use the property :attr:`icon` and :attr:`title` to set this property correctly. This property is kept public for specific and backward compatibility purposes. :attr:`tab_label_text` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ tab_label = ObjectProperty() """ It is the label object reference of the tab. :attr:`tab_label` is an :class:`~kivy.properties.ObjectProperty` and defaults to `None`. """ def _get_label_font_style(self): if self.tab_label: return self.tab_label.font_style def _set_label_font_style(self, value): if self.tab_label: if value in theme_font_styles: self.tab_label.font_style = value else: raise ValueError( "tab_label_font_style:\n\t" "font_style not found in theme_font_styles\n\t" f"font_style = {value}" ) else: Clock.schedule_once(lambda x: self._set_label_font_style(value)) return True tab_label_font_style = AliasProperty( _get_label_font_style, _set_label_font_style, cache=True, ) """ :attr:`tab_label_font_style` is an :class:`~kivy.properties.AliasProperty` that behavies similar to an :class:`~kivy.properties.OptionProperty`. This property's behavior allows the developer to use any new label style registered to the app. This property will affect the Tab's Title Label widget. """ def __init__(self, *args, **kwargs): self.tab_label = MDTabsLabel(tab=self) super().__init__(*args, **kwargs) self.bind( icon=self._update_text, title=self._update_text, title_icon_mode=self._update_text, tab_label_text=self.update_label_text, title_is_capital=self.update_label_text, ) Clock.schedule_once( self._update_text ) # this will ensure the text is correct def _update_text(self, *args): # Ensures that the title is in capital letters. if self.title and self.title_is_capital is True: if self.title != self.title.upper(): self.title = self.title.upper() # Avoids event recursion. return # Add the icon. if self.icon and self.icon in md_icons: self.tab_label_text = f"[size=24sp][font={fonts[-1]['fn_regular']}]{md_icons[self.icon]}[/size][/font]" if self.title: self.tab_label_text = ( self.tab_label_text + (" " if self.title_icon_mode == "Lead" else "\n") + self.title ) # Add the title. else: if self.icon: Logger.error( f"{self}: [UID] = [{self.uid}]:\n\t" f"Icon '{self.icon}' not found in md_icons" ) if self.title: self.tab_label_text = self.title else: if not self.tab_label_text: raise ValueError( f"{self}: [UID] = [{self.uid}]:\n\t" "No valid Icon was found.\n\t" "No valid Title was found.\n\t" f"Icon\t= '{self.icon}'\n\t" f"Title\t= '{self.title}'\n\t" ) self.tab_label.padding = dp(16), 0 self.update_label_text(None, self.tab_label_text) def update_label_text(self, instance_user_tab, text_tab: str) -> None: self.tab_label.text = text_tab class MDTabsMain(MDBoxLayout): """ This class is just a boxlayout that contain the carousel. It allows you to have control over the carousel. """ class MDTabsCarousel(MDCarousel): lock_swiping = BooleanProperty(False) """ If True - disable switching tabs by swipe. :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ def on_touch_move(self, touch): if self.lock_swiping: # lock a swiping return if not self.touch_mode_change: if self.ignore_perpendicular_swipes and self.direction in ( "top", "bottom", ): if abs(touch.oy - touch.y) < self.scroll_distance: if abs(touch.ox - touch.x) > self.scroll_distance: self._change_touch_mode() self.touch_mode_change = True elif self.ignore_perpendicular_swipes and self.direction in ( "right", "left", ): if abs(touch.ox - touch.x) < self.scroll_distance: if abs(touch.oy - touch.y) > self.scroll_distance: self._change_touch_mode() self.touch_mode_change = True if self._get_uid("cavoid") in touch.ud: return if self._touch is not touch: super().on_touch_move(touch) return self._get_uid() in touch.ud if touch.grab_current is not self: return True ud = touch.ud[self._get_uid()] direction = self.direction[0] if ud["mode"] == "unknown": if direction in "rl": distance = abs(touch.ox - touch.x) else: distance = abs(touch.oy - touch.y) if distance > self.scroll_distance: ev = self._change_touch_mode_ev if ev is not None: ev.cancel() ud["mode"] = "scroll" else: if direction in "rl": self._offset += touch.dx if direction in "tb": self._offset += touch.dy return True class MDTabsScrollView(ScrollView): """This class hacked version to fix scroll_x manual setting.""" def goto( self, scroll_x: Union[float, None], scroll_y: Union[float, None] ) -> None: """Update event value along with scroll_*.""" def _update(e, x): if e: e.value = (e.max + e.min) * x if not (scroll_x is None): self.scroll_x = scroll_x _update(self.effect_x, scroll_x) if not (scroll_y is None): self.scroll_y = scroll_y _update(self.effect_y, scroll_y) class MDTabsBar(MDCard): """ This class is just a boxlayout that contains the scroll view for tabs. It is also responsible for resizing the tab shortcut when necessary. """ target = ObjectProperty(None, allownone=True) """ It is the carousel reference of the next tab / slide. When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the target tab / slide of the carousel. :attr:`target` is an :class:`~kivy.properties.ObjectProperty` and default to `None`. """ def get_rect_instruction(self): canvas_instructions = self.layout.canvas.before.get_group( "Indicator_line" ) return canvas_instructions[0] indicator = AliasProperty(get_rect_instruction, cache=True) """ It is the :class:`~kivy.graphics.vertex_instructions.RoundedRectangle` instruction reference of the tab indicator. :attr:`indicator` is an :class:`~kivy.properties.AliasProperty`. """ def get_last_scroll_x(self): return self.scrollview.scroll_x last_scroll_x = AliasProperty( get_last_scroll_x, bind=("target",), cache=True ) """ Is the carousel reference of the next tab/slide. When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the target tab/slide of the carousel. :attr:`last_scroll_x` is an :class:`~kivy.properties.AliasProperty`. """ def __init__(self, **kwargs): super().__init__(**kwargs) def update_indicator( self, x: Union[float, int], w: Union[float, int], radius=None ) -> None: # Update position and size of the indicator. if self.parent.tab_indicator_type == "line-round": self.parent._line_x = x self.parent._line_width = w self.parent._line_height = self.parent.tab_indicator_height self.parent._line_radius = self.parent.tab_indicator_height / 2 elif self.parent.tab_indicator_type == "line-rect": self.parent._line_x = x self.parent._line_width = w self.parent._line_height = self.parent.tab_indicator_height else: self.indicator.pos = (x, 0) self.indicator.size = (w, self.parent.tab_indicator_height) if radius: self.indicator.radius = radius def tab_bar_autoscroll(self, instance_tab_label: MDTabsLabel, step: float): # Automatic scroll animation of the tab bar. bound_left = self.center_x - self.x bound_right = self.layout.width - bound_left dt = instance_tab_label.center_x - bound_left sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0) lsx = self.last_scroll_x # ast scroll x of the tab bar scroll_is_late = lsx < sx # determine scroll direction dst = abs(lsx - sx) * step # distance to run if not dst: return if scroll_is_late and instance_tab_label.center_x > bound_left: x = lsx + dst elif not scroll_is_late and instance_tab_label.center_x < bound_right: x = lsx - dst else: return x = boundary(x, 0.0, 1.0) self.scrollview.goto(x, None) def android_animation( self, instance_carousel: MDTabsCarousel, offset: Union[float, int] ): # Try to reproduce the android animation effect. if offset != 0 and abs(offset) < instance_carousel.width: forward = offset < 0 offset = abs(offset) step = offset / float(instance_carousel.width) indicator_animation = self.parent.tab_indicator_anim skip_slide = ( instance_carousel.slides[instance_carousel._skip_slide] if instance_carousel._skip_slide is not None else None ) next_slide = ( instance_carousel.next_slide if forward else instance_carousel.previous_slide ) self.target = skip_slide if skip_slide else next_slide if not self.target: return a = instance_carousel.current_slide.tab_label b = self.target.tab_label self.tab_bar_autoscroll(b, step) # Avoids the animation if `indicator_animation` is True. if indicator_animation is False: return gap_x = abs((a.x) - (b.x)) gap_w = (b.width) - (a.width) if forward: x_step = a.x + (gap_x * step) else: x_step = a.x - gap_x * step w_step = a.width + (gap_w * step) self.update_indicator(x_step, w_step) def _label_request_indicator_update(self, *args): widget = self.carousel.current_slide.tab_label self.update_indicator(widget.x, widget.width) class MDTabs( DeclarativeBehavior, ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout, ): """ Tabs class. You can use this class to create your own tabbed panel. For more information, see in the :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and :class:`~kivymd.theming.ThemableBehavior` and :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and :class:`~kivy.uix.anchorlayout.AnchorLayout` classes documentation. :Events: `on_tab_switch` Called when switching tabs. `on_slide_progress` Called while the slide is scrolling. `on_ref_press` The method will be called when the ``on_ref_press`` event occurs when you, for example, use markup text for tabs. """ tab_bar_height = NumericProperty("48dp") """ Height of the tab bar. :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty` and defaults to `'48dp'`. """ tab_padding = ListProperty([0, 0, 0, 0]) """ Padding of the tab bar. :attr:`tab_padding` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0, 0, 0]`. """ tab_indicator_anim = BooleanProperty(False) """ Tab indicator animation. If you want use animation set it to ``True``. :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ tab_indicator_height = NumericProperty("2dp") """ Height of the tab indicator. :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty` and defaults to `'2dp'`. """ tab_indicator_type = OptionProperty( "line", options=["line", "fill", "round", "line-round", "line-rect"] ) """ Type of tab indicator. Available options are: `'line'`, `'fill'`, `'round'`, `'line-rect'` and `'line-round'`. :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty` and defaults to `'line'`. """ tab_hint_x = BooleanProperty(False) """ This option affects the size of each child. if it's `True`, the size of each tab will be ignored and will use the size available by the container. :attr:`tab_hint_x` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ anim_duration = NumericProperty(0.2) """ Duration of the slide animation. :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty` and defaults to `0.2`. """ anim_threshold = BoundedNumericProperty( 0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0 ) """ Animation threshold allow you to change the tab indicator animation effect. :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `0.8`. """ allow_stretch = BooleanProperty(True) """ If `True`, the tab will update dynamically (if :attr:`tab_hint_x` is `True`) to it's content width, and wrap any text if the widget is wider than `"360dp"`. If `False`, the tab won't update to it's maximum texture width. this means that the `fixed_tab_label_width` will be used as the label width. this will wrap any text inside to fit the fixed value. :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ fixed_tab_label_width = NumericProperty("140dp") """ If :attr:`allow_stretch` is `False`, the class will set this value as the width to all the tabs title label. :attr:`fixed_tab_label_width` is an :class:`~kivy.properties.NumericProperty` and defaults to `140dp`. """ background_color = ColorProperty(None) """ Background color of tabs in (r, g, b, a) or string format. :attr:`background_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ underline_color = ColorProperty([0, 0, 0, 0]) """ Underline color of tabs in (r, g, b, a) or string format. :attr:`underline_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0]`. """ text_color_normal = ColorProperty(None) """ Text color in (r, g, b, a) or string format of the label when it is not selected. :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ text_color_active = ColorProperty(None) """ Text color in (r, g, b, a) or string format of the label when it is selected. :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ shadow_softness = NumericProperty(12) """ See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.shadow_softness` attribute. .. versionadded:: 1.1.0 :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty` and defaults to `12`. """ shadow_color = ColorProperty([0, 0, 0, 0.6]) """ See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.shadow_color` attribute. .. versionadded:: 1.1.0 :attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0.6]`. """ shadow_offset = ListProperty((0, 0)) """ See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.shadow_offset` attribute. .. versionadded:: 1.1.0 :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. """ elevation = NumericProperty(0) """ See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.elevation` attribute. :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ indicator_color = ColorProperty(None) """ Color indicator in (r, g, b, a) or string format. :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ lock_swiping = BooleanProperty(False) """ If True - disable switching tabs by swipe. :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ font_name = StringProperty("Roboto") """ Font name for tab text. :attr:`font_name` is an :class:`~kivy.properties.StringProperty` and defaults to `'Roboto'`. """ ripple_duration = NumericProperty(2) """ Ripple duration when long touching to tab. :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty` and defaults to `2`. """ no_ripple_effect = BooleanProperty(True) """ Whether to use the ripple effect when tapping on a tab. :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"]) """ This property sets the mode in wich the tab's title and icon are shown. :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Lead'`. """ force_title_icon_mode = BooleanProperty(True) """ If this property is se to `True`, it will force the class to update every tab inside the scroll view to the current `title_icon_mode` :attr:`force_title_icon_mode` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.register_event_type("on_tab_switch") self.register_event_type("on_ref_press") self.register_event_type("on_slide_progress") Clock.schedule_once(self._carousel_bind, 1) self.theme_cls.bind( primary_palette=self.update_icon_color, theme_style=self.update_icon_color, ) self.bind( force_title_icon_mode=self._parse_icon_mode, title_icon_mode=self._parse_icon_mode, ) self.bind(tab_hint_x=self._update_tab_hint_x) def update_icon_color( self, instance_theme_manager: ThemeManager, name_theme_style_name_palette: str, ) -> None: """ Called when the app's color scheme or style has changed (dark theme/light theme). """ for tab_label in self.get_tab_list(): if not self.text_color_normal: tab_label.text_color_normal = self.theme_cls.text_color if not self.text_color_active: tab_label.text_color_active = self.specific_secondary_text_color def switch_tab(self, name_tab: Union[MDTabsLabel, str], search_by="text"): """ This method switch between tabs name_tab can be either a String or a :class:`~MDTabsBase`. `search_by` will look up through the properties of every tab. If the value doesnt match, it will raise a ValueError. Search_by options: text : will search by the raw text of the label (`tab_label_text`) icon : will search by the `icon` property title : will search by the `title` property """ if isinstance(name_tab, str): if search_by == "title": for tab_instance in self.tab_bar.parent.carousel.slides: if tab_instance.title_is_capital is True: _name_tab = name_tab.upper() else: _name_tab = name_tab if tab_instance.title == _name_tab: self.carousel.load_slide(tab_instance) return # Search by icon. elif search_by == "icon": for tab_instance in self.tab_bar.parent.carousel.slides: if tab_instance.icon == name_tab: self.carousel.load_slide(tab_instance) return # Search by title. else: for tab_instance in self.tab_bar.parent.carousel.slides: if tab_instance.tab_label_text == name_tab: self.carousel.load_slide(tab_instance) return raise ValueError( "switch_tab:\n\t" "name_tab not found in the tab list\n\t" f"search_by = {repr(search_by)} \n\t" f"name_tab = {repr(name_tab)} \n\t" ) else: self.carousel.load_slide(name_tab.tab) def get_tab_list(self) -> list: """Returns a list of :class:`~MDTabsLabel` objects.""" return self.tab_bar.layout.children[::-1] def get_slides(self) -> list: """Returns a list of user tab objects.""" return self.carousel.slides def get_current_tab(self): """ Returns current tab object. .. versionadded:: 1.0.0 """ return self.carousel.current_slide def add_widget(self, widget, index=0, canvas=None): # You can add only subclass of MDTabsBase. if not isinstance(widget, (MDTabsBase, MDTabsMain, MDTabsBar)): raise ValueError( f"MDTabs[{self.uid}].add_widget:\n\t" "The widget provided is not a subclass of MDTabsBase." ) if len(self.children) >= 2: try: # FIXME: Can't set the value of the `no_ripple_effect` # and `ripple_duration` properties for widget.tab_label. widget.tab_label._no_ripple_effect = self.no_ripple_effect widget.tab_label.ripple_duration_in_slow = self.ripple_duration widget.tab_label.group = str(self) widget.tab_label.tab_bar = self.tab_bar widget.tab_label.font_name = self.font_name widget.tab_label.text_color_normal = ( self.text_color_normal if self.text_color_normal else self.specific_secondary_text_color ) widget.tab_label.text_color_active = ( self.text_color_active if self.text_color_active else self.specific_text_color ) self.bind( allow_stretch=widget.tab_label._update_text_size, fixed_tab_label_width=widget.tab_label._update_text_size, font_name=widget.tab_label.setter("font_name"), text_color_active=widget.tab_label.setter( "text_color_active" ), text_color_normal=widget.tab_label.setter( "text_color_normal" ), ) Clock.schedule_once(widget.tab_label._update_text_size, 0) self.tab_bar.layout.add_widget(widget.tab_label) self.carousel.add_widget(widget) if self.force_title_icon_mode is True: widget.title_icon_mode = self.title_icon_mode Clock.schedule_once( self.tab_bar._label_request_indicator_update, 0 ) return except AttributeError: pass if isinstance(widget, (MDTabsMain, MDTabsBar)): return super().add_widget(widget) def remove_widget(self, widget): # You can remove only subclass of MDTabsLabel or MDTabsBase. if not issubclass(widget.__class__, (MDTabsLabel, MDTabsBase)): raise MDTabsException( "MDTabs can remove only subclass of MDTabsLabel or MDTabsBase" ) # If the widget is an instance of MDTabsBase, then the widget is # set as the widget's tab_label object. if issubclass(widget.__class__, MDTabsBase): slide = widget title_label = widget.tab_label else: # We already got the label, so we set the slide reference. slide = widget.tab title_label = widget # Set memory. # Search object next tab. # Clean all bindings to allow the widget to be collected. self.unbind( allow_stretch=title_label._update_text_size, fixed_tab_label_width=title_label._update_text_size, font_name=title_label.setter("font_name"), text_color_active=title_label.setter("text_color_active"), text_color_normal=title_label.setter("text_color_normal"), ) self.carousel.remove_widget(slide) self.tab_bar.layout.remove_widget(title_label) # Clean the references. slide = None title_label = None widget = None def on_slide_progress(self, *args) -> None: """ This event is deployed every available frame while the tab is scrolling. """ def on_carousel_index(self, instance_tabs_carousel, index: int) -> None: """ Called when the Tab index have changed. This event is deployed by the built in carousel of the class. """ # When the index of the carousel change, update tab indicator, # select the current tab and reset threshold data. if instance_tabs_carousel.current_slide: current_tab_label = instance_tabs_carousel.current_slide.tab_label if current_tab_label.state == "normal": # current_tab_label._do_press() current_tab_label.dispatch("on_release") current_tab_label._release_group(self) current_tab_label.state = "down" if self.tab_indicator_type == "round": self.tab_indicator_height = self.tab_bar_height if index == 0: radius = [ 0, self.tab_bar_height / 2, self.tab_bar_height / 2, 0, ] self.tab_bar.update_indicator( current_tab_label.x, current_tab_label.width, radius ) elif index == len(self.get_tab_list()) - 1: radius = [ self.tab_bar_height / 2, 0, 0, self.tab_bar_height / 2, ] self.tab_bar.update_indicator( current_tab_label.x, current_tab_label.width, radius ) else: radius = [ self.tab_bar_height / 2, ] self.tab_bar.update_indicator( current_tab_label.x, current_tab_label.width, radius ) elif ( self.tab_indicator_type == "fill" or self.tab_indicator_type == "line-round" or self.tab_indicator_type == "line-rect" ): self.tab_indicator_height = self.tab_bar_height self.tab_bar.update_indicator( current_tab_label.x, current_tab_label.width ) else: self.tab_bar.update_indicator( current_tab_label.x, current_tab_label.width ) def on_ref_press(self, *args) -> None: """ This event will be launched every time the user press a markup enabled label with a link or reference inside. """ def on_tab_switch(self, *args) -> None: """This event is launched every time the current tab is changed.""" def on_size(self, instance_tab, size: list) -> None: """Called when the application screen is resized.""" if self.carousel.current_slide: self._update_indicator(self.carousel.current_slide.tab_label) def _update_tab_hint_x(self, *args): if not self.ids.layout.children: return if self.tab_hint_x is True: self.fixed_tab_label_width = self.width // len( self.ids.layout.children ) self.allow_stretch = False else: self.allow_stretch = True def _parse_icon_mode(self, *args): if self.force_title_icon_mode is True: for slide in self.carousel.slides: slide.title_icon_mode = self.title_icon_mode if self.title_icon_mode == "Top": self.tab_bar_height = dp(72) else: self.tab_bar_height = dp(48) def _carousel_bind(self, interval): self.carousel.bind(on_slide_progress=self._on_slide_progress) def _on_slide_progress(self, *args): self.dispatch("on_slide_progress", args) def _update_indicator(self, current_tab_label): def update_indicator(interval): self.tab_bar.update_indicator( current_tab_label.x, current_tab_label.width ) if not current_tab_label: current_tab_label = self.tab_bar.layout.children[-1] Clock.schedule_once(update_indicator) def _update_padding(self, layout, *args): if self.tab_hint_x is True: layout.padding = [0, 0] Clock.schedule_once(self._update_tab_hint_x) return True padding = [0, 0] # FIXME: It's not entirely clear why the `padding = [dp (52), 0]` # instruction is needed? This creates an extra 52px left padding and # looks like a bug. This instruction was added by the contributors in # previous commits and I have not yet figured out why this was done. # This is more efficient than to use sum([layout.children]). # width = layout.width - (layout.padding[0] * 2) # Forces the padding of the tab_bar when the tab_bar is scrollable. # if width > self.width: # padding = [dp(52), 0] # Set the new padding. layout.padding = padding # Update the indicator. if self.carousel.current_slide: self._update_indicator(self.carousel.current_slide.tab_label) Clock.schedule_once( lambda x: setattr( self.carousel.current_slide.tab_label, "state", "down" ), -1, ) return True