mirror of
https://github.com/liberatedsystems/Sideband_CE.git
synced 2024-09-03 04:13:27 +02:00
1946 lines
65 KiB
Python
Executable File
1946 lines
65 KiB
Python
Executable File
"""
|
|
Components/Tabs
|
|
===============
|
|
|
|
.. seealso::
|
|
|
|
`Material Design spec, Tabs <https://material.io/components/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
|
|
|
|
<Tab>
|
|
|
|
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)
|
|
|
|
|
|
<Tab>
|
|
|
|
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: <kivymd.uix.tab.MDTabs object>;
|
|
:param instance_tab: <__main__.Tab object>;
|
|
:param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>;
|
|
: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: <kivymd.uix.tab.MDTabs object>;
|
|
:param instance_tab: <__main__.Tab object>;
|
|
:param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>;
|
|
: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)
|
|
|
|
|
|
<Tab>
|
|
|
|
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: <kivymd.uix.tab.MDTabs object>;
|
|
:param instance_tab: <__main__.Tab object>;
|
|
:param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>;
|
|
: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: <kivymd.uix.tab.MDTabs object>;
|
|
:param instance_tab: <__main__.Tab object>;
|
|
:param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>;
|
|
: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
|
|
|
|
|
|
<Tab>
|
|
|
|
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)
|
|
|
|
|
|
<Tab>
|
|
|
|
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: <kivymd.uix.tab.MDTabs object>
|
|
:param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>
|
|
:param instance_tab: <__main__.Tab object>
|
|
:param instance_tab_bar: <kivymd.uix.tab.MDTabsBar object>
|
|
:param instance_carousel: <kivymd.uix.tab.MDTabsCarousel object>
|
|
'''
|
|
|
|
# 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: <kivymd.uix.tab.MDTabs object>
|
|
:param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>
|
|
:param instance_tab: <__main__.Tab object>
|
|
:param instance_tab_bar: <kivymd.uix.tab.MDTabsBar object>
|
|
:param instance_carousel: <kivymd.uix.tab.MDTabsCarousel object>
|
|
'''
|
|
|
|
# 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
|
|
|
|
|
|
<Tab>
|
|
|
|
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.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 (
|
|
DeclarativeBehavior,
|
|
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:
|
|
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(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`.
|
|
"""
|
|
|
|
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,
|
|
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(
|
|
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(
|
|
DeclarativeBehavior,
|
|
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 <https://kivymd.readthedocs.io/en/latest/behaviors/elevation/index.html>`_
|
|
|
|
: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, *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
|