1148 lines
33 KiB
Python
Executable file
1148 lines
33 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(FloatLayout, MDTabsBase):
|
|
'''Class implementing content for a tab.'''
|
|
|
|
.. code-block:: kv
|
|
|
|
<Tab>:
|
|
|
|
MDLabel:
|
|
text: "Content"
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
|
|
Tabs must be placed in the :class:`~MDTabs` container:
|
|
|
|
.. code-block:: kv
|
|
|
|
Root:
|
|
|
|
MDTabs:
|
|
|
|
Tab:
|
|
text: "Tab 1"
|
|
|
|
Tab:
|
|
text: "Tab 1"
|
|
|
|
...
|
|
|
|
Example with tab icon
|
|
---------------------
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
from kivy.uix.floatlayout import FloatLayout
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import MDTabsBase
|
|
from kivymd.icon_definitions import md_icons
|
|
|
|
KV = '''
|
|
BoxLayout:
|
|
orientation: "vertical"
|
|
|
|
MDToolbar:
|
|
title: "Example Tabs"
|
|
|
|
MDTabs:
|
|
id: tabs
|
|
on_tab_switch: app.on_tab_switch(*args)
|
|
|
|
|
|
<Tab>:
|
|
|
|
MDIconButton:
|
|
id: icon
|
|
icon: app.icons[0]
|
|
user_font_size: "48sp"
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
'''
|
|
|
|
|
|
class Tab(FloatLayout, 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=name_tab))
|
|
|
|
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 = [k for k, v in md_icons.items() if v == tab_text]
|
|
instance_tab.ids.icon.icon = count_icon[0]
|
|
|
|
|
|
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,
|
|
then the name of the tab will be plain text, if found, the tab will look
|
|
like the corresponding icon.
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
from kivy.uix.floatlayout import FloatLayout
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import MDTabsBase
|
|
|
|
KV = '''
|
|
BoxLayout:
|
|
orientation: "vertical"
|
|
|
|
MDToolbar:
|
|
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(FloatLayout, 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(text=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
|
|
------------------------------
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang import Builder
|
|
from kivy.uix.floatlayout import FloatLayout
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import MDTabsBase
|
|
from kivymd.font_definitions import fonts
|
|
from kivymd.icon_definitions import md_icons
|
|
|
|
KV = '''
|
|
BoxLayout:
|
|
orientation: "vertical"
|
|
|
|
MDToolbar:
|
|
title: "Example Tabs"
|
|
|
|
MDTabs:
|
|
id: tabs
|
|
'''
|
|
|
|
|
|
class Tab(FloatLayout, 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(
|
|
text=f"[size=20][font={fonts[-1]['fn_regular']}]{md_icons[name_tab]}[/size][/font] {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 = '''
|
|
BoxLayout:
|
|
orientation: "vertical"
|
|
|
|
MDToolbar:
|
|
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(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()[0]
|
|
)
|
|
|
|
|
|
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 kivy.uix.floatlayout import FloatLayout
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.font_definitions import fonts
|
|
from kivymd.uix.tab import MDTabsBase
|
|
from kivymd.icon_definitions import md_icons
|
|
|
|
KV = '''
|
|
BoxLayout:
|
|
orientation: "vertical"
|
|
|
|
MDToolbar:
|
|
title: "Example Tabs"
|
|
|
|
MDTabs:
|
|
id: tabs
|
|
on_ref_press: app.on_ref_press(*args)
|
|
|
|
|
|
<Tab>:
|
|
|
|
MDIconButton:
|
|
id: icon
|
|
icon: app.icons[0]
|
|
user_font_size: "48sp"
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
'''
|
|
|
|
|
|
class Tab(FloatLayout, 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: <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.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 kivy.uix.floatlayout import FloatLayout
|
|
|
|
from kivymd.app import MDApp
|
|
from kivymd.uix.tab import MDTabsBase
|
|
from kivymd.icon_definitions import md_icons
|
|
|
|
KV = '''
|
|
BoxLayout:
|
|
orientation: "vertical"
|
|
|
|
MDToolbar:
|
|
title: "Example Tabs"
|
|
|
|
MDTabs:
|
|
id: tabs
|
|
|
|
|
|
<Tab>:
|
|
|
|
MDIconButton:
|
|
id: icon
|
|
icon: "arrow-right"
|
|
user_font_size: "48sp"
|
|
pos_hint: {"center_x": .5, "center_y": .5}
|
|
on_release: app.switch_tab()
|
|
'''
|
|
|
|
|
|
class Tab(FloatLayout, MDTabsBase):
|
|
'''Class implementing content for a tab.'''
|
|
|
|
|
|
class Example(MDApp):
|
|
icons = list(md_icons.keys())[15:30]
|
|
|
|
def build(self):
|
|
self.iter_list = 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(text=name_tab))
|
|
|
|
def switch_tab(self):
|
|
'''Switching the tab by name.'''
|
|
|
|
try:
|
|
self.root.ids.tabs.switch_tab(next(self.iter_list))
|
|
except StopIteration:
|
|
pass
|
|
|
|
|
|
Example().run()
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switching-tab-by-name.gif
|
|
:align: center
|
|
"""
|
|
|
|
__all__ = ("MDTabs", "MDTabsBase")
|
|
|
|
from kivy.clock import Clock
|
|
from kivy.graphics import Rectangle
|
|
from kivy.lang import Builder
|
|
from kivy.properties import (
|
|
AliasProperty,
|
|
BooleanProperty,
|
|
BoundedNumericProperty,
|
|
ListProperty,
|
|
NumericProperty,
|
|
ObjectProperty,
|
|
StringProperty,
|
|
)
|
|
from kivy.uix.anchorlayout import AnchorLayout
|
|
from kivy.uix.behaviors import ToggleButtonBehavior
|
|
from kivy.uix.label import Label
|
|
from kivy.uix.scrollview import ScrollView
|
|
from kivy.uix.widget import Widget
|
|
from kivy.utils import boundary
|
|
|
|
from kivymd import fonts_path
|
|
from kivymd.icon_definitions import md_icons
|
|
from kivymd.theming import ThemableBehavior
|
|
from kivymd.uix.behaviors import (
|
|
RectangularElevationBehavior,
|
|
SpecificBackgroundColorBehavior,
|
|
)
|
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|
from kivymd.uix.carousel import MDCarousel
|
|
|
|
Builder.load_string(
|
|
"""
|
|
#:import DampedScrollEffect kivy.effects.dampedscroll.DampedScrollEffect
|
|
|
|
|
|
<MDTabsLabel>
|
|
size_hint: None, 1
|
|
halign: 'center'
|
|
padding: '12dp', 0
|
|
group: 'tabs'
|
|
font: root.font_name
|
|
allow_no_selection: False
|
|
markup: True
|
|
on_ref_press:
|
|
self.tab_bar.parent.dispatch(\
|
|
"on_ref_press",
|
|
self, \
|
|
self.tab, \
|
|
self.tab_bar, \
|
|
self.tab_bar.parent.carousel)
|
|
text_color_normal:
|
|
(\
|
|
(0, 0, 0, .5) \
|
|
if app.theme_cls.theme_style == 'Dark' and not self.text_color_normal \
|
|
else (1, 1, 1, .6) \
|
|
if app.theme_cls.theme_style == 'Light' and not self.text_color_normal \
|
|
else self.text_color_normal \
|
|
)
|
|
text_color_active:
|
|
(\
|
|
(0, 0, 0, .75) \
|
|
if app.theme_cls.theme_style == 'Dark' and not self.text_color_active \
|
|
else (1, 1, 1, 1) \
|
|
if app.theme_cls.theme_style == 'Light' and not self.text_color_normal \
|
|
else self.text_color_active
|
|
)
|
|
color:
|
|
self.text_color_active if self.state == 'down' \
|
|
else self.text_color_normal
|
|
on_x: self._trigger_update_tab_indicator()
|
|
on_width: self._trigger_update_tab_indicator()
|
|
|
|
|
|
<MDTabsScrollView>
|
|
size_hint: 1, 1
|
|
do_scroll_y: False
|
|
bar_color: 0, 0, 0, 0
|
|
bar_inactive_color: 0, 0, 0, 0
|
|
bar_width: 0
|
|
effect_cls: DampedScrollEffect
|
|
|
|
|
|
<MDTabs>
|
|
carousel: carousel
|
|
tab_bar: tab_bar
|
|
anchor_y: 'top'
|
|
background_palette: "Primary"
|
|
text_color_normal: self.specific_secondary_text_color
|
|
text_color_active: self.specific_text_color
|
|
|
|
MDTabsMain:
|
|
padding: 0, tab_bar.height, 0, 0
|
|
|
|
MDTabsCarousel:
|
|
id: carousel
|
|
lock_swiping: root.lock_swiping
|
|
ignore_perpendicular_swipes: True
|
|
anim_move_duration: root.anim_duration
|
|
on_index: root.on_carousel_index(*args)
|
|
on__offset: tab_bar.android_animation(*args)
|
|
on_slides: self.index = root.default_tab
|
|
on_slides: root.on_carousel_index(self, 0)
|
|
|
|
MDTabsBar:
|
|
id: tab_bar
|
|
carousel: carousel
|
|
scrollview: scrollview
|
|
layout: layout
|
|
size_hint: 1, None
|
|
elevation: root.elevation
|
|
height: root.tab_bar_height
|
|
md_bg_color: self.theme_cls.primary_color if not root.background_color else root.background_color
|
|
|
|
MDTabsScrollView:
|
|
id: scrollview
|
|
on_width: tab_bar._trigger_update_tab_bar()
|
|
|
|
MDGridLayout:
|
|
id: layout
|
|
rows: 1
|
|
size_hint_y: 1
|
|
adaptive_width: True
|
|
on_width: tab_bar._trigger_update_tab_bar()
|
|
|
|
canvas.after:
|
|
Color:
|
|
rgba: root.theme_cls.accent_color if not root.color_indicator else root.color_indicator
|
|
Rectangle:
|
|
pos: self.pos
|
|
size: 0, root.tab_indicator_height
|
|
"""
|
|
)
|
|
|
|
|
|
class MDTabsException(Exception):
|
|
pass
|
|
|
|
|
|
class MDTabsLabel(ToggleButtonBehavior, Label):
|
|
"""This class it represent the label of each tab."""
|
|
|
|
text_color_normal = ListProperty((1, 1, 1, 1))
|
|
text_color_active = ListProperty((1, 1, 1, 1))
|
|
tab = ObjectProperty()
|
|
tab_bar = ObjectProperty()
|
|
font_name = StringProperty("Roboto")
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.min_space = 0
|
|
|
|
def on_release(self):
|
|
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, widget, texture):
|
|
# just save the minimum width of the label based of the content
|
|
if texture:
|
|
self.width = texture.width
|
|
self.min_space = self.width
|
|
|
|
def _trigger_update_tab_indicator(self):
|
|
# update the position and size of the indicator
|
|
# when the label changes size or position
|
|
if self.state == "down":
|
|
self.tab_bar.update_indicator(self.x, self.width)
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
text = StringProperty()
|
|
"""
|
|
It will be the label text of the tab.
|
|
|
|
:attr:`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 __init__(self, **kwargs):
|
|
self.tab_label = MDTabsLabel(tab=self)
|
|
super().__init__(**kwargs)
|
|
|
|
def on_text(self, widget, text):
|
|
# Set the icon
|
|
if text in md_icons:
|
|
self.tab_label.font_name = (
|
|
fonts_path + "materialdesignicons-webfont.ttf"
|
|
)
|
|
self.tab_label.text = md_icons[self.text]
|
|
self.tab_label.font_size = "24sp"
|
|
# Set the label text
|
|
else:
|
|
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):
|
|
# lock a swiping
|
|
if self.lock_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, scroll_y):
|
|
"""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, RectangularElevationBehavior, MDBoxLayout):
|
|
"""
|
|
This class is just a boxlayout that contains the scroll view for tabs.
|
|
He is also responsible for resizing the tab shortcut when necessary.
|
|
"""
|
|
|
|
target = ObjectProperty(None, allownone=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:`target` is an :class:`~kivy.properties.ObjectProperty`
|
|
and default to `None`.
|
|
"""
|
|
|
|
def get_rect_instruction(self):
|
|
for i in self.layout.canvas.after.children:
|
|
if isinstance(i, Rectangle):
|
|
return i
|
|
|
|
indicator = AliasProperty(get_rect_instruction, cache=True)
|
|
"""
|
|
Is the Rectangle 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):
|
|
self._trigger_update_tab_bar = Clock.schedule_once(
|
|
self._update_tab_bar, 0
|
|
)
|
|
super().__init__(**kwargs)
|
|
|
|
def _update_tab_bar(self, *args):
|
|
if self.parent.allow_stretch:
|
|
# update width of the labels when it is needed
|
|
width, tabs = self.scrollview.width, self.layout.children
|
|
tabs_widths = [t.min_space for t in tabs if t.min_space]
|
|
tabs_space = float(sum(tabs_widths))
|
|
|
|
if not tabs_space:
|
|
return
|
|
|
|
ratio = width / tabs_space
|
|
use_ratio = True in (width / len(tabs) < w for w in tabs_widths)
|
|
|
|
for t in tabs:
|
|
t.width = (
|
|
t.min_space
|
|
if tabs_space > width
|
|
else t.min_space * ratio
|
|
if use_ratio is True
|
|
else width / len(tabs)
|
|
)
|
|
|
|
def update_indicator(self, x, w):
|
|
# update position and size of the indicator
|
|
self.indicator.pos = (x, 0)
|
|
self.indicator.size = (w, self.indicator.size[1])
|
|
|
|
def tab_bar_autoscroll(self, target, step):
|
|
# automatic scroll animation of the tab bar.
|
|
bound_left = self.center_x
|
|
bound_right = self.layout.width - bound_left
|
|
dt = target.center_x - bound_left
|
|
sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0)
|
|
|
|
# last scroll x of the tab bar
|
|
lsx = self.last_scroll_x
|
|
# determine scroll direction
|
|
scroll_is_late = lsx < sx
|
|
# distance to run
|
|
dst = abs(lsx - sx) * step
|
|
|
|
if not dst:
|
|
return
|
|
|
|
if scroll_is_late and target.center_x > bound_left:
|
|
x = lsx + dst
|
|
|
|
elif not scroll_is_late and target.center_x < bound_right:
|
|
x = lsx - dst
|
|
|
|
x = boundary(x, 0.0, 1.0)
|
|
self.scrollview.goto(x, None)
|
|
|
|
def android_animation(self, carousel, offset):
|
|
# try to reproduce the android animation effect.
|
|
if offset != 0 and abs(offset) < carousel.width:
|
|
forward = offset < 0
|
|
offset = abs(offset)
|
|
step = offset / float(carousel.width)
|
|
distance = abs(offset - carousel.width)
|
|
threshold = self.parent.anim_threshold
|
|
breakpoint = carousel.width - (carousel.width * threshold)
|
|
traveled = distance / breakpoint if breakpoint else 0
|
|
break_step = 1.0 - traveled
|
|
indicator_animation = self.parent.tab_indicator_anim
|
|
|
|
skip_slide = (
|
|
carousel.slides[carousel._skip_slide]
|
|
if carousel._skip_slide is not None
|
|
else None
|
|
)
|
|
next_slide = (
|
|
carousel.next_slide if forward else carousel.previous_slide
|
|
)
|
|
self.target = skip_slide if skip_slide else next_slide
|
|
|
|
if not self.target:
|
|
return
|
|
|
|
a = carousel.current_slide.tab_label
|
|
b = self.target.tab_label
|
|
self.tab_bar_autoscroll(b, step)
|
|
|
|
if not indicator_animation:
|
|
return
|
|
|
|
if step <= threshold:
|
|
if forward:
|
|
gap_w = abs((a.x + a.width) - (b.x + b.width))
|
|
w_step = a.width + (gap_w * step)
|
|
x_step = a.x
|
|
else:
|
|
gap = abs((a.x - b.x))
|
|
x_step = a.x - gap * step
|
|
w_step = a.width + gap * step
|
|
else:
|
|
if forward:
|
|
x_step = a.x + abs((a.x - b.x)) * break_step
|
|
gap_w = abs((a.x + a.width) - (b.x + b.width))
|
|
ind_width = a.width + gap_w * threshold
|
|
gap_w = ind_width - b.width
|
|
w_step = ind_width - (gap_w * break_step)
|
|
else:
|
|
x_step = a.x - abs((a.x - b.x)) * threshold
|
|
x_step = x_step - abs(x_step - b.x) * break_step
|
|
ind_width = (
|
|
(a.x + a.width) - x_step if threshold else a.width
|
|
)
|
|
gap_w = ind_width - b.width
|
|
w_step = ind_width - (gap_w * break_step)
|
|
w_step = (
|
|
w_step
|
|
if w_step + x_step <= a.x + a.width
|
|
else ind_width
|
|
)
|
|
self.update_indicator(x_step, w_step)
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
default_tab = NumericProperty(0)
|
|
"""
|
|
Index of the default tab.
|
|
|
|
:attr:`default_tab` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0`.
|
|
"""
|
|
|
|
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_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'`.
|
|
"""
|
|
|
|
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 False - tabs will not stretch to full screen.
|
|
|
|
:attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `True`.
|
|
"""
|
|
|
|
background_color = ListProperty()
|
|
"""
|
|
Background color of tabs in ``rgba`` format.
|
|
|
|
:attr:`background_color` is an :class:`~kivy.properties.ListProperty`
|
|
and defaults to `[]`.
|
|
"""
|
|
|
|
text_color_normal = ListProperty((1, 1, 1, 1))
|
|
"""
|
|
Text color of the label when it is not selected.
|
|
|
|
:attr:`text_color_normal` is an :class:`~kivy.properties.ListProperty`
|
|
and defaults to `(1, 1, 1, 1)`.
|
|
"""
|
|
|
|
text_color_active = ListProperty((1, 1, 1, 1))
|
|
"""
|
|
Text color of the label when it is selected.
|
|
|
|
:attr:`text_color_active` is an :class:`~kivy.properties.ListProperty`
|
|
and defaults to `(1, 1, 1, 1)`.
|
|
"""
|
|
|
|
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`.
|
|
"""
|
|
|
|
color_indicator = ListProperty()
|
|
"""
|
|
Color indicator in ``rgba`` format.
|
|
|
|
:attr:`color_indicator` is an :class:`~kivy.properties.ListProperty`
|
|
and defaults to `[]`.
|
|
"""
|
|
|
|
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'`.
|
|
"""
|
|
|
|
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)
|
|
|
|
def switch_tab(self, name_tab):
|
|
"""Switching the tab by name."""
|
|
|
|
for instance_tab in self.tab_bar.parent.carousel.slides:
|
|
if instance_tab.text == name_tab:
|
|
self.tab_bar.parent.carousel.load_slide(instance_tab)
|
|
break
|
|
|
|
def get_tab_list(self):
|
|
"""Returns a list of tab objects."""
|
|
|
|
return self.tab_bar.layout.children
|
|
|
|
def add_widget(self, widget, index=0, canvas=None):
|
|
# You can add only subclass of MDTabsBase.
|
|
if len(self.children) >= 2:
|
|
try:
|
|
widget.tab_label.group = str(self)
|
|
widget.tab_label.tab_bar = self.tab_bar
|
|
widget.tab_label.text_color_normal = self.text_color_normal
|
|
widget.tab_label.text_color_active = self.text_color_active
|
|
self.bind(
|
|
text_color_normal=widget.tab_label.setter(
|
|
"text_color_normal"
|
|
)
|
|
)
|
|
self.bind(
|
|
text_color_active=widget.tab_label.setter(
|
|
"text_color_active"
|
|
)
|
|
)
|
|
self.bind(font_name=widget.tab_label.setter("font_name"))
|
|
self.tab_bar.layout.add_widget(widget.tab_label)
|
|
self.carousel.add_widget(widget)
|
|
return
|
|
except AttributeError:
|
|
pass
|
|
return super().add_widget(widget)
|
|
|
|
def remove_widget(self, widget):
|
|
# You can remove only subclass of MDTabsLabel.
|
|
if not issubclass(widget.__class__, MDTabsLabel):
|
|
raise MDTabsException(
|
|
"MDTabs can remove only subclass of MDTabsLabel"
|
|
)
|
|
# The last tab is not deleted.
|
|
if len(self.tab_bar.layout.children) == 1:
|
|
return
|
|
self.tab_bar.layout.remove_widget(widget)
|
|
for tab in self.carousel.slides:
|
|
if tab.text == widget.text:
|
|
self.carousel.remove_widget(tab)
|
|
break
|
|
|
|
def on_slide_progress(self, *args):
|
|
"""Called while the slide is scrolling."""
|
|
|
|
def on_carousel_index(self, carousel, index):
|
|
"""Called when the carousel index changes."""
|
|
|
|
# when the index of the carousel change, update
|
|
# tab indicator, select the current tab and reset threshold data.
|
|
if carousel.current_slide:
|
|
current_tab_label = carousel.current_slide.tab_label
|
|
if current_tab_label.state == "normal":
|
|
current_tab_label._do_press()
|
|
current_tab_label.dispatch("on_release")
|
|
self.tab_bar.update_indicator(
|
|
current_tab_label.x, current_tab_label.width
|
|
)
|
|
|
|
def on_ref_press(self, *args):
|
|
"""The method will be called when the ``on_ref_press`` event
|
|
occurs when you, for example, use markup text for tabs."""
|
|
|
|
def on_tab_switch(self, *args):
|
|
"""Called when switching tabs."""
|
|
|
|
def _carousel_bind(self, i):
|
|
self.carousel.bind(on_slide_progress=self._on_slide_progress)
|
|
|
|
def _on_slide_progress(self, *args):
|
|
self.dispatch("on_slide_progress", args)
|