1393 lines
41 KiB
Python
1393 lines
41 KiB
Python
"""
|
|
Components/Tabs
|
|
===============
|
|
|
|
.. seealso::
|
|
|
|
`Material Design spec, Tabs <https://m3.material.io/components/tabs/overview>`_
|
|
|
|
.. rubric:: Tabs organize content across different screens, data sets,
|
|
and other interactions.
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-preview.png
|
|
:align: center
|
|
|
|
- Use tabs to group content into helpful categories
|
|
- Two types: primary and secondary
|
|
- Tabs can horizontally scroll, so a UI can have as many tabs as needed
|
|
- Place tabs next to each other as peers
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-types.png
|
|
:align: center
|
|
|
|
1. Primary tabs
|
|
2. Secondary tabs
|
|
|
|
Usage primary tabs
|
|
------------------
|
|
|
|
Primary tabs should be used when just one set of tabs are needed.
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import (
|
|
MDTabsItem,
|
|
MDTabsItemIcon,
|
|
MDTabsItemText,
|
|
MDTabsBadge,
|
|
)
|
|
|
|
KV = '''
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDTabsPrimary:
|
|
id: tabs
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
|
|
MDDivider:
|
|
'''
|
|
|
|
|
|
class Example(MDApp):
|
|
def on_start(self):
|
|
for tab_icon, tab_name in {
|
|
"airplane": "Flights",
|
|
"treasure-chest": "Trips",
|
|
"compass-outline": "Explore",
|
|
}.items():
|
|
if tab_icon == "treasure-chest":
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItem(
|
|
MDTabsItemIcon(
|
|
MDTabsBadge(
|
|
text="99",
|
|
),
|
|
icon=tab_icon,
|
|
),
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
)
|
|
)
|
|
else:
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItem(
|
|
MDTabsItemIcon(
|
|
icon=tab_icon,
|
|
),
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
)
|
|
)
|
|
self.root.ids.tabs.switch_tab(icon="airplane")
|
|
|
|
def build(self):
|
|
self.theme_cls.primary_palette = "Olive"
|
|
return Builder.load_string(KV)
|
|
|
|
|
|
Example().run()
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-usage.png
|
|
:align: center
|
|
|
|
Anatomy primary tabs
|
|
--------------------
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-anatomy.png
|
|
:align: center
|
|
|
|
Usage secondary tabs
|
|
--------------------
|
|
|
|
Secondary tabs are necessary when a screen requires more than one level of
|
|
tabs. These tabs use a simpler style of indicator, but their function is
|
|
identical to primary tabs.
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import (
|
|
MDTabsItemIcon,
|
|
MDTabsItemText,
|
|
MDTabsBadge, MDTabsItemSecondary,
|
|
)
|
|
|
|
KV = '''
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDTabsSecondary:
|
|
id: tabs
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
|
|
MDDivider:
|
|
'''
|
|
|
|
|
|
class Example(MDApp):
|
|
def on_start(self):
|
|
for tab_icon, tab_name in {
|
|
"airplane": "Flights",
|
|
"treasure-chest": "Trips",
|
|
"compass-outline": "Explore",
|
|
}.items():
|
|
if tab_icon == "treasure-chest":
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItemSecondary(
|
|
MDTabsItemIcon(
|
|
icon=tab_icon,
|
|
),
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
MDTabsBadge(
|
|
text="5",
|
|
),
|
|
)
|
|
)
|
|
else:
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItemSecondary(
|
|
MDTabsItemIcon(
|
|
icon=tab_icon,
|
|
),
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
)
|
|
)
|
|
self.root.ids.tabs.switch_tab(icon="airplane")
|
|
|
|
def build(self):
|
|
self.theme_cls.primary_palette = "Olive"
|
|
return Builder.load_string(KV)
|
|
|
|
|
|
Example().run()
|
|
|
|
Anatomy secondary tabs
|
|
----------------------
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-secondary-anatomy.png
|
|
:align: center
|
|
|
|
Related content
|
|
---------------
|
|
|
|
Use tabs to group related content, not sequential content.
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.label import MDLabel
|
|
from kivymd.uix.tab import (
|
|
MDTabsItemIcon,
|
|
MDTabsItemText,
|
|
MDTabsItem,
|
|
)
|
|
|
|
KV = '''
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDTabsPrimary:
|
|
id: tabs
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
size_hint_x: .6
|
|
|
|
MDDivider:
|
|
|
|
MDTabsCarousel:
|
|
id: related_content_container
|
|
size_hint_y: None
|
|
height: dp(320)
|
|
'''
|
|
|
|
|
|
class Example(MDApp):
|
|
def on_start(self):
|
|
for tab_icon, tab_name in {
|
|
"airplane": "Flights",
|
|
"treasure-chest": "Trips",
|
|
"compass-outline": "Explore",
|
|
}.items():
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItem(
|
|
MDTabsItemIcon(
|
|
icon=tab_icon,
|
|
),
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
)
|
|
)
|
|
self.root.ids.related_content_container.add_widget(
|
|
MDLabel(
|
|
text=tab_name,
|
|
halign="center",
|
|
)
|
|
)
|
|
self.root.ids.tabs.switch_tab(icon="airplane")
|
|
|
|
def build(self):
|
|
self.theme_cls.primary_palette = "Olive"
|
|
return Builder.load_string(KV)
|
|
|
|
|
|
Example().run()
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-related-content.gif
|
|
:align: center
|
|
|
|
Behaviors
|
|
=========
|
|
|
|
Scrollable tabs
|
|
---------------
|
|
|
|
When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs
|
|
can use longer text labels and a larger number of tabs. They are best used for
|
|
browsing on touch interfaces.
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import MDTabsItemText, MDTabsItem
|
|
|
|
KV = '''
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDTabsPrimary:
|
|
id: tabs
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
size_hint_x: .6
|
|
allow_stretch: False
|
|
label_only: True
|
|
|
|
MDDivider:
|
|
'''
|
|
|
|
|
|
class Example(MDApp):
|
|
def on_start(self):
|
|
for tab_name in [
|
|
"Moscow",
|
|
"Saint Petersburg",
|
|
"Novosibirsk",
|
|
"Yekaterinburg",
|
|
"Kazan",
|
|
"Nizhny Novgorod",
|
|
"Chelyabinsk",
|
|
]:
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItem(
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
)
|
|
)
|
|
self.root.ids.tabs.switch_tab(text="Moscow")
|
|
|
|
def build(self):
|
|
self.theme_cls.primary_palette = "Olive"
|
|
return Builder.load_string(KV)
|
|
|
|
|
|
Example().run()
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-scrollable-behavior.gif
|
|
:align: center
|
|
|
|
Fixed tabs
|
|
==========
|
|
|
|
Fixed tabs display all tabs in a set simultaneously. They are best for
|
|
switching between related content quickly, such as between transportation
|
|
methods in a map. To navigate between fixed tabs, tap an individual tab, or
|
|
swipe left or right in the content area.
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import MDTabsItemText, MDTabsItem
|
|
|
|
KV = '''
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDTabsPrimary:
|
|
id: tabs
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
size_hint_x: .6
|
|
allow_stretch: True
|
|
label_only: True
|
|
|
|
MDDivider:
|
|
'''
|
|
|
|
|
|
class Example(MDApp):
|
|
def on_start(self):
|
|
for tab_name in [
|
|
"Moscow", "Saint Petersburg", "Novosibirsk"
|
|
]:
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItem(
|
|
MDTabsItemText(
|
|
text=tab_name,
|
|
),
|
|
)
|
|
)
|
|
self.root.ids.tabs.switch_tab(text="Moscow")
|
|
|
|
def build(self):
|
|
self.theme_cls.primary_palette = "Olive"
|
|
return Builder.load_string(KV)
|
|
|
|
|
|
Example().run()
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-fixed-behavior.png
|
|
:align: center
|
|
|
|
Tap a tab
|
|
---------
|
|
|
|
Navigate to a tab by tapping on it.
|
|
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-tap-a-tab-behavior.gif
|
|
:align: center
|
|
|
|
Swipe within the content area
|
|
-----------------------------
|
|
|
|
To navigate between tabs, users can swipe left or right within the content
|
|
area.
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tab-primary-swipe-within-content-area-behavior.gif
|
|
:align: center
|
|
|
|
Switching tab
|
|
=============
|
|
|
|
You can switch tabs by icon name, by tab name, and by tab objects:
|
|
|
|
.. code-block:: python
|
|
|
|
instance_tabs.switch_tab(icon="airplane")
|
|
|
|
.. code-block:: python
|
|
|
|
instance_tabs.switch_tab(text="Airplane")
|
|
|
|
.. code-block:: python
|
|
|
|
instance_tabs.switch_tab(
|
|
instance=instance_tabs_item # MDTabsItem
|
|
)
|
|
|
|
API break
|
|
=========
|
|
|
|
1.2.0 version
|
|
-------------
|
|
|
|
.. 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
|
|
from kivymd.icon_definitions import md_icons
|
|
|
|
KV = '''
|
|
MDBoxLayout:
|
|
|
|
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):
|
|
return Builder.load_string(KV)
|
|
|
|
def on_start(self):
|
|
for name_tab in self.icons:
|
|
self.root.ids.tabs.add_widget(
|
|
Tab(title=name_tab, icon=name_tab)
|
|
)
|
|
|
|
|
|
Example().run()
|
|
|
|
2.0.0 version
|
|
-------------
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.icon_definitions import md_icons
|
|
from kivymd.uix.label import MDIcon
|
|
from kivymd.uix.tab import MDTabsItem, MDTabsItemIcon
|
|
from kivymd.uix.tab.tab import MDTabsItemText
|
|
|
|
KV = '''
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDTabsPrimary:
|
|
id: tabs
|
|
allow_stretch: False
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
|
|
MDDivider:
|
|
|
|
MDTabsCarousel:
|
|
id: related_content
|
|
size_hint_y: None
|
|
height: root.height - tabs.ids.tab_scroll.height
|
|
'''
|
|
|
|
|
|
class Example(MDApp):
|
|
def on_start(self):
|
|
for name_tab in list(md_icons.keys())[15:30]:
|
|
self.root.ids.tabs.add_widget(
|
|
MDTabsItem(
|
|
MDTabsItemIcon(
|
|
icon=name_tab,
|
|
),
|
|
MDTabsItemText(
|
|
text=name_tab,
|
|
),
|
|
)
|
|
)
|
|
self.root.ids.related_content.add_widget(
|
|
MDIcon(
|
|
icon=name_tab,
|
|
pos_hint={"center_x": 0.5, "center_y": 0.5},
|
|
)
|
|
)
|
|
self.root.ids.tabs.switch_tab(icon="airplane")
|
|
|
|
def build(self):
|
|
return Builder.load_string(KV)
|
|
|
|
|
|
Example().run()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
__all__ = (
|
|
"MDTabsPrimary",
|
|
"MDTabsSecondary",
|
|
"MDTabsItem",
|
|
"MDTabsItemSecondary",
|
|
"MDTabsItemIcon",
|
|
"MDTabsItemText",
|
|
"MDTabsCarousel",
|
|
"MDTabsBadge",
|
|
)
|
|
|
|
import os
|
|
|
|
from kivy.uix.anchorlayout import AnchorLayout
|
|
from kivy.uix.carousel import Carousel
|
|
from kivy.uix.widget import Widget
|
|
from kivy.utils import boundary
|
|
from kivy.animation import Animation
|
|
from kivy.clock import Clock
|
|
from kivy.lang import Builder
|
|
from kivy.metrics import dp
|
|
from kivy.properties import (
|
|
ObjectProperty,
|
|
BooleanProperty,
|
|
ColorProperty,
|
|
NumericProperty,
|
|
AliasProperty,
|
|
StringProperty,
|
|
VariableListProperty,
|
|
)
|
|
from kivy.uix.behaviors import ButtonBehavior
|
|
from kivy.uix.boxlayout import BoxLayout
|
|
from kivy.uix.scrollview import ScrollView
|
|
|
|
from kivymd import uix_path
|
|
from kivymd.theming import ThemableBehavior
|
|
from kivymd.uix.badge import MDBadge
|
|
from kivymd.uix.behaviors import (
|
|
DeclarativeBehavior,
|
|
RectangularRippleBehavior,
|
|
BackgroundColorBehavior,
|
|
)
|
|
from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior
|
|
from kivymd.uix.label import MDLabel, MDIcon
|
|
|
|
with open(os.path.join(uix_path, "tab", "tab.kv"), encoding="utf-8") as kv_file:
|
|
Builder.load_string(kv_file.read())
|
|
|
|
|
|
###############################################################################
|
|
#
|
|
# COMMON CLASSES
|
|
#
|
|
###############################################################################
|
|
|
|
|
|
class MDTabsBadge(MDBadge):
|
|
"""
|
|
Implements an badge for secondary tabs.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.badge.badge.MDBadge` class documentation.
|
|
"""
|
|
|
|
|
|
class MDTabsCarousel(Carousel):
|
|
"""
|
|
Implements a carousel for user-generated content.
|
|
|
|
For more information, see in the
|
|
:class:`~kivy.uix.carousel.Carousel` class documentation.
|
|
"""
|
|
|
|
lock_swiping = BooleanProperty(False)
|
|
"""
|
|
If True - disable switching tabs by swipe.
|
|
|
|
:attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `False`.
|
|
"""
|
|
|
|
_tabs = ObjectProperty() # MDTabsPrimary/MDTabsSecondary object
|
|
|
|
def on_touch_move(self, touch) -> str | bool | None:
|
|
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(BackgroundColorBehavior, ScrollView):
|
|
"""
|
|
Implements a scrollable list of tabs.
|
|
This class hacked version to fix scroll_x manual setting.
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and
|
|
:class:`~kivy.uix.scrollview.ScrollView`
|
|
classes documentation.
|
|
"""
|
|
|
|
def goto(self, scroll_x: float | None, scroll_y: 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 MDTabsItemText(MDLabel):
|
|
"""
|
|
Implements an label for the :class:`~MDTabsItem` class.
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.label.label.MDLabel` class documentation.
|
|
|
|
.. versionadded:: 2.0.0
|
|
"""
|
|
|
|
_active = BooleanProperty(False)
|
|
|
|
|
|
class MDTabsItemIcon(MDIcon):
|
|
"""
|
|
Implements an icon for the :class:`~MDTabsItem` class.
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.label.label.MDIcon` class documentation.
|
|
|
|
.. versionadded:: 2.0.0
|
|
"""
|
|
|
|
|
|
class MDTabsItemBase(
|
|
DeclarativeBehavior,
|
|
BackgroundColorBehavior,
|
|
RectangularRippleBehavior,
|
|
ButtonBehavior,
|
|
ThemableBehavior,
|
|
StateLayerBehavior,
|
|
):
|
|
"""
|
|
Implements a base item with an icon and text.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and
|
|
:class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and
|
|
:class:`~kivymd.uix.behaviors.behaviors.ripple_behavior.RectangularRippleBehavior` and
|
|
:class:`~kivy.uix.behaviors.ButtonBehavior` and
|
|
:class:`~kivymd.theming.ThemableBehavior`
|
|
classes documentation.
|
|
"""
|
|
|
|
active = BooleanProperty(False)
|
|
"""
|
|
Is the tab active.
|
|
|
|
:attr:`active` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `False`.
|
|
"""
|
|
|
|
_tabs = ObjectProperty() # MDTabsPrimary/MDTabsSecondary object
|
|
_tab_content = ObjectProperty() # Carousel slide (related content) object
|
|
|
|
def on_release(self, *args) -> None:
|
|
"""
|
|
Fired when the button is released
|
|
(i.e. the touch/click that pressed the button goes away).
|
|
"""
|
|
|
|
if self._tab_content:
|
|
self._tabs._tabs_carousel.load_slide(self._tab_content)
|
|
|
|
self._tabs.update_indicator(instance=self)
|
|
self._tabs.dispatch("on_tab_switch", self, self._tab_content)
|
|
self._tabs._current_tab = self
|
|
self._tabs._current_related_content = self._tab_content
|
|
|
|
|
|
class MDTabsItem(MDTabsItemBase, BoxLayout):
|
|
"""
|
|
Implements a item with an icon and text for :class:`~MDTabsPrimary` class.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
For more information, see in the
|
|
:class:`~MDTabsItemBase` and
|
|
:class:`~kivy.uix.boxlayout.BoxLayout`
|
|
classes documentation.
|
|
"""
|
|
|
|
def add_widget(self, widget, *args, **kwargs):
|
|
if isinstance(widget, (MDTabsItemText, MDTabsItemIcon)):
|
|
if len(self.children) <= 1:
|
|
Clock.schedule_once(lambda x: self._set_width(widget))
|
|
|
|
def _set_width(self, widget):
|
|
def set_width(*args):
|
|
self.width = widget.texture_size[0] + widget.padding_x + 2
|
|
|
|
if not self._tabs.allow_stretch and isinstance(widget, MDTabsItemText):
|
|
Clock.schedule_once(set_width)
|
|
|
|
super().add_widget(widget)
|
|
|
|
|
|
###############################################################################
|
|
#
|
|
# PRIMARY CLASSES
|
|
#
|
|
###############################################################################
|
|
|
|
|
|
class MDTabsPrimary(DeclarativeBehavior, ThemableBehavior, BoxLayout):
|
|
"""
|
|
Tabs primary class.
|
|
|
|
.. versionchanged:: 2.0.0
|
|
|
|
Rename from `MDTabs` to `MDTabsPrimary` class.
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and
|
|
:class:`~kivymd.theming.ThemableBehavior` and
|
|
:class:`~kivy.uix.boxlayout.BoxLayout`
|
|
classes documentation.
|
|
|
|
:Events:
|
|
`on_tab_switch`
|
|
Fired when switching tabs.
|
|
"""
|
|
|
|
md_bg_color = ColorProperty(None)
|
|
"""
|
|
The background color of the widget.
|
|
|
|
:attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty`
|
|
and defaults to `None`.
|
|
"""
|
|
|
|
label_only = BooleanProperty(False)
|
|
"""
|
|
Tabs with a label only or with an icon and a label.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
:attr:`label_only` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `False`.
|
|
"""
|
|
|
|
allow_stretch = BooleanProperty(True)
|
|
"""
|
|
Whether to stretch tabs to the width of the panel.
|
|
|
|
:attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `True`.
|
|
"""
|
|
|
|
lock_swiping = BooleanProperty(False)
|
|
"""
|
|
If True - disable switching tabs by swipe.
|
|
|
|
:attr:`lock_swiping` 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`.
|
|
"""
|
|
|
|
indicator_anim = BooleanProperty(True)
|
|
"""
|
|
Tab indicator animation. If you want use animation set it to ``True``.
|
|
|
|
.. versionchanged:: 2.0.0
|
|
|
|
Rename from `tab_indicator_anim` to `indicator_anim` attribute.
|
|
|
|
:attr:`indicator_anim` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `True`.
|
|
"""
|
|
|
|
indicator_radius = VariableListProperty([dp(2), dp(2), 0, 0], lenght=4)
|
|
"""
|
|
Radius of the tab indicator.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
:attr:`indicator_radius` is an :class:`~kivy.properties.VariableListProperty`
|
|
and defaults to `[dp(2), dp(2), 0, 0]`.
|
|
"""
|
|
|
|
indicator_height = NumericProperty("4dp")
|
|
"""
|
|
Height of the tab indicator.
|
|
|
|
.. versionchanged:: 2.0.0
|
|
|
|
Rename from `tab_indicator_height` to `indicator_height` attribute.
|
|
|
|
:attr:`indicator_height` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `'4dp'`.
|
|
"""
|
|
|
|
indicator_duration = NumericProperty(0.5)
|
|
"""
|
|
The duration of the animation of the indicator movement when switching
|
|
tabs.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
:attr:`indicator_duration` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.5`.
|
|
"""
|
|
|
|
indicator_transition = StringProperty("out_expo")
|
|
"""
|
|
The transition name of animation of the indicator movement when switching
|
|
tabs.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
:attr:`indicator_transition` is an :class:`~kivy.properties.StringProperty`
|
|
and defaults to `'out_expo'.
|
|
"""
|
|
|
|
def get_last_scroll_x(self):
|
|
return self.ids.tab_scroll.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`.
|
|
"""
|
|
|
|
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.ids.container.canvas.before.get_group(
|
|
"md-tabs-rounded-rectangle"
|
|
)
|
|
return canvas_instructions[0]
|
|
|
|
indicator = AliasProperty(get_rect_instruction, cache=True)
|
|
"""
|
|
It is the :class:`~kivy.graphics.vertex_instructions.SmoothRoundedRectangle`
|
|
instruction reference of the tab indicator.
|
|
|
|
:attr:`indicator` is an :class:`~kivy.properties.AliasProperty`.
|
|
"""
|
|
|
|
_tabs_carousel = ObjectProperty() # MDTabsCarousel object
|
|
_current_tab = None # MDTabsItem object
|
|
_current_related_content = None # Carousel slide (related content) object
|
|
_do_releasing = True
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.register_event_type("on_tab_switch")
|
|
self.register_event_type("on_slide_progress")
|
|
Clock.schedule_once(self._check_panel_height)
|
|
Clock.schedule_once(self._set_slides_attributes)
|
|
|
|
def add_widget(self, widget, *args, **kwargs):
|
|
if isinstance(widget, MDTabsCarousel):
|
|
self._tabs_carousel = widget
|
|
widget._tabs = self
|
|
widget.bind(
|
|
_offset=self.android_animation, index=self.on_carousel_index
|
|
)
|
|
return super().add_widget(widget)
|
|
elif isinstance(widget, MDTabsItem) or (
|
|
isinstance(self, MDTabsSecondary)
|
|
and isinstance(widget, MDTabsItemSecondary)
|
|
):
|
|
widget._tabs = self
|
|
widget.bind(on_release=self.set_active_item)
|
|
self.ids.container.add_widget(widget)
|
|
else:
|
|
return super().add_widget(widget)
|
|
|
|
def do_autoscroll_tabs(self, instance: MDTabsItem, value: float) -> None:
|
|
"""
|
|
Automatically scrolls the list of tabs when swiping the carousel
|
|
slide (related content).
|
|
|
|
.. versionchanged:: 2.0.0
|
|
|
|
Rename from `tab_bar_autoscroll` to `do_autoscroll_tabs` method.
|
|
"""
|
|
|
|
bound_left = self.center_x - self.x
|
|
bound_right = self.ids.container.width - bound_left
|
|
dt = instance.center_x - bound_left
|
|
sx, sy = self.ids.tab_scroll.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) * value # distance to run)
|
|
|
|
if not dst:
|
|
return
|
|
if scroll_is_late and instance.center_x > bound_left:
|
|
x = lsx + dst
|
|
elif not scroll_is_late and instance.center_x < bound_right:
|
|
x = lsx - dst
|
|
else:
|
|
return
|
|
|
|
x = boundary(x, 0.0, 1.0)
|
|
self.ids.tab_scroll.goto(x, None)
|
|
|
|
def android_animation(
|
|
self, instance: MDTabsCarousel, offset: float
|
|
) -> None:
|
|
"""Fired when swiping a carousel slide (related content)."""
|
|
|
|
self.dispatch("on_slide_progress", instance, offset)
|
|
|
|
# Try to reproduce the android animation effect.
|
|
if offset != 0 and abs(offset) < instance.width:
|
|
forward = offset < 0
|
|
offset = abs(offset)
|
|
step = offset / float(instance.width)
|
|
|
|
skip_slide = (
|
|
instance.slides[instance._skip_slide]
|
|
if instance._skip_slide is not None
|
|
else None
|
|
)
|
|
next_slide = (
|
|
instance.next_slide if forward else instance.previous_slide
|
|
)
|
|
self.target = skip_slide if skip_slide else next_slide
|
|
|
|
if not self.target:
|
|
return
|
|
|
|
a = instance.current_slide.tab_item
|
|
b = self.target.tab_item
|
|
self.do_autoscroll_tabs(b, step)
|
|
item_text_object = self._get_tab_item_text_icon_object()
|
|
|
|
if item_text_object:
|
|
if self.__class__.__name__ == "MDTabsSecondary":
|
|
tab_text_width = a.width
|
|
else:
|
|
tab_text_width = item_text_object.texture_size[0]
|
|
|
|
if self.indicator_anim is False:
|
|
return
|
|
|
|
gap_x = abs(a.x - b.x)
|
|
if forward:
|
|
x_step = (
|
|
a.x
|
|
+ (a.width / 2 - tab_text_width / 2)
|
|
+ dp(4)
|
|
+ (gap_x * step)
|
|
)
|
|
else:
|
|
x_step = (
|
|
a.x
|
|
+ (a.width / 2 - tab_text_width / 2)
|
|
+ dp(4)
|
|
- gap_x * step
|
|
)
|
|
|
|
w_step = tab_text_width - (
|
|
dp(8) if self.__class__.__name__ == "MDTabsPrimary" else 0
|
|
)
|
|
self.update_indicator(x_step, w_step)
|
|
|
|
def update_indicator(
|
|
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
|
) -> None:
|
|
"""Update position and size of the indicator."""
|
|
|
|
def update_indicator(*args):
|
|
indicator_pos = (0, 0)
|
|
indicator_size = (0, 0)
|
|
|
|
if self.__class__.__name__ == "MDTabsPrimary":
|
|
item_text_object = self._get_tab_item_text_icon_object()
|
|
|
|
if item_text_object:
|
|
tab_text_width = item_text_object.texture_size[0]
|
|
indicator_pos = (
|
|
instance.x
|
|
+ (instance.width / 2 - tab_text_width / 2)
|
|
+ dp(4),
|
|
self.indicator.pos[1]
|
|
if not self._tabs_carousel
|
|
else self._tabs_carousel.height,
|
|
)
|
|
indicator_size = (
|
|
tab_text_width - dp(8),
|
|
self.indicator_height,
|
|
)
|
|
elif self.__class__.__name__ == "MDTabsSecondary":
|
|
indicator_pos = (instance.x, self.indicator.pos[1])
|
|
indicator_size = (instance.width, self.indicator_height)
|
|
|
|
Animation(
|
|
pos=indicator_pos,
|
|
size=indicator_size,
|
|
d=0 if not self.indicator_anim else self.indicator_duration,
|
|
t=self.indicator_transition,
|
|
).start(self.indicator)
|
|
|
|
if not instance:
|
|
self.indicator.pos = (x, self.indicator.pos[1])
|
|
self.indicator.size = (w, self.indicator_height)
|
|
else:
|
|
Clock.schedule_once(update_indicator)
|
|
|
|
def switch_tab(
|
|
self, instance: MDTabsItem = None, text: str = "", icon: str = ""
|
|
) -> None:
|
|
"""Switches tabs by tab object/tab text/tab icon name."""
|
|
|
|
Clock.schedule_once(
|
|
lambda x: self._switch_tab(instance, text, icon), 0.8
|
|
)
|
|
|
|
def set_active_item(self, item: MDTabsItem) -> None:
|
|
"""Sets the active tab item."""
|
|
|
|
for widget in self.ids.container.children:
|
|
if item is widget:
|
|
# Trying to switch an already active tab.
|
|
if widget.active and item.active:
|
|
break
|
|
|
|
widget.active = not widget.active
|
|
|
|
for widget_item in item.children:
|
|
if isinstance(widget_item, MDTabsItemText):
|
|
widget_item._active = widget.active
|
|
Animation(
|
|
text_color=self.theme_cls.primaryColor
|
|
if widget.active
|
|
else self.theme_cls.onSurfaceVariantColor,
|
|
d=0.2,
|
|
).start(widget_item)
|
|
if isinstance(widget_item, MDTabsItemIcon):
|
|
widget_item._active = widget.active
|
|
Animation(
|
|
icon_color=self.theme_cls.primaryColor
|
|
if widget.active
|
|
else self.theme_cls.onSurfaceVariantColor,
|
|
d=0.2,
|
|
).start(widget_item)
|
|
else:
|
|
widget.active = False
|
|
for widget_item in widget.children:
|
|
widget_item._active = widget.active
|
|
if isinstance(widget_item, MDTabsItemText):
|
|
Animation(
|
|
text_color=self.theme_cls.onSurfaceVariantColor,
|
|
d=0.2,
|
|
).start(widget_item)
|
|
if isinstance(widget_item, MDTabsItemIcon):
|
|
Animation(
|
|
icon_color=self.theme_cls.onSurfaceVariantColor,
|
|
d=0.2,
|
|
).start(widget_item)
|
|
|
|
def get_tabs_list(self) -> list:
|
|
"""
|
|
Returns a list of :class:`~MDTabsItem` objects.
|
|
|
|
.. versionchanged:: 2.0.0
|
|
|
|
Rename from `get_tab_list` to `get_tabs_list` method.
|
|
"""
|
|
|
|
return self.ids.container.children
|
|
|
|
def get_slides_list(self) -> list:
|
|
"""
|
|
Returns a list of user tab objects.
|
|
|
|
.. versionchanged:: 2.0.0
|
|
|
|
Rename from `get_slides` to `get_slides_list` method.
|
|
"""
|
|
|
|
if self._tabs_carousel:
|
|
return self._tabs_carousel.slides
|
|
|
|
def get_current_tab(self) -> MDTabsItem:
|
|
"""
|
|
Returns current tab object.
|
|
|
|
.. versionadded:: 1.0.0
|
|
"""
|
|
|
|
return self._current_tab
|
|
|
|
def get_current_related_content(self) -> Widget:
|
|
"""
|
|
Returns the carousel slide object (related content).
|
|
|
|
.. versionadded:: 2.0.0
|
|
"""
|
|
|
|
return self._current_related_content
|
|
|
|
def on_tab_switch(self, *args) -> None:
|
|
"""This event is launched every time the current tab is changed."""
|
|
|
|
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: MDTabsCarousel, value: int) -> None:
|
|
"""
|
|
Fired when the Tab index have changed.
|
|
This event is deployed by the builtin carousel of the class.
|
|
"""
|
|
|
|
# When the index of the carousel change, update tab indicator,
|
|
# select the current tab and reset threshold data.
|
|
if instance.current_slide and hasattr(
|
|
instance.current_slide, "tab_item"
|
|
):
|
|
Clock.schedule_once(
|
|
lambda x: instance.current_slide.tab_item.dispatch("on_release")
|
|
)
|
|
|
|
def on_size(self, instance, size) -> None:
|
|
"""Fired when the application screen size changes."""
|
|
|
|
width, height = size
|
|
number_tabs = len(self.ids.container.children)
|
|
|
|
if self.allow_stretch:
|
|
for tab in self.ids.container.children:
|
|
tab.width = width / number_tabs
|
|
|
|
if self._tabs_carousel:
|
|
Clock.schedule_once(
|
|
lambda x: self._tabs_carousel.current_slide.tab_item.dispatch(
|
|
"on_release"
|
|
)
|
|
)
|
|
|
|
def _switch_tab(
|
|
self, instance: MDTabsItem = None, text: str = "", icon: str = ""
|
|
):
|
|
def get_match(widget_to_compare, widget_to_compare_with, value, attr):
|
|
if isinstance(widget_to_compare, widget_to_compare_with):
|
|
if getattr(widget_to_compare, attr) == value:
|
|
return True
|
|
|
|
def switch_by(by_attr, attr):
|
|
for tab_item in self.ids.container.children:
|
|
for child in tab_item.children:
|
|
if isinstance(child, MDTabsItemSecondaryContainer):
|
|
for w in child.children:
|
|
if get_match(
|
|
w,
|
|
MDTabsItemText
|
|
if by_attr == "text"
|
|
else MDTabsItemIcon,
|
|
attr,
|
|
by_attr,
|
|
):
|
|
tab_item.dispatch("on_release")
|
|
break
|
|
else:
|
|
if get_match(
|
|
child,
|
|
MDTabsItemText
|
|
if by_attr == "text"
|
|
else MDTabsItemIcon,
|
|
attr,
|
|
by_attr,
|
|
):
|
|
tab_item.dispatch("on_release")
|
|
break
|
|
|
|
if instance and isinstance(instance, MDTabsItem):
|
|
instance.dispatch("on_release")
|
|
elif text:
|
|
switch_by("text", text)
|
|
elif icon:
|
|
switch_by("icon", icon)
|
|
|
|
def _set_slides_attributes(self, *args):
|
|
if self._tabs_carousel:
|
|
tabs_item_list = self.ids.container.children.copy()
|
|
tabs_item_list.reverse()
|
|
|
|
for i, tab_item in enumerate(tabs_item_list):
|
|
setattr(tab_item, "_tab_content", self._tabs_carousel.slides[i])
|
|
setattr(self._tabs_carousel.slides[i], "tab_item", tab_item)
|
|
|
|
def _get_tab_item_text_icon_object(
|
|
self, get_type="text"
|
|
) -> MDTabsItemText | MDTabsItemIcon | None:
|
|
item_text_object = None
|
|
|
|
for tab_item in self.ids.container.children:
|
|
if tab_item.active:
|
|
for child in tab_item.children:
|
|
if isinstance(child, MDTabsItemSecondaryContainer):
|
|
for w in child.children:
|
|
if isinstance(
|
|
w,
|
|
MDTabsItemText
|
|
if get_type == "text"
|
|
else MDTabsItemIcon,
|
|
):
|
|
item_text_object = w
|
|
break
|
|
else:
|
|
if isinstance(
|
|
child,
|
|
MDTabsItemText
|
|
if get_type == "text"
|
|
else MDTabsItemIcon,
|
|
):
|
|
item_text_object = child
|
|
break
|
|
return item_text_object
|
|
|
|
def _check_panel_height(self, *args):
|
|
if self.label_only:
|
|
self.ids.tab_scroll.height = dp(48)
|
|
else:
|
|
self.ids.tab_scroll.height = dp(64)
|
|
|
|
|
|
###############################################################################
|
|
#
|
|
# SECONDARY CLASSES
|
|
#
|
|
###############################################################################
|
|
|
|
|
|
class MDTabsItemSecondaryContainer(BoxLayout):
|
|
"""
|
|
Implements a container for placing widgets for the
|
|
:class:`~MDTabsItemSecondary` class.
|
|
|
|
For more information, see in the
|
|
:class:`~kivy.uix.boxlayout.BoxLayout` class documentation.
|
|
"""
|
|
|
|
|
|
class MDTabsItemSecondary(MDTabsItemBase, AnchorLayout):
|
|
"""
|
|
Implements a item with an icon and text for :class:`~MDTabsSecondary`
|
|
class.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
For more information, see in the
|
|
:class:`~MDTabsItemBase` and
|
|
:class:`~kivy.uix.anchorlayout.AnchorLayout`
|
|
classes documentation.
|
|
"""
|
|
|
|
def add_widget(self, widget, *args, **kwargs):
|
|
if isinstance(widget, (MDTabsItemText, MDTabsItemIcon, MDTabsBadge)):
|
|
Clock.schedule_once(
|
|
lambda x: self.ids.box_container.add_widget(widget)
|
|
)
|
|
else:
|
|
return super().add_widget(widget)
|
|
|
|
|
|
class MDTabsSecondary(MDTabsPrimary):
|
|
"""
|
|
Tabs secondary class.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
For more information, see in the
|
|
:class:`~MDTabsPrimary` class documentation.
|
|
"""
|
|
|
|
indicator_radius = VariableListProperty(0, lenght=4)
|
|
"""
|
|
Radius of the tab indicator.
|
|
|
|
:attr:`indicator_radius` is an :class:`~kivy.properties.VariableListProperty`
|
|
and defaults to `[0, 0, 0, 0]`.
|
|
"""
|
|
|
|
indicator_height = NumericProperty("2dp")
|
|
"""
|
|
Height of the tab indicator.
|
|
|
|
:attr:`indicator_height` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `'2dp'`.
|
|
"""
|
|
|
|
def _check_panel_height(self, *args):
|
|
self.ids.tab_scroll.height = dp(48)
|