""" 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.''' content_text = StringProperty("") .. code-block:: kv content_text 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 --------------------- .. 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 user_font_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): 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; ''' # get the tab icon. count_icon = instance_tab.icon # print it on shell/bash. 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. .. 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): 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() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-text.gif :align: center Example with tab icon and text ------------------------------ .. 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): 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() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-icon-text.png :align: center Dynamic tab management ---------------------- .. code-block:: python from kivy.lang import Builder from kivy.uix.scrollview import ScrollView 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(ScrollView, MDTabsBase): '''Class implementing content for a tab.''' class Example(MDApp): index = 0 def build(self): 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(text=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() .. 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: .. 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] user_font_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): return Builder.load_string(KV) def on_start(self): for name_tab in self.icons: self.root.ids.tabs.add_widget( Tab( text=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.text == 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 ------------------------- .. 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} size_hint: None, None spacing: dp(48) MDIconButton: id: icon icon: "arrow-right" user_font_size: "48sp" on_release: app.switch_tab_by_name() MDIconButton: id: icon2 icon: "page-next" user_font_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.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() .. 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.uix.widget import Widget 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 ( FakeRectangularElevationBehavior, RectangularRippleBehavior, SpecificBackgroundColorBehavior, ) from kivymd.uix.boxlayout import MDBoxLayout 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: 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) 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(Widget): """ 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`. """ text = StringProperty(deprecated=True) """ This property is the actual title 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:`text` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. .. deprecated:: 1.0.0 Use :attr:`tab_label_text` instead. """ 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, **kwargs): self.tab_label = MDTabsLabel(tab=self) super().__init__(**kwargs) self.bind( icon=self._update_text, title=self._update_text, title_icon_mode=self._update_text, text=self.update_label_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.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 = self.text = self.tab_label_text def on_text(self, instance_user_tab, text_tab: str) -> None: self.tab_label_text = self.text 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( ThemableBehavior, FakeRectangularElevationBehavior, MDBoxLayout ): """ 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(ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout): """ You can use this class to create your own tabbed panel. :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 ``rgba`` 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 ``rgba`` format. :attr:`underline_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0]`. """ text_color_normal = ColorProperty(None) """ Text color 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 of the label when it is selected. :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ elevation = NumericProperty(0) """ Tab value elevation. .. seealso:: `Behaviors/Elevation `_ :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ indicator_color = ColorProperty(None) """ Color indicator in ``rgba`` 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, **kwargs): super().__init__(**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