first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -0,0 +1,92 @@
__all__ = ("MDAdaptiveWidget",)
from kivy.properties import BooleanProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.screenmanager import Screen
from kivymd.uix.behaviors import SpecificBackgroundColorBehavior
class MDAdaptiveWidget(SpecificBackgroundColorBehavior):
adaptive_height = BooleanProperty(False)
"""
If `True`, the following properties will be applied to the widget:
.. code-block:: kv
size_hint_y: None
height: self.minimum_height
:attr:`adaptive_height` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
adaptive_width = BooleanProperty(False)
"""
If `True`, the following properties will be applied to the widget:
.. code-block:: kv
size_hint_x: None
width: self.minimum_width
:attr:`adaptive_width` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
adaptive_size = BooleanProperty(False)
"""
If `True`, the following properties will be applied to the widget:
.. code-block:: kv
size_hint: None, None
size: self.minimum_size
:attr:`adaptive_size` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
def on_adaptive_height(self, md_widget, value: bool) -> None:
self.size_hint_y = None
if issubclass(self.__class__, Label):
self.bind(
texture_size=lambda *x: self.setter("height")(
self, self.texture_size[1]
)
)
else:
if not isinstance(self, (FloatLayout, Screen)):
self.bind(minimum_height=self.setter("height"))
if not self.children:
self.height = 0
def on_adaptive_width(self, md_widget, value: bool) -> None:
self.size_hint_x = None
if issubclass(self.__class__, Label):
self.bind(
texture_size=lambda *x: self.setter("width")(
self, self.texture_size[0]
)
)
else:
if not isinstance(self, (FloatLayout, Screen)):
self.bind(minimum_width=self.setter("width"))
if not self.children:
self.width = 0
def on_adaptive_size(self, md_widget, value: bool) -> None:
self.size_hint = (None, None)
if issubclass(self.__class__, Label):
self.text_size = (None, None)
self.bind(
texture_size=lambda *x: self.setter("size")(
self, self.texture_size
)
)
else:
if not isinstance(self, (FloatLayout, Screen)):
self.bind(minimum_size=self.setter("size"))
if not self.children:
self.size = (0, 0)

View file

@ -0,0 +1,47 @@
"""
Components/AnchorLayout
=======================
.. versionadded:: 1.0.0
:class:`~kivy.uix.anchorlayout.AnchorLayout` class equivalent. Simplifies working
with some widget properties. For example:
AnchorLayout
------------
.. code-block:: kv
AnchorLayout:
canvas:
Color:
rgba: app.theme_cls.primary_color
Rectangle:
pos: self.pos
size: self.size
MDAnchorLayout
--------------
.. code-block:: kv
MDAnchorLayout:
md_bg_color: app.theme_cls.primary_color
"""
__all__ = ("MDAnchorLayout",)
from kivy.uix.anchorlayout import AnchorLayout
from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
class MDAnchorLayout(
DeclarativeBehavior, ThemableBehavior, AnchorLayout, MDAdaptiveWidget
):
"""
Anchor layout class. For more information, see in the
:class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation.
"""

View file

@ -0,0 +1 @@
from .backdrop import MDBackdrop # NOQA F401

View file

@ -0,0 +1,50 @@
<MDBackdrop>
md_bg_color:
root.theme_cls.primary_color \
if not root.back_layer_color \
else root.back_layer_color
MDBackdropToolbar:
id: toolbar
type_height: "small"
anchor_title: root.anchor_title
title: root.title
elevation: 0
left_action_items: root.left_action_items
right_action_items: root.right_action_items
pos_hint: {"top": 1}
md_bg_color:
root.theme_cls.primary_color \
if not root.back_layer_color \
else root.back_layer_color
_BackLayer:
id: back_layer
y: -toolbar.height
padding: 0, 0, 0, toolbar.height + dp(10)
_FrontLayer:
id: _front_layer
md_bg_color: 0, 0, 0, 0
orientation: "vertical"
size_hint_y: None
height: root.height - toolbar.height
padding: root.padding
md_bg_color:
root.theme_cls.bg_normal \
if not root.front_layer_color \
else root.front_layer_color
radius:
[root.radius_left, root.radius_right,
0, 0]
OneLineListItem:
id: header_button
text: root.header_text
divider: None
_no_ripple_effect: True
on_press: root.open()
MDBoxLayout:
id: front_layer
padding: 0, 0, 0, "10dp"

View file

@ -0,0 +1,548 @@
"""
Components/Backdrop
===================
.. seealso::
`Material Design spec, Backdrop <https://material.io/components/backdrop>`_
.. rubric:: Skeleton layout for using :class:`~MDBackdrop`:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop.png
:align: center
Usage
-----
.. code-block:: kv
<Root>
MDBackdrop:
MDBackdropBackLayer:
ContentForBackdropBackLayer:
MDBackdropFrontLayer:
ContentForBackdropFrontLayer:
Example
-------
.. tabs::
.. tab:: Declarative KV styles
.. code-block:: python
from kivy.lang import Builder
from kivymd.uix.screen import MDScreen
from kivymd.app import MDApp
# Your layouts.
Builder.load_string(
'''
#:import os os
#:import Window kivy.core.window.Window
#:import IconLeftWidget kivymd.uix.list.IconLeftWidget
#:import images_path kivymd.images_path
<ItemBackdropFrontLayer@TwoLineAvatarListItem>
icon: "android"
IconLeftWidget:
icon: root.icon
<MyBackdropFrontLayer@ItemBackdropFrontLayer>
backdrop: None
text: "Lower the front layer"
secondary_text: " by 50 %"
icon: "transfer-down"
on_press: root.backdrop.open(-Window.height / 2)
pos_hint: {"top": 1}
_no_ripple_effect: True
<MyBackdropBackLayer@Image>
size_hint: .8, .8
source: os.path.join(images_path, "logo", "kivymd-icon-512.png")
pos_hint: {"center_x": .5, "center_y": .6}
'''
)
# Usage example of MDBackdrop.
Builder.load_string(
'''
<ExampleBackdrop>
MDBackdrop:
id: backdrop
left_action_items: [['menu', lambda x: self.open()]]
title: "Example Backdrop"
radius_left: "25dp"
radius_right: "0dp"
header_text: "Menu:"
MDBackdropBackLayer:
MyBackdropBackLayer:
id: backlayer
MDBackdropFrontLayer:
MyBackdropFrontLayer:
backdrop: backdrop
'''
)
class ExampleBackdrop(MDScreen):
pass
class Example(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return ExampleBackdrop()
Example().run()
.. tab:: Declarative python styles
.. code-block:: python
import os
from kivy.core.window import Window
from kivy.uix.image import Image
from kivymd import images_path
from kivymd.uix.backdrop import MDBackdrop
from kivymd.uix.backdrop.backdrop import (
MDBackdropBackLayer, MDBackdropFrontLayer
)
from kivymd.uix.list import TwoLineAvatarListItem, IconLeftWidget
from kivymd.uix.screen import MDScreen
from kivymd.app import MDApp
class Example(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return (
MDScreen(
MDBackdrop(
MDBackdropBackLayer(
Image(
size_hint=(0.8, 0.8),
source=os.path.join(images_path, "logo", "kivymd-icon-512.png"),
pos_hint={"center_x": 0.5, "center_y": 0.6},
)
),
MDBackdropFrontLayer(
TwoLineAvatarListItem(
IconLeftWidget(icon="transfer-down"),
text="Lower the front layer",
secondary_text=" by 50 %",
on_press=self.backdrop_open_by_50_percent,
pos_hint={"top": 1},
_no_ripple_effect=True,
),
),
id="backdrop",
title="Example Backdrop",
radius_left="25dp",
radius_right="0dp",
header_text="Menu:",
)
)
)
def backdrop_open_by_50_percent(self, *args):
self.root.ids.backdrop.open(-Window.height / 2)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop.gif
:align: center
.. Note:: `See full example <https://github.com/kivymd/KivyMD/wiki/Components-Backdrop>`_
"""
__all__ = (
"MDBackdropToolbar",
"MDBackdropFrontLayer",
"MDBackdropBackLayer",
"MDBackdrop",
)
import os
from typing import Union
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import (
BooleanProperty,
ColorProperty,
ListProperty,
NumericProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivymd import uix_path
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.card import MDCard
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.toolbar.toolbar import ActionTopAppBarButton, MDTopAppBar
with open(
os.path.join(uix_path, "backdrop", "backdrop.kv"),
encoding="utf-8",
) as kv_file:
Builder.load_string(kv_file.read())
class MDBackdrop(MDFloatLayout):
"""
For more information, see in the
:class:`~kivymd.uix.floatlayout.MDFloatLayout` class documentation.
:Events:
:attr:`on_open`
When the front layer drops.
:attr:`on_close`
When the front layer rises.
"""
anchor_title = OptionProperty("left", options=["left", "center", "right"])
"""
Position toolbar title. Only used with `material_style = 'M3'`
Available options are: `'left'`, `'center'`, `'right'`.
.. versionadded:: 1.0.0
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-anchor-title.png
:align: center
:attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'left'`.
"""
padding = ListProperty([0, 0, 0, 0])
"""
Padding for contents of the front layer.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-padding.png
:align: center
:attr:`padding` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
left_action_items = ListProperty()
"""
The icons and methods left of the :class:`kivymd.uix.toolbar.MDTopAppBar`
in back layer. For more information, see the
:class:`kivymd.uix.toolbar.MDTopAppBar` module
and :attr:`left_action_items` parameter.
:attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
right_action_items = ListProperty()
"""
Works the same way as :attr:`left_action_items`.
:attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
title = StringProperty()
"""
See the :class:`kivymd.uix.toolbar.MDTopAppBar.title` parameter.
:attr:`title` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
back_layer_color = ColorProperty(None)
"""
Background color of back layer in (r, g, b, a) or string format.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-back-layer-color.png
:align: center
:attr:`back_layer_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
front_layer_color = ColorProperty(None)
"""
Background color of front layer in (r, g, b, a) or string format.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-front-layer-color.png
:align: center
:attr:`front_layer_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
radius_left = NumericProperty("16dp")
"""
The value of the rounding radius of the upper left corner
of the front layer.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-radius-left.png
:align: center
:attr:`radius_left` is an :class:`~kivy.properties.NumericProperty`
and defaults to `16dp`.
"""
radius_right = NumericProperty("16dp")
"""
The value of the rounding radius of the upper right corner
of the front layer.
:attr:`radius_right` is an :class:`~kivy.properties.NumericProperty`
and defaults to `16dp`.
"""
header = BooleanProperty(True)
"""
Whether to use a header above the contents of the front layer.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-header.png
:align: center
:attr:`header` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
header_text = StringProperty("Header")
"""
Text of header.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-header-text.png
:align: center
:attr:`header_text` is an :class:`~kivy.properties.StringProperty`
and defaults to `'Header'`.
"""
close_icon = StringProperty("close")
"""
The name of the icon that will be installed on the toolbar
on the left when opening the front layer.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-close-icon.png
:align: center
:attr:`close_icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'close'`.
"""
opening_time = NumericProperty(0.2)
"""
The time taken for the panel to slide to the :attr:`state` `'open'`.
.. versionadded:: 1.0.0
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
opening_transition = StringProperty("out_quad")
"""
The name of the animation transition type to use when animating to
the :attr:`state` `'open'`.
.. versionadded:: 1.0.0
:attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_quad'`.
"""
closing_time = NumericProperty(0.2)
"""
The time taken for the panel to slide to the :attr:`state` `'close'`.
.. versionadded:: 1.0.0
:attr:`closing_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
closing_transition = StringProperty("out_quad")
"""
The name of the animation transition type to use when animating to
the :attr:`state` 'close'.
.. versionadded:: 1.0.0
:attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_quad'`.
"""
_open_icon = ""
_front_layer_open = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.register_event_type("on_open")
self.register_event_type("on_close")
Clock.schedule_once(
lambda x: self.on_left_action_items(self, self.left_action_items)
)
def on_open(self) -> None:
"""When the front layer drops."""
def on_close(self) -> None:
"""When the front layer rises."""
def on_left_action_items(self, instance_backdrop, menu: list) -> None:
if menu:
self.left_action_items = [menu[0]]
else:
self.left_action_items = [["menu", lambda x: self.open()]]
self._open_icon = self.left_action_items[0][0]
def on_header(self, instance_backdrop, value: bool) -> None:
def on_header(*args):
if not value:
self.ids._front_layer.remove_widget(self.ids.header_button)
Clock.schedule_once(on_header)
def open(self, open_up_to: int = 0) -> None:
"""
Opens the front layer.
:open_up_to:
the height to which the front screen will be lowered;
if equal to zero - falls to the bottom of the screen;
"""
self.animate_opacity_icon()
if self._front_layer_open:
self.close()
return
if open_up_to:
if open_up_to < (
self.ids.header_button.height - self.ids._front_layer.height
):
y = self.ids.header_button.height - self.ids._front_layer.height
elif open_up_to > 0:
y = 0
else:
y = open_up_to
else:
y = self.ids.header_button.height - self.ids._front_layer.height
Animation(y=y, d=self.opening_time, t=self.opening_transition).start(
self.ids._front_layer
)
self._front_layer_open = True
self.dispatch("on_open")
def close(self) -> None:
"""Opens the front layer."""
Animation(y=0, d=self.closing_time, t=self.closing_transition).start(
self.ids._front_layer
)
self._front_layer_open = False
self.dispatch("on_close")
def animate_opacity_icon(
self,
instance_icon_menu: Union[ActionTopAppBarButton, None] = None,
opacity_value: int = 0,
call_set_new_icon: bool = True,
) -> None:
"""Starts the opacity animation of the icon."""
if not instance_icon_menu:
instance_icon_menu = self.ids.toolbar.ids.left_actions.children[0]
anim = Animation(
opacity=opacity_value,
d=self.opening_time,
t=self.opening_transition,
)
if call_set_new_icon:
anim.bind(on_complete=self.set_new_icon)
anim.start(instance_icon_menu)
def set_new_icon(
self,
instance_animation: Animation,
instance_icon_menu: ActionTopAppBarButton,
) -> None:
"""
Sets the icon of the button depending on the state of the backdrop.
"""
instance_icon_menu.icon = (
self.close_icon
if instance_icon_menu.icon == self._open_icon
else self._open_icon
)
self.animate_opacity_icon(instance_icon_menu, 1, False)
def add_widget(self, widget, index=0, canvas=None):
if widget.__class__ in (MDBackdropToolbar, _BackLayer, _FrontLayer):
return super().add_widget(widget)
else:
if widget.__class__ is MDBackdropBackLayer:
self.ids.back_layer.add_widget(widget)
elif widget.__class__ is MDBackdropFrontLayer:
self.ids.front_layer.add_widget(widget)
class MDBackdropToolbar(MDTopAppBar):
"""
Implements a toolbar for back content.
For more information, see in the
:class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar` classes documentation.
"""
class MDBackdropFrontLayer(MDBoxLayout):
"""
Container for front content.
For more information, see in the
:class:`~kivymd.uix.boxlayout.MDBoxLayout` classes documentation.
"""
class MDBackdropBackLayer(MDBoxLayout):
"""
Container for back content.
For more information, see in the
:class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
"""
class _BackLayer(BoxLayout):
pass
class _FrontLayer(MDCard):
pass

View file

@ -0,0 +1 @@
from .banner import MDBanner # NOQA F401

View file

@ -0,0 +1,85 @@
#:import Window kivy.core.window.Window
<ThreeLineIconBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
tertiary_text: root.text_message[2]
divider: None
_no_ripple_effect: True
ImageLeftWidget:
source: root.icon
<TwoLineIconBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
divider: None
_no_ripple_effect: True
ImageLeftWidget:
source: root.icon
<OneLineIconBanner>
text: root.text_message[0]
divider: None
_no_ripple_effect: True
ImageLeftWidget:
source: root.icon
<ThreeLineBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
tertiary_text: root.text_message[2]
divider: None
_no_ripple_effect: True
<TwoLineBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
divider: None
_no_ripple_effect: True
<OneLineBanner>
text: root.text_message[0]
divider: None
_no_ripple_effect: True
<MDBanner>
size_hint_y: None
height: self.minimum_height
banner_y: 0
orientation: "vertical"
y: Window.height - self.banner_y
canvas:
Color:
rgba: 0, 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
MDBoxLayout:
id: container_message
adaptive_height: True
MDBoxLayout:
adaptive_size: True
pos_hint: {"right": 1}
padding: 0, 0, "8dp", "8dp"
spacing: "8dp"
MDBoxLayout:
id: left_action_box
adaptive_size: True
MDBoxLayout:
id: right_action_box
adaptive_size: True

View file

@ -0,0 +1,439 @@
"""
Components/Banner
=================
.. seealso::
`Material Design spec, Banner <https://material.io/components/banners>`_
.. rubric:: A banner displays a prominent message and related optional actions.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner.png
:align: center
Usage
=====
.. code-block:: python
from kivy.lang import Builder
from kivy.factory import Factory
from kivymd.app import MDApp
Builder.load_string('''
<ExampleBanner@Screen>
MDBanner:
id: banner
text: ["One line string text example without actions."]
# The widget that is under the banner.
# It will be shifted down to the height of the banner.
over_widget: screen
vertical_pad: toolbar.height
MDTopAppBar:
id: toolbar
title: "Example Banners"
elevation: 4
pos_hint: {'top': 1}
MDBoxLayout:
id: screen
orientation: "vertical"
size_hint_y: None
height: Window.height - toolbar.height
OneLineListItem:
text: "Banner without actions"
on_release: banner.show()
Widget:
''')
class Test(MDApp):
def build(self):
return Factory.ExampleBanner()
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-example-1.gif
:align: center
.. rubric:: Banner type.
By default, the banner is of the type ``'one-line'``:
.. code-block:: kv
MDBanner:
text: ["One line string text example without actions."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-one-line.png
:align: center
To use a two-line banner, specify the ``'two-line'`` :attr:`MDBanner.type` for the banner
and pass the list of two lines to the :attr:`MDBanner.text` parameter:
.. code-block:: kv
MDBanner:
type: "two-line"
text:
["One line string text example without actions.", "This is the second line of the banner message."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-two-line.png
:align: center
Similarly, create a three-line banner:
.. code-block:: kv
MDBanner:
type: "three-line"
text:
["One line string text example without actions.", "This is the second line of the banner message.", "and this is the third line of the banner message."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-three-line.png
:align: center
To add buttons to any type of banner,
use the :attr:`MDBanner.left_action` and :attr:`MDBanner.right_action` parameters,
which should take a list ['Button name', function]:
.. code-block:: kv
MDBanner:
text: ["One line string text example without actions."]
left_action: ["CANCEL", lambda x: None]
Or two buttons:
.. code-block:: kv
MDBanner:
text: ["One line string text example without actions."]
left_action: ["CANCEL", lambda x: None]
right_action: ["CLOSE", lambda x: None]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-actions.png
:align: center
If you want to use the icon on the left in the banner,
add the prefix `'-icon'` to the banner type:
.. code-block:: kv
MDBanner:
type: "one-line-icon"
icon: f"{images_path}/kivymd.png"
text: ["One line string text example without actions."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-icon.png
:align: center
.. Note:: `See full example <https://github.com/kivymd/KivyMD/wiki/Components-Banner>`_
"""
__all__ = ("MDBanner",)
import os
from typing import Union
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BoundedNumericProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.widget import Widget
from kivymd import uix_path
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDFlatButton
from kivymd.uix.card import MDCard
from kivymd.uix.list import (
OneLineAvatarListItem,
OneLineListItem,
ThreeLineAvatarListItem,
ThreeLineListItem,
TwoLineAvatarListItem,
TwoLineListItem,
)
with open(
os.path.join(uix_path, "banner", "banner.kv"),
encoding="utf-8",
) as kv_file:
Builder.load_string(kv_file.read())
class MDBanner(MDCard):
"""
Banner class.
For more information, see in the :class:`~kivymd.uix.card.MDCard`
class documentation.
"""
vertical_pad = NumericProperty(dp(68))
"""
Indent the banner at the top of the screen.
:attr:`vertical_pad` is an :class:`~kivy.properties.NumericProperty`
and defaults to `dp(68)`.
"""
opening_transition = StringProperty("in_quad")
"""
The name of the animation transition.
:attr:`opening_transition` is an :class:`~kivy.properties.StringProperty`
and defaults to `'in_quad'`.
"""
icon = StringProperty("data/logo/kivy-icon-128.png")
"""
Icon banner.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'data/logo/kivy-icon-128.png'`.
"""
over_widget = ObjectProperty()
"""
The widget that is under the banner.
It will be shifted down to the height of the banner.
:attr:`over_widget` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
text = ListProperty()
"""
List of lines for banner text.
Must contain no more than three lines for a
`'one-line'`, `'two-line'` and `'three-line'` banner, respectively.
:attr:`text` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
left_action = ListProperty()
"""
The action of banner.
To add one action, make a list [`'name_action'`, callback]
where `'name_action'` is a string that corresponds to an action name and
``callback`` is the function called on a touch release event.
:attr:`left_action` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
right_action = ListProperty()
"""
Works the same way as :attr:`left_action`.
:attr:`right_action` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
type = OptionProperty(
"one-line",
options=[
"one-line",
"two-line",
"three-line",
"one-line-icon",
"two-line-icon",
"three-line-icon",
],
allownone=True,
)
"""
Banner type. . Available options are: (`"one-line"`, `"two-line"`,
`"three-line"`, `"one-line-icon"`, `"two-line-icon"`, `"three-line-icon"`).
:attr:`type` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'one-line'`.
"""
opening_timeout = BoundedNumericProperty(0.7, min=0.7)
"""
Time interval after which the banner will be shown.
.. versionadded:: 1.0.0
:attr:`opening_timeout` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `0.7`.
"""
opening_time = NumericProperty(0.15)
"""
The time taken for the banner to slide to the :attr:`state` `'open'`.
.. versionadded:: 1.0.0
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.15`.
"""
closing_time = NumericProperty(0.15)
"""
The time taken for the banner to slide to the :attr:`state` `'close'`.
.. versionadded:: 1.0.0
:attr:`closing_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.15`.
"""
_type_message = None
_progress = False
def add_actions_buttons(
self, instance_box: MDBoxLayout, data: list
) -> None:
"""
Adds buttons to the banner.
:param data: ['NAME BUTTON', <function>];
"""
if data:
name_action_button, function_action_button = data
action_button = MDFlatButton(
text=f"[b]{name_action_button}[/b]",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
on_release=function_action_button,
)
action_button.markup = True
instance_box.add_widget(action_button)
def show(self) -> None:
"""Displays a banner on the screen."""
def show(interval: Union[int, float]):
self.set_type_banner()
self.add_actions_buttons(self.ids.left_action_box, self.left_action)
self.add_actions_buttons(
self.ids.right_action_box, self.right_action
)
self._add_banner_to_container()
Clock.schedule_once(self.animation_display_banner, 0.1)
if not self._progress:
self._progress = True
if self.ids.container_message.children:
self.hide()
Clock.schedule_once(show, self.opening_timeout)
def hide(self) -> None:
"""Hides the banner from the screen."""
def hide(interval: Union[int, float]):
anim = Animation(banner_y=0, d=self.closing_time)
anim.bind(on_complete=self._remove_banner)
anim.start(self)
Animation(
y=self.over_widget.y + self.height, d=self.closing_time
).start(self.over_widget)
if not self._progress:
self._progress = True
Clock.schedule_once(hide, 0.5)
def set_type_banner(self) -> None:
self._type_message = {
"three-line-icon": ThreeLineIconBanner,
"two-line-icon": TwoLineIconBanner,
"one-line-icon": OneLineIconBanner,
"three-line": ThreeLineBanner,
"two-line": TwoLineBanner,
"one-line": OneLineBanner,
}[self.type]
def animation_display_banner(self, interval: Union[int, float]) -> None:
Animation(
banner_y=self.height + self.vertical_pad,
d=self.opening_time,
t=self.opening_transition,
).start(self)
anim = Animation(
y=self.over_widget.y - self.height,
d=self.opening_time,
t=self.opening_transition,
)
anim.bind(on_complete=self._reset_progress)
anim.start(self.over_widget)
def _remove_banner(self, *args):
self.ids.container_message.clear_widgets()
self.ids.left_action_box.clear_widgets()
self.ids.right_action_box.clear_widgets()
self._reset_progress()
def _reset_progress(self, *args):
self._progress = False
def _add_banner_to_container(self) -> None:
self.ids.container_message.add_widget(
self._type_message(text_message=self.text, icon=self.icon)
)
class BaseBanner(Widget):
"""Implements the base banner class."""
text_message = ListProperty(["", "", ""])
"""
List of banner strings. First, second and, respectively, third lines.
:attr:`text_message` is an :class:`~kivy.properties.ListProperty`
and defaults to `['', '', '']`.
"""
icon = StringProperty()
"""
Icon banner.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
def on_touch_down(self, touch):
self.parent.parent.hide()
class ThreeLineIconBanner(ThreeLineAvatarListItem, BaseBanner):
pass
class TwoLineIconBanner(TwoLineAvatarListItem, BaseBanner):
pass
class OneLineIconBanner(OneLineAvatarListItem, BaseBanner):
pass
class ThreeLineBanner(ThreeLineListItem, BaseBanner):
pass
class TwoLineBanner(TwoLineListItem, BaseBanner):
pass
class OneLineBanner(OneLineListItem, BaseBanner):
pass

View file

@ -0,0 +1,35 @@
"""
Behaviors
=========
Modules and classes implementing various behaviors for buttons etc.
"""
from .backgroundcolor_behavior import (
BackgroundColorBehavior,
SpecificBackgroundColorBehavior,
)
# flake8: NOQA
from .declarative_behavior import DeclarativeBehavior
from .elevation import (
CircularElevationBehavior,
CommonElevationBehavior,
FakeCircularElevationBehavior,
FakeRectangularElevationBehavior,
RectangularElevationBehavior,
RoundedRectangularElevationBehavior,
)
from .motion_behavior import (
MotionDialogBehavior,
MotionShackBehavior,
MotionDropDownMenuBehavior,
)
from .magic_behavior import MagicBehavior
from .ripple_behavior import CircularRippleBehavior, RectangularRippleBehavior
from .rotate_behavior import RotateBehavior
from .scale_behavior import ScaleBehavior
from .stencil_behavior import StencilBehavior
from .touch_behavior import TouchBehavior
from .hover_behavior import HoverBehavior # isort:skip

View file

@ -0,0 +1,290 @@
"""
Behaviors/Background Color
==========================
.. note:: The following classes are intended for in-house use of the library.
"""
from __future__ import annotations
__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior")
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.properties import (
ColorProperty,
ListProperty,
NumericProperty,
OptionProperty,
ReferenceListProperty,
StringProperty,
VariableListProperty,
)
from kivy.utils import get_color_from_hex
from kivymd.color_definitions import hue, palette, text_colors
from kivymd.theming import ThemeManager
Builder.load_string(
"""
#:import RelativeLayout kivy.uix.relativelayout.RelativeLayout
<BackgroundColorBehavior>
canvas:
PushMatrix
Rotate:
angle: self.angle
origin: self._background_origin
Color:
rgba: self._md_bg_color
RoundedRectangle:
group: "Background_instruction"
size: self.size
pos: self.pos if not isinstance(self, RelativeLayout) else (0, 0)
# FIXME: Sometimes the radius has the value [], which get a
# `GraphicException:
# Invalid radius value, must be list of tuples/numerics` error`
radius: root.radius if root.radius else [0, 0, 0, 0]
source: root.background
Color:
rgba: self.line_color if self.line_color else (0, 0, 0, 0)
# TODO: maybe we should use SmoothLine,
# but this should be tested on all widgets.
Line:
width: root.line_width
rounded_rectangle:
[ \
self.x,
self.y, \
self.width, \
self.height, \
*self.radius, \
]
PopMatrix
""",
filename="BackgroundColorBehavior.kv",
)
class BackgroundColorBehavior:
background = StringProperty()
"""
Background image path.
:attr:`background` is a :class:`~kivy.properties.StringProperty`
and defaults to `None`.
"""
radius = VariableListProperty([0], length=4)
"""
Canvas radius.
.. code-block:: python
# Top left corner slice.
MDBoxLayout:
md_bg_color: app.theme_cls.primary_color
radius: [25, 0, 0, 0]
:attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
# FIXME: in this case, we will not be able to animate this property
# using the `Animation` class.
md_bg_color = ColorProperty([1, 1, 1, 0])
"""
The background color of the widget (:class:`~kivy.uix.widget.Widget`)
that will be inherited from the :attr:`BackgroundColorBehavior` class.
For example:
.. code-block:: kv
Widget:
canvas:
Color:
rgba: 0, 1, 1, 1
Rectangle:
size: self.size
pos: self.pos
similar to code:
.. code-block:: kv
<MyWidget@BackgroundColorBehavior>
md_bg_color: 0, 1, 1, 1
:attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 0]`.
"""
line_color = ColorProperty([0, 0, 0, 0])
"""
If a custom value is specified for the `line_color parameter`, the border
of the specified color will be used to border the widget:
.. code-block:: kv
MDBoxLayout:
size_hint: .5, .2
md_bg_color: 0, 1, 1, .5
line_color: 0, 0, 1, 1
radius: [24, ]
.. versionadded:: 0.104.2
:attr:`line_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0]`.
"""
line_width = NumericProperty(1)
"""
Border of the specified width will be used to border the widget.
.. versionadded:: 1.0.0
:attr:`line_width` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
angle = NumericProperty(0)
background_origin = ListProperty(None)
_background_x = NumericProperty(0)
_background_y = NumericProperty(0)
_background_origin = ReferenceListProperty(_background_x, _background_y)
_md_bg_color = ColorProperty([0, 0, 0, 0])
_origin_line_color = ColorProperty(None)
_origin_md_bg_color = ColorProperty(None)
def __init__(self, **kwarg):
super().__init__(**kwarg)
self.bind(
pos=self.update_background_origin,
disabled=self.restore_color_origin,
)
def restore_color_origin(self, instance_md_widget, value: bool) -> None:
"""Called when the values of :attr:`disabled` change."""
if not value:
if self._origin_line_color:
self.line_color = self._origin_line_color
if self._origin_md_bg_color:
self.md_bg_color = self._origin_md_bg_color
def on_line_color(self, instance_md_widget, value: list | str) -> None:
"""Called when the values of :attr:`line_color` change."""
if not self.disabled:
self._origin_line_color = value
def on_md_bg_color(self, instance_md_widget, color: list | str):
"""Called when the values of :attr:`md_bg_color` change."""
if (
hasattr(self, "theme_cls")
and self.theme_cls.theme_style_switch_animation
):
Animation(
_md_bg_color=color,
d=self.theme_cls.theme_style_switch_animation_duration,
t="linear",
).start(self)
else:
self._md_bg_color = color
if not self.disabled:
self._origin_md_bg_color = color
def update_background_origin(self, instance_md_widget, pos: list) -> None:
"""Called when the values of :attr:`pos` change."""
if self.background_origin:
self._background_origin = self.background_origin
else:
self._background_origin = self.center
class SpecificBackgroundColorBehavior(BackgroundColorBehavior):
background_palette = OptionProperty(
"Primary", options=["Primary", "Accent", *palette]
)
"""
See :attr:`kivymd.color_definitions.palette`.
:attr:`background_palette` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'Primary'`.
"""
background_hue = OptionProperty("500", options=hue)
"""
See :attr:`kivymd.color_definitions.hue`.
:attr:`background_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'500'`.
"""
specific_text_color = ColorProperty([0, 0, 0, 0.87])
"""
:attr:`specific_text_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0.87]`.
"""
specific_secondary_text_color = ColorProperty([0, 0, 0, 0.87])
"""
:attr:`specific_secondary_text_color`is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0.87]`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
if hasattr(self, "theme_cls"):
self.theme_cls.bind(
primary_palette=self._update_specific_text_color,
accent_palette=self._update_specific_text_color,
theme_style=self._update_specific_text_color,
)
self.bind(
background_hue=self._update_specific_text_color,
background_palette=self._update_specific_text_color,
)
self._update_specific_text_color(None, None)
def _update_specific_text_color(
self, instance_theme_manager: ThemeManager, theme_style: str
) -> None:
if hasattr(self, "theme_cls"):
palette = {
"Primary": self.theme_cls.primary_palette,
"Accent": self.theme_cls.accent_palette,
}.get(self.background_palette, self.background_palette)
else:
palette = {"Primary": "Blue", "Accent": "Amber"}.get(
self.background_palette, self.background_palette
)
color = get_color_from_hex(text_colors[palette][self.background_hue])
secondary_color = color[:]
# Check for black text (need to adjust opacity).
if (color[0] + color[1] + color[2]) == 0:
color[3] = 0.87
secondary_color[3] = 0.54
else:
secondary_color[3] = 0.7
if (
hasattr(self, "theme_cls")
and self.theme_cls.theme_style_switch_animation
):
Animation(
specific_text_color=color,
specific_secondary_text_color=secondary_color,
d=self.theme_cls.theme_style_switch_animation_duration,
t="linear",
).start(self)
else:
self.specific_text_color = color
self.specific_secondary_text_color = secondary_color

View file

@ -0,0 +1,317 @@
"""
Behaviors/Declarative
=====================
.. versionadded:: 1.0.0
.. raw:: html
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
<iframe
src="https://www.youtube.com/embed/_kiaJacLz8o"
frameborder="0"
allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</iframe>
</div>
As you already know, the Kivy framework provides the best/simplest/modern
UI creation tool that allows you to separate the logic of your application
from the description of the properties of widgets/GUI components.
This tool is named `KV Language <https://kivy.org/doc/stable/guide/lang.html>`_.
But in addition to creating a user interface using the KV Language Kivy allows
you to create user interface elements directly in the Python code.
And if you've ever created a user interface in Python code, you know how ugly
it looks. Even in the simplest user interface design, which was created using
Python code it is impossible to trace the widget tree, because in Python code
you build the user interface in an imperative style.
Imperative style
----------------
.. code-block:: python
from kivymd.app import MDApp
from kivymd.uix.bottomnavigation import MDBottomNavigation, MDBottomNavigationItem
from kivymd.uix.label import MDLabel
from kivymd.uix.screen import MDScreen
class Example(MDApp):
def build(self):
screen = MDScreen()
bottom_navigation = MDBottomNavigation(
panel_color="#eeeaea",
selected_color_background="#97ecf8",
text_color_active="white",
)
data = {
"screen 1": {"text": "Mail", "icon": "gmail"},
"screen 2": {"text": "Discord", "icon": "discord"},
"screen 3": {"text": "LinkedIN", "icon": "linkedin"},
}
for key in data.keys():
text = data[key]["text"]
navigation_item = MDBottomNavigationItem(
name=key, text=text, icon=data[key]["icon"]
)
navigation_item.add_widget(MDLabel(text=text, halign="center"))
bottom_navigation.add_widget(navigation_item)
screen.add_widget(bottom_navigation)
return screen
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-styles-programming.png
:align: center
Take a look at the above code example. This is a very simple UI. But looking
at this code, you will not be able to figure the widget tree and understand
which UI this code implements. This is named imperative programming style,
which is used in Kivy.
Now let's see how the same code is implemented using the KV language,
which uses a declarative style of describing widget properties.
Declarative style with KV language
----------------------------------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
class Test(MDApp):
def build(self):
return Builder.load_string(
'''
MDScreen:
MDBottomNavigation:
panel_color: "#eeeaea"
selected_color_background: "#97ecf8"
text_color_active: "white"
MDBottomNavigationItem:
name: "screen 1"
text: "Mail"
icon: "gmail"
MDLabel:
text: "Mail"
halign: "center"
MDBottomNavigationItem:
name: "screen 2"
text: "Discord"
icon: "discord"
MDLabel:
text: "Discord"
halign: "center"
MDBottomNavigationItem:
name: "screen 3"
text: "LinkedIN"
icon: "linkedin"
MDLabel:
text: "LinkedIN"
halign: "center"
'''
)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-styles-programming.png
:align: center
Looking at this code, we can now clearly see the widget tree and their properties.
We can quickly navigate through the components of the screen and quickly
change/add new properties/widgets. This is named declarative UI creation style.
But now the KivyMD library allows you to write Python code in a declarative style.
Just as it is implemented in Flutter/Jetpack Compose/SwiftUI.
Declarative style with Python code
----------------------------------
.. code-block:: python
from kivymd.app import MDApp
from kivymd.uix.bottomnavigation import MDBottomNavigation, MDBottomNavigationItem
from kivymd.uix.label import MDLabel
from kivymd.uix.screen import MDScreen
class Example(MDApp):
def build(self):
return (
MDScreen(
MDBottomNavigation(
MDBottomNavigationItem(
MDLabel(
text="Mail",
halign="center",
),
name="screen 1",
text="Mail",
icon="gmail",
),
MDBottomNavigationItem(
MDLabel(
text="Discord",
halign="center",
),
name="screen 2",
text="Discord",
icon="discord",
),
MDBottomNavigationItem(
MDLabel(
text="LinkedIN",
halign="center",
),
name="screen 3",
text="LinkedIN",
icon="linkedin",
),
panel_color="#eeeaea",
selected_color_background="#97ecf8",
text_color_active="white",
)
)
)
Example().run()
.. note:: The KivyMD library does not support creating Kivy widgets in Python
code in a declarative style.
But you can still use the declarative style of creating Kivy widgets in Python code.
To do this, you need to create a new class that will be inherited from the Kivy
widget and the :class:`~DeclarativeBehavior` class:
.. code-block:: python
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivymd.app import MDApp
from kivymd.uix.behaviors import DeclarativeBehavior
class DeclarativeStyleBoxLayout(DeclarativeBehavior, BoxLayout):
pass
class Example(MDApp):
def build(self):
return (
DeclarativeStyleBoxLayout(
Button(),
Button(),
orientation="vertical",
)
)
Example().run()
Get objects by identifiers
--------------------------
In the declarative style in Python code, the ids parameter of the specified
widget will return only the id of the child widget/container, ignoring other ids.
Therefore, to get objects by identifiers in declarative style in Python code,
you must specify all the container ids in which the widget is nested until you
get to the desired id:
.. code-block::
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.floatlayout import MDFloatLayout
class Example(MDApp):
def build(self):
return (
MDBoxLayout(
MDFloatLayout(
MDRaisedButton(
id="button_1",
text="Button 1",
pos_hint={"center_x": 0.5, "center_y": 0.5},
),
id="box_container_1",
),
MDBoxLayout(
MDFloatLayout(
MDRaisedButton(
id="button_2",
text="Button 2",
pos_hint={"center_x": 0.5, "center_y": 0.5},
),
id="float_container",
),
id="box_container_2",
)
)
)
def on_start(self):
# {
# 'box_container_1': <kivymd.uix.floatlayout.MDFloatLayout>,
# 'box_container_2': <kivymd.uix.boxlayout.MDBoxLayout object>
# }
print(self.root.ids)
# <kivymd.uix.button.button.MDRaisedButton>
print(self.root.ids.box_container_2.ids.float_container.ids.button_2)
Example().run()
Yes, this is not a very good solution, but I think it will be fixed soon.
.. warning:: Declarative programming style in Python code in the KivyMD library
is an experimental feature. Therefore, if you receive errors, do not hesitate
to create new issue in the KivyMD repository.
"""
from kivy.properties import StringProperty
from kivy.uix.widget import Widget
class DeclarativeBehavior:
"""
Implements the creation and addition of child widgets as declarative
programming style.
"""
id = StringProperty()
"""
Widget ID.
:attr:`id` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
for child in args:
if issubclass(child.__class__, Widget):
self.add_widget(child)
if hasattr(child, "id") and child.id:
self.ids[child.id] = child

View file

@ -0,0 +1,772 @@
"""
Behaviors/Elevation
===================
.. seealso::
`Material Design spec, Elevation <https://material.io/design/environment/elevation.html>`_
.. rubric:: Elevation is the relative distance between two surfaces along the z-axis.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-previous.png
:align: center
To create an elevation effect, use the :class:`~CommonElevationBehavior` class.
For example, let's create a button with a rectangular elevation effect:
.. tabs::
.. tab:: Declarative style with KV
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import (
RectangularRippleBehavior,
BackgroundColorBehavior,
CommonElevationBehavior,
)
KV = '''
<RectangularElevationButton>
size_hint: None, None
size: "250dp", "50dp"
MDScreen:
# With elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 4
shadow_offset: 0, -6
shadow_softness: 4
# Without elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .4}
'''
class RectangularElevationButton(
RectangularRippleBehavior,
CommonElevationBehavior,
ButtonBehavior,
BackgroundColorBehavior,
):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = "red"
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. tab:: Declarative python style
.. code-block:: python
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import (
RectangularRippleBehavior,
BackgroundColorBehavior,
CommonElevationBehavior,
)
from kivymd.uix.screen import MDScreen
class RectangularElevationButton(
RectangularRippleBehavior,
CommonElevationBehavior,
ButtonBehavior,
BackgroundColorBehavior,
):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = "red"
self.size_hint = (None, None)
self.size = ("250dp", "50dp")
class Example(MDApp):
def build(self):
return (
MDScreen(
RectangularElevationButton(
pos_hint={"center_x": .5, "center_y": .6},
elevation=4,
shadow_softness=4,
shadow_offset=(0, -6),
),
RectangularElevationButton(
pos_hint={"center_x": .5, "center_y": .4},
),
)
)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-effect.png
:align: center
.. warning::
If before the KivyMD 1.1.0 library version you used the elevation property
with an average value of `12` for the shadow, then starting with the KivyMD
1.1.0 library version, the average value of the elevation property will be
somewhere `4`.
Similarly, create a circular button:
.. tabs::
.. tab:: Declarative style with KV
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import CircularRippleBehavior, CommonElevationBehavior
from kivymd.uix.floatlayout import MDFloatLayout
KV = '''
<CircularElevationButton>
size_hint: None, None
size: "100dp", "100dp"
radius: self.size[0] / 2
shadow_radius: self.radius[0]
md_bg_color: "red"
MDIcon:
icon: "hand-heart"
halign: "center"
valign: "center"
pos_hint: {"center_x": .5, "center_y": .5}
size: root.size
pos: root.pos
font_size: root.size[0] * .6
theme_text_color: "Custom"
text_color: "white"
MDScreen:
CircularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 4
shadow_softness: 4
'''
class CircularElevationButton(
CommonElevationBehavior,
CircularRippleBehavior,
ButtonBehavior,
MDFloatLayout,
):
pass
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. tab:: Declarative python style
.. code-block:: python
from kivy.metrics import dp
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import CircularRippleBehavior, CommonElevationBehavior
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.label import MDIcon
from kivymd.uix.screen import MDScreen
class CircularElevationButton(
CommonElevationBehavior,
CircularRippleBehavior,
ButtonBehavior,
MDFloatLayout,
):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size_hint = (None, None)
self.size = (dp(100), dp(100))
self.radius = dp(100) / 2
self.shadow_radius = dp(100) / 2
self.md_bg_color = "red"
self.add_widget(
MDIcon(
icon="hand-heart",
halign="center",
valign="center",
pos_hint={"center_x": .5, "center_y": .5},
size=self.size,
theme_text_color="Custom",
text_color="white",
font_size=self.size[0] * 0.6,
)
)
class Example(MDApp):
def build(self):
return (
MDScreen(
CircularElevationButton(
pos_hint={"center_x": .5, "center_y": .5},
elevation=4,
shadow_softness=4,
)
)
)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-elevation-effect.png
:align: center
Animating the elevation
-----------------------
.. tabs::
.. tab:: Declarative style with KV
.. code-block:: python
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import CommonElevationBehavior, RectangularRippleBehavior
from kivymd.uix.widget import MDWidget
KV = '''
MDScreen:
ElevatedWidget:
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: None, None
size: 100, 100
md_bg_color: 0, 0, 1, 1
elevation: 2
radius: 18
'''
class ElevatedWidget(
CommonElevationBehavior,
RectangularRippleBehavior,
ButtonBehavior,
MDWidget,
):
_elev = 0 # previous elevation value
def on_press(self, *args):
if not self._elev:
self._elev = self.elevation
Animation(elevation=self.elevation + 2, d=0.4).start(self)
def on_release(self, *args):
Animation.cancel_all(self, "elevation")
Animation(elevation=self._elev, d=0.1).start(self)
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. tab:: Declarative python style
.. code-block:: python
from kivy.animation import Animation
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import CommonElevationBehavior, RectangularRippleBehavior
from kivymd.uix.screen import MDScreen
from kivymd.uix.widget import MDWidget
class ElevatedWidget(
CommonElevationBehavior,
RectangularRippleBehavior,
ButtonBehavior,
MDWidget,
):
_elev = 0 # previous elevation value
def on_press(self, *args):
if not self._elev:
self._elev = self.elevation
Animation(elevation=self.elevation + 2, d=0.4).start(self)
def on_release(self, *args):
Animation.cancel_all(self, "elevation")
Animation(elevation=self._elev, d=0.1).start(self)
class Example(MDApp):
def build(self):
return (
MDScreen(
ElevatedWidget(
pos_hint={'center_x': .5, 'center_y': .5},
size_hint=(None, None),
size=(100, 100),
md_bg_color="blue",
elevation=2,
radius=18,
)
)
)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-animation-effect.gif
:align: center
"""
from __future__ import annotations
__all__ = (
"CommonElevationBehavior",
"RectangularElevationBehavior",
"CircularElevationBehavior",
"RoundedRectangularElevationBehavior",
"FakeRectangularElevationBehavior",
"FakeCircularElevationBehavior",
)
from kivy import Logger
from kivy.lang import Builder
from kivy.properties import (
BoundedNumericProperty,
ColorProperty,
ListProperty,
NumericProperty,
VariableListProperty,
)
from kivy.uix.widget import Widget
Builder.load_string(
"""
<CommonElevationBehavior>
canvas.before:
PushMatrix
Scale:
x: self.scale_value_x
y: self.scale_value_y
z: self.scale_value_x
origin:
self.center \
if not self.scale_value_center else \
self.scale_value_center
Rotate:
angle: self.rotate_value_angle
axis: tuple(self.rotate_value_axis)
origin: self.center
Color:
rgba:
(0, 0, 0, 0) \
if self.disabled or not self.elevation else \
root.shadow_color
BoxShadow:
pos: self.pos
size: self.size
offset: root.shadow_offset
spread_radius: -(root.shadow_softness), -(root.shadow_softness)
blur_radius: root.elevation * 10
border_radius:
(root.radius if hasattr(self, "radius") else [0, 0, 0, 0]) \
if root.shadow_radius == [0.0, 0.0, 0.0, 0.0] else \
root.shadow_radius
canvas.after:
PopMatrix
"""
)
class CommonElevationBehavior(Widget):
"""
Common base class for rectangular and circular elevation behavior.
For more information, see in the :class:`~kivy.uix.widget.Widget`
class documentation.
"""
elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
"""
Elevation of the widget.
:attr:`elevation` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `0`.
"""
shadow_radius = VariableListProperty([0], length=4)
"""
Radius of the corners of the shadow.
.. versionadded:: 1.1.0
You don't have to use this parameter.
The radius of the elevation effect is calculated automatically one way
or another based on the radius of the parent widget, for example:
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
MDScreen:
MDCard:
radius: 12, 46, 12, 46
size_hint: .5, .3
pos_hint: {"center_x": .5, "center_y": .5}
elevation: 2
shadow_softness: 4
shadow_offset: (2, -2)
'''
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-radius.png
:align: center
:attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
shadow_softness = NumericProperty(0.0)
"""
Softness of the shadow.
.. versionadded:: 1.1.0
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import BackgroundColorBehavior, CommonElevationBehavior
KV = '''
<RectangularElevationButton>
size_hint: None, None
size: "250dp", "50dp"
MDScreen:
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 6
shadow_softness: 6
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .4}
elevation: 6
shadow_softness: 12
'''
class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = "blue"
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-softness.png
:align: center
:attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty`
and defaults to `12`.
"""
shadow_softness_size = BoundedNumericProperty(2, min=2, deprecated=True)
"""
The value of the softness of the shadow.
.. versionadded:: 1.1.0
.. deprecated:: 1.2.0
:attr:`shadow_softness_size` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2`.
"""
shadow_offset = ListProperty((0, 0))
"""
Offset of the shadow.
.. versionadded:: 1.1.0
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import BackgroundColorBehavior, CommonElevationBehavior
KV = '''
<RectangularElevationButton>
size_hint: None, None
size: "100dp", "100dp"
MDScreen:
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .5}
elevation: 6
shadow_radius: 6
shadow_softness: 12
shadow_offset: -12, -12
'''
class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = "blue"
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-1.png
:align: center
.. code-block:: kv
RectangularElevationButton:
shadow_offset: 12, -12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-2.png
:align: center
.. code-block:: kv
RectangularElevationButton:
shadow_offset: 12, 12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-3.png
:align: center
.. code-block:: kv
RectangularElevationButton:
shadow_offset: -12, 12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-4.png
:align: center
:attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty`
and defaults to `(0, 0)`.
"""
shadow_color = ColorProperty([0, 0, 0, 0.6])
"""
Offset of the shadow.
.. versionadded:: 1.1.0
.. code-block:: python
RectangularElevationButton:
shadow_color: 0, 0, 1, .8
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-color.png
:align: center
:attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0.6]`.
"""
scale_value_x = NumericProperty(1)
"""
X-axis value.
.. versionadded:: 1.2.0
:attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
scale_value_y = NumericProperty(1)
"""
Y-axis value.
.. versionadded:: 1.2.0
:attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
scale_value_z = NumericProperty(1)
"""
Z-axis value.
.. versionadded:: 1.2.0
:attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
scale_value_center = ListProperty()
"""
Origin of the scale.
.. versionadded:: 1.2.0
The format of the origin can be either (x, y) or (x, y, z).
:attr:`scale_value_center` is an :class:`~kivy.properties.NumericProperty`
and defaults to `[]`.
"""
rotate_value_angle = NumericProperty(0)
"""
Property for getting/setting the angle of the rotation.
.. versionadded:: 1.2.0
:attr:`rotate_value_angle` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
rotate_value_axis = ListProperty((0, 0, 1))
"""
Property for getting/setting the axis of the rotation.
.. versionadded:: 1.2.0
:attr:`rotate_value_axis` is an :class:`~kivy.properties.ListProperty`
and defaults to `(0, 0, 1)`.
"""
_elevation = 0
def on_elevation(self, instance, value) -> None:
self._elevation = value
class RectangularElevationBehavior(CommonElevationBehavior):
"""
.. deprecated:: 1.1.0
Use :class:`~CommonElevationBehavior` class instead.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Logger.warning(
"KivyMD: "
"The `RectangularElevationBehavior` class has been deprecated. "
"Use the `CommonElevationBehavior` class instead.`"
)
class CircularElevationBehavior(CommonElevationBehavior):
"""
.. deprecated:: 1.1.0
Use :class:`~CommonElevationBehavior` class instead.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Logger.warning(
"KivyMD: "
"The `CircularElevationBehavior` class has been deprecated. "
"Use the `CommonElevationBehavior` class instead.`"
)
class RoundedRectangularElevationBehavior(CommonElevationBehavior):
"""
.. deprecated:: 1.1.0
Use :class:`~CommonElevationBehavior` class instead.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Logger.warning(
"KivyMD: "
"The `RoundedRectangularElevationBehavior` class has been "
"deprecated. Use the `CommonElevationBehavior` class instead.`"
)
class FakeRectangularElevationBehavior(CommonElevationBehavior):
"""
.. deprecated:: 1.1.0
Use :class:`~CommonElevationBehavior` class instead.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Logger.warning(
"KivyMD: "
"The `FakeRectangularElevationBehavior` class has been "
"deprecated. Use the `CommonElevationBehavior` class instead."
)
class FakeCircularElevationBehavior(CommonElevationBehavior):
"""
.. deprecated:: 1.1.0
Use :class:`~CommonElevationBehavior` class instead.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Logger.warning(
"KivyMD: "
"The `FakeCircularElevationBehavior` class has been deprecated. "
"Use the `CommonElevationBehavior` class instead."
)

View file

@ -0,0 +1,147 @@
"""
Behaviors/Focus
===============
.. rubric:: Changing the background color when the mouse is on the widget.
To apply focus behavior, you must create a new class that is inherited from the
widget to which you apply the behavior and from the :class:`FocusBehavior` class.
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularElevationBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.behaviors.focus_behavior import FocusBehavior
KV = '''
MDScreen:
md_bg_color: 1, 1, 1, 1
FocusWidget:
size_hint: .5, .3
pos_hint: {"center_x": .5, "center_y": .5}
md_bg_color: app.theme_cls.bg_light
MDLabel:
text: "Label"
theme_text_color: "Primary"
pos_hint: {"center_y": .5}
halign: "center"
'''
class FocusWidget(MDBoxLayout, RectangularElevationBehavior, FocusBehavior):
pass
class Test(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/focus-widget.gif
:align: center
Color change at focus/defocus
.. code-block:: kv
FocusWidget:
focus_color: 1, 0, 1, 1
unfocus_color: 0, 0, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/focus-defocus-color.gif
:align: center
"""
__all__ = ("FocusBehavior",)
from kivy.app import App
from kivy.properties import BooleanProperty, ColorProperty
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.behaviors import HoverBehavior
class FocusBehavior(HoverBehavior, ButtonBehavior):
"""
Focus behavior class.
For more information, see in the :class:`~kivymd.uix.behavior.HoverBehavior`
and :class:`~kivy.uix.button.ButtonBehavior` classes documentation.
:Events:
:attr:`on_enter`
Called when mouse enters the bbox of the widget AND the widget is visible
:attr:`on_leave`
Called when the mouse exits the widget AND the widget is visible
"""
focus_behavior = BooleanProperty(True)
"""
Using focus when hovering over a widget.
:attr:`focus_behavior` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
focus_color = ColorProperty(None)
"""
The color of the widget when the mouse enters the bbox of the widget.
:attr:`focus_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
unfocus_color = ColorProperty(None)
"""
The color of the widget when the mouse exits the bbox widget.
:attr:`unfocus_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
def on_enter(self):
"""Called when mouse enter the bbox of the widget."""
if (
hasattr(self, "md_bg_color") or hasattr(self, "bg_color")
) and self.focus_behavior:
if hasattr(self, "theme_cls") and not self.focus_color:
color = self.theme_cls.bg_normal
else:
if not self.focus_color:
color = App.get_running_app().theme_cls.bg_normal
else:
color = self.focus_color
self._set_bg_color(color)
def on_leave(self):
"""Called when the mouse exit the widget."""
if (
hasattr(self, "md_bg_color") or hasattr(self, "bg_color")
) and self.focus_behavior:
if hasattr(self, "theme_cls") and not self.unfocus_color:
color = self.theme_cls.bg_light
else:
if not self.unfocus_color:
color = App.get_running_app().theme_cls.bg_light
else:
color = self.unfocus_color
self._set_bg_color(color)
def _set_bg_color(self, color):
if hasattr(self, "md_bg_color"):
self.md_bg_color = color
elif hasattr(self, "bg_color"):
self.bg_color = color

View file

@ -0,0 +1,234 @@
"""
Behaviors/Hover
===============
.. rubric:: Changing when the mouse is on the widget and the widget is visible.
To apply hover behavior, you must create a new class that is inherited from the
widget to which you apply the behavior and from the :attr:`HoverBehavior` class.
In `KV file`:
.. code-block:: kv
<HoverItem@MDBoxLayout+HoverBehavior>
In `python file`:
.. code-block:: python
class HoverItem(MDBoxLayout, HoverBehavior):
'''Custom item implementing hover behavior.'''
After creating a class, you must define two methods for it:
:attr:`HoverBehavior.on_enter` and :attr:`HoverBehavior.on_leave`, which will be automatically called
when the mouse cursor is over the widget and when the mouse cursor goes beyond
the widget.
.. note::
:class:`~HoverBehavior` will by default check to see if the current Widget is visible (i.e. not covered by a modal or popup and not a part of a Relative Layout, MDTab or Carousel that is not currently visible etc) and will only issue events if the widget is visible.
To get the legacy behavior that the events are always triggered, you can set `detect_visible` on the Widget to `False`.
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
Screen
MDBoxLayout:
id: box
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: .8, .8
md_bg_color: app.theme_cls.bg_darkest
'''
class HoverItem(MDBoxLayout, HoverBehavior):
'''Custom item implementing hover behavior.'''
def on_enter(self, *args):
'''The method will be called when the mouse cursor
is within the borders of the current widget.'''
self.md_bg_color = (1, 1, 1, 1)
def on_leave(self, *args):
'''The method will be called when the mouse cursor goes beyond
the borders of the current widget.'''
self.md_bg_color = self.theme_cls.bg_darkest
class Test(MDApp):
def build(self):
self.screen = Builder.load_string(KV)
for i in range(5):
self.screen.ids.box.add_widget(HoverItem())
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hover-behavior.gif
:width: 250 px
:align: center
"""
__all__ = ("HoverBehavior",)
from kivy.core.window import Window
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.widget import Widget
class HoverBehavior(object):
"""
:Events:
:attr:`on_enter`
Called when mouse enters the bbox of the widget AND the widget is visible
:attr:`on_leave`
Called when the mouse exits the widget AND the widget is visible
"""
hovering = BooleanProperty(False)
"""
`True`, if the mouse cursor is within the borders of the widget.
Note that this is set and cleared even if the widget is not visible
:attr:`hover` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
hover_visible = BooleanProperty(False)
"""
`True` if hovering is True AND is the current widget is visible
:attr:`hover_visible` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
enter_point = ObjectProperty(allownone=True)
"""
Holds the last position where the mouse pointer crossed into the Widget
if the Widget is visible and is currently in a hovering state
:attr:`enter_point` is a :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
detect_visible = BooleanProperty(True)
"""
Should this widget perform the visibility check?
:attr:`detect_visible` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
def __init__(self, **kwargs):
self.register_event_type("on_enter")
self.register_event_type("on_leave")
Window.bind(mouse_pos=self.on_mouse_update)
super(HoverBehavior, self).__init__(**kwargs)
def on_mouse_update(self, *args):
# If the Widget currently has no parent, do nothing
if not self.get_root_window():
return
pos = args[1]
#
# is the pointer in the same position as the widget?
# If not - then issue an on_exit event if needed
#
if not self.collide_point(*self.to_widget(*pos)):
self.hovering = False
self.enter_point = None
if self.hover_visible:
self.hover_visible = False
self.dispatch("on_leave")
return
#
# The pointer is in the same position as the widget
#
if self.hovering:
#
# nothing to do here. Not - this does not handle the case where
# a popup comes over an existing hover event.
# This seems reasonable
#
return
#
# Otherwise - set the hovering attribute
#
self.hovering = True
#
# We need to traverse the tree to see if the Widget is visible
#
# This is a two stage process:
# - first go up the tree to the root Window.
# At each stage - check that the Widget is actually visible
# - Second - At the root Window check that there is not another branch
# covering the Widget
#
self.hover_visible = True
if self.detect_visible:
widget: Widget = self
while True:
# Walk up the Widget tree from the target Widget
parent = widget.parent
try:
# See if the mouse point collides with the parent
# using both local and glabal coordinates to cover absoluet and relative layouts
pinside = parent.collide_point(
*parent.to_widget(*pos)
) or parent.collide_point(*pos)
except Exception:
# The collide_point will error when you reach the root Window
break
if not pinside:
self.hover_visible = False
break
# Iterate upwards
widget = parent
#
# parent = root window
# widget = first Widget on the current branch
#
children = parent.children
for child in children:
# For each top level widget - check if is current branch
# If it is - then break.
# If not then - since we start at 0 - this widget is visible
#
# Check to see if it should take the hover
#
if child == widget:
# this means that the current widget is visible
break
if child.collide_point(*pos):
# this means that the current widget is covered by a modal or popup
self.hover_visible = False
break
if self.hover_visible:
self.enter_point = pos
self.dispatch("on_enter")
def on_enter(self):
"""Called when mouse enters the bbox of the widget AND the widget is visible."""
def on_leave(self):
"""Called when the mouse exits the widget AND the widget is visible."""

View file

@ -0,0 +1,189 @@
"""
Behaviors/Magic
===============
.. rubric:: Magical effects for buttons.
.. warning:: Magic effects do not work correctly with `KivyMD` buttons!
To apply magic effects, you must create a new class that is inherited from the
widget to which you apply the effect and from the :attr:`MagicBehavior` class.
In `KV file`:
.. code-block:: kv
<MagicButton@MagicBehavior+MDRectangleFlatButton>
In `python file`:
.. code-block:: python
class MagicButton(MagicBehavior, MDRectangleFlatButton):
pass
.. rubric:: The :attr:`MagicBehavior` class provides five effects:
- :attr:`MagicBehavior.wobble`
- :attr:`MagicBehavior.grow`
- :attr:`MagicBehavior.shake`
- :attr:`MagicBehavior.twist`
- :attr:`MagicBehavior.shrink`
Example:
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
<MagicButton@MagicBehavior+MDRectangleFlatButton>
MDFloatLayout:
MagicButton:
text: "WOBBLE EFFECT"
on_release: self.wobble()
pos_hint: {"center_x": .5, "center_y": .3}
MagicButton:
text: "GROW EFFECT"
on_release: self.grow()
pos_hint: {"center_x": .5, "center_y": .4}
MagicButton:
text: "SHAKE EFFECT"
on_release: self.shake()
pos_hint: {"center_x": .5, "center_y": .5}
MagicButton:
text: "TWIST EFFECT"
on_release: self.twist()
pos_hint: {"center_x": .5, "center_y": .6}
MagicButton:
text: "SHRINK EFFECT"
on_release: self.shrink()
pos_hint: {"center_x": .5, "center_y": .7}
'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/magic-button.gif
:width: 250 px
:align: center
"""
__all__ = ("MagicBehavior",)
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.properties import NumericProperty
Builder.load_string(
"""
<MagicBehavior>
translate_x: 0
translate_y: 0
scale_x: 1
scale_y: 1
rotate: 0
canvas.before:
PushMatrix
Translate:
x: self.translate_x or 0
y: self.translate_y or 0
Rotate:
origin: self.center
angle: self.rotate or 0
Scale:
origin: self.center
x: self.scale_x or 1
y: self.scale_y or 1
canvas.after:
PopMatrix
"""
)
class MagicBehavior:
magic_speed = NumericProperty(1)
"""
Animation playback speed.
:attr:`magic_speed` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
def grow(self) -> None:
"""Grow effect animation."""
(
Animation(
scale_x=1.2,
scale_y=1.2,
t="out_quad",
d=0.03 / self.magic_speed,
)
+ Animation(
scale_x=1, scale_y=1, t="out_elastic", d=0.4 / self.magic_speed
)
).start(self)
def shake(self) -> None:
"""Shake effect animation."""
(
Animation(translate_x=50, t="out_quad", d=0.02 / self.magic_speed)
+ Animation(
translate_x=0, t="out_elastic", d=0.5 / self.magic_speed
)
).start(self)
def wobble(self) -> None:
"""Wobble effect animation."""
(
(
Animation(scale_y=0.7, t="out_quad", d=0.03 / self.magic_speed)
& Animation(
scale_x=1.4, t="out_quad", d=0.03 / self.magic_speed
)
)
+ (
Animation(scale_y=1, t="out_elastic", d=0.5 / self.magic_speed)
& Animation(
scale_x=1, t="out_elastic", d=0.4 / self.magic_speed
)
)
).start(self)
def twist(self) -> None:
"""Twist effect animation."""
(
Animation(rotate=25, t="out_quad", d=0.05 / self.magic_speed)
+ Animation(rotate=0, t="out_elastic", d=0.5 / self.magic_speed)
).start(self)
def shrink(self) -> None:
"""Shrink effect animation."""
Animation(
scale_x=0.95, scale_y=0.95, t="out_quad", d=0.1 / self.magic_speed
).start(self)
def on_touch_up(self, *args):
Animation.stop_all(self)
return super().on_touch_up(*args)

View file

@ -0,0 +1,285 @@
"""
Behaviors/Motion
================
.. rubric:: Use motion to make a UI expressive and easy to use.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/motion.png
:align: center
.. versionadded:: 1.2.0
Classes of the `Motion` type implement the display behavior of widgets such
as dialogs, dropdown menu, snack bars, and so on.
"""
__all__ = (
"MotionBase",
"MotionDropDownMenuBehavior",
"MotionDialogBehavior",
"MotionShackBehavior",
)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.properties import StringProperty, NumericProperty
from kivymd.uix.behaviors.stencil_behavior import StencilBehavior
class MotionBase:
"""Base class for widget display movement behavior."""
show_transition = StringProperty("linear")
"""
The type of transition of the widget opening.
:attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'linear'`.
"""
show_duration = NumericProperty(0.2)
"""
Duration of widget display transition.
:attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
hide_transition = StringProperty("linear")
"""
The type of transition of the widget closing.
:attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'linear'`.
"""
hide_duration = NumericProperty(0.2)
"""
Duration of widget closing transition.
:attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
class MotionDropDownMenuBehavior(MotionBase):
"""
Base class for the dropdown menu movement behavior.
For more information, see in the :class:`~MotionBase` class documentation.
"""
show_transition = StringProperty("out_back")
"""
The type of transition of the widget opening.
:attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_back'`.
"""
show_duration = NumericProperty(0.4)
"""
Duration of widget display transition.
:attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
hide_transition = StringProperty("out_cubic")
"""
The type of transition of the widget closing.
:attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_cubic'`.
"""
_scale_x = NumericProperty(None)
"""
Default X-axis scaling values.
:attr:`_scale_x` is a :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
_scale_y = NumericProperty(None)
"""
Default Y-axis scaling values.
:attr:`_scale_y` is a :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
_opacity = NumericProperty(None)
"""
Menu transparency values.
:attr:`_opacity` is a :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_scale()
# self.set_opacity()
def set_opacity(self) -> None:
self._opacity = 0
def set_scale(self) -> None:
self._scale_x = 0
self._scale_y = 0
def on_dismiss(self) -> None:
anim = Animation(
_scale_x=0,
_scale_y=0,
# _opacity=0,
duration=self.hide_duration,
transition=self.hide_transition,
)
anim.bind(on_complete=lambda *args: Window.remove_widget(self))
anim.start(self)
def on_open(self, *args):
anim = Animation(
_scale_y=1,
# _opacity=1,
duration=self.show_duration,
transition=self.show_transition,
)
anim &= Animation(
_scale_x=1,
duration=self.show_duration - 0.3,
transition="out_quad",
)
anim.start(self)
def on__opacity(self, instance, value):
self.opacity = value
def on__scale_x(self, instance, value):
self.scale_value_x = value
def on__scale_y(self, instance, value):
self.scale_value_y = value
class MotionDialogBehavior(MotionBase):
"""
Base class for dialog movement behavior.
For more information, see in the :class:`~MotionBase` class documentation.
"""
show_duration = NumericProperty(0.1)
"""
Duration of widget display transition.
:attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.1`.
"""
scale_x = NumericProperty(1.5)
"""
Default X-axis scaling values.
:attr:`scale_x` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1.5`.
"""
scale_y = NumericProperty(1.5)
"""
Default Y-axis scaling values.
:attr:`scale_y` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1.5`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_default_values()
def set_default_values(self):
"""Sets default scaled and transparency values."""
self.scale_value_x = self.scale_x
self.scale_value_y = self.scale_y
self.opacity = 0
def on_dismiss(self, *args):
"""Called when a dialog closed."""
self.set_default_values()
def on_open(self, *args):
"""Called when a dialog opened."""
Animation(
opacity=1,
scale_value_x=1,
scale_value_y=1,
t=self.show_transition,
d=self.show_duration,
).start(self)
class MotionShackBehavior(StencilBehavior, MotionBase):
"""
The base class for the behavior of the movement of snack bars.
For more information, see in the
:class:`~MotionBase` class and
:class:`~kivy.uix.behaviors.stencil_behavior.StencilBehavior` class
documentation.
"""
_interval = 0
_height = 0
def on_dismiss(self, *args):
"""Called when a snackbar closed."""
def remove_snackbar(*args):
Window.parent.remove_widget(self)
self.height = self._height
self.dispatch("on_dismiss")
Clock.unschedule(self._wait_interval)
anim = Animation(
opacity=0,
height=0,
t=self.hide_transition,
d=self.hide_duration,
)
anim.bind(on_complete=remove_snackbar)
anim.start(self)
def on_open(self, *args):
"""Called when a snackbar opened."""
def open(*args):
self._height = self.height
self.height = 0
anim = Animation(
opacity=1,
height=self._height,
t=self.show_transition,
d=self.show_duration,
)
anim.bind(
on_complete=lambda *args: Clock.schedule_interval(
self._wait_interval, 1
)
)
anim.start(self)
Clock.schedule_once(open)
self.dispatch("on_open")
def _wait_interval(self, interval):
self._interval += interval
if self._interval > self.duration:
self.dismiss()
self._interval = 0

View file

@ -0,0 +1,538 @@
"""
Behaviors/Ripple
================
.. rubric:: Classes implements a circular and rectangular ripple effects.
To create a widget with сircular ripple effect, you must create a new class
that inherits from the :class:`~CircularRippleBehavior` class.
For example, let's create an image button with a circular ripple effect:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.image import Image
from kivymd.app import MDApp
from kivymd.uix.behaviors import CircularRippleBehavior
KV = '''
MDScreen:
CircularRippleButton:
source: "data/logo/kivy-icon-256.png"
size_hint: None, None
size: "250dp", "250dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class CircularRippleButton(CircularRippleBehavior, ButtonBehavior, Image):
def __init__(self, **kwargs):
self.ripple_scale = 0.85
super().__init__(**kwargs)
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-ripple-effect.gif
:align: center
To create a widget with rectangular ripple effect, you must create a new class
that inherits from the :class:`~RectangularRippleBehavior` class:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularRippleBehavior, BackgroundColorBehavior
KV = '''
MDScreen:
RectangularRippleButton:
size_hint: None, None
size: "250dp", "50dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class RectangularRippleButton(
RectangularRippleBehavior, ButtonBehavior, BackgroundColorBehavior
):
md_bg_color = [0, 0, 1, 1]
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-ripple-effect.gif
:align: center
"""
__all__ = (
"CommonRipple",
"RectangularRippleBehavior",
"CircularRippleBehavior",
)
from typing import NoReturn
from kivy.animation import Animation
from kivy.graphics import (
Color,
Ellipse,
StencilPop,
StencilPush,
StencilUnUse,
StencilUse,
)
from kivy.graphics.vertex_instructions import RoundedRectangle
from kivy.properties import (
BooleanProperty,
ColorProperty,
ListProperty,
NumericProperty,
StringProperty,
)
from kivy.uix.behaviors import ToggleButtonBehavior
class CommonRipple:
"""Base class for ripple effect."""
ripple_rad_default = NumericProperty(1)
"""
The starting value of the radius of the ripple effect.
.. code-block:: kv
CircularRippleButton:
ripple_rad_default: 100
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-rad-default.gif
:align: center
:attr:`ripple_rad_default` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
ripple_color = ColorProperty(None)
"""
Ripple color in (r, g, b, a) format.
.. code-block:: kv
CircularRippleButton:
ripple_color: app.theme_cls.primary_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-color.gif
:align: center
:attr:`ripple_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
ripple_alpha = NumericProperty(0.5)
"""
Alpha channel values for ripple effect.
.. code-block:: kv
CircularRippleButton:
ripple_alpha: .9
ripple_color: app.theme_cls.primary_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-alpha.gif
:align: center
:attr:`ripple_alpha` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.5`.
"""
ripple_scale = NumericProperty(None)
"""
Ripple effect scale.
.. code-block:: kv
CircularRippleButton:
ripple_scale: .5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-scale-05.gif
:align: center
.. code-block:: kv
CircularRippleButton:
ripple_scale: 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-scale-1.gif
:align: center
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
ripple_duration_in_fast = NumericProperty(0.3)
"""
Ripple duration when touching to widget.
.. code-block:: kv
CircularRippleButton:
ripple_duration_in_fast: .1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-in-fast.gif
:align: center
:attr:`ripple_duration_in_fast` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
ripple_duration_in_slow = NumericProperty(2)
"""
Ripple duration when long touching to widget.
.. code-block:: kv
CircularRippleButton:
ripple_duration_in_slow: 5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-in-slow.gif
:align: center
:attr:`ripple_duration_in_slow` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2`.
"""
ripple_duration_out = NumericProperty(0.3)
"""
The duration of the disappearance of the wave effect.
.. code-block:: kv
CircularRippleButton:
ripple_duration_out: 5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-out.gif
:align: center
:attr:`ripple_duration_out` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
ripple_canvas_after = BooleanProperty(True)
"""
The ripple effect is drawn above/below the content.
.. versionadded:: 1.0.0
.. code-block:: kv
MDIconButton:
ripple_canvas_after: True
icon: "android"
ripple_alpha: .8
ripple_color: app.theme_cls.primary_color
icon_size: "100sp"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-canvas-after-true.gif
:align: center
.. code-block:: kv
MDIconButton:
ripple_canvas_after: False
icon: "android"
ripple_alpha: .8
ripple_color: app.theme_cls.primary_color
icon_size: "100sp"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-canvas-after-false.gif
:align: center
:attr:`ripple_canvas_after` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
ripple_func_in = StringProperty("out_quad")
"""
Type of animation for ripple in effect.
:attr:`ripple_func_in` is an :class:`~kivy.properties.StringProperty`
and defaults to `'out_quad'`.
"""
ripple_func_out = StringProperty("out_quad")
"""
Type of animation for ripple out effect.
:attr:`ripple_func_out` is an :class:`~kivy.properties.StringProperty`
and defaults to `'ripple_func_out'`.
"""
_ripple_rad = NumericProperty()
_doing_ripple = BooleanProperty(False)
_finishing_ripple = BooleanProperty(False)
_fading_out = BooleanProperty(False)
_no_ripple_effect = BooleanProperty(False)
_round_rad = ListProperty([0, 0, 0, 0])
def lay_canvas_instructions(self) -> NoReturn:
raise NotImplementedError
def start_ripple(self) -> None:
if not self._doing_ripple:
self._doing_ripple = True
anim = Animation(
_ripple_rad=self.finish_rad,
t="linear",
duration=self.ripple_duration_in_slow,
)
anim.bind(on_complete=self.fade_out)
anim.start(self)
def finish_ripple(self) -> None:
if self._doing_ripple and not self._finishing_ripple:
self._finishing_ripple = True
self._doing_ripple = False
Animation.cancel_all(self, "_ripple_rad")
anim = Animation(
_ripple_rad=self.finish_rad,
t=self.ripple_func_in,
duration=self.ripple_duration_in_fast,
)
anim.bind(on_complete=self.fade_out)
anim.start(self)
def fade_out(self, *args) -> None:
rc = self.ripple_color
if not self._fading_out:
self._fading_out = True
Animation.cancel_all(self, "ripple_color")
anim = Animation(
ripple_color=[rc[0], rc[1], rc[2], 0.0],
t=self.ripple_func_out,
duration=self.ripple_duration_out,
)
anim.bind(on_complete=self.anim_complete)
anim.start(self)
def anim_complete(self, *args) -> None:
self._doing_ripple = False
self._finishing_ripple = False
self._fading_out = False
if not self.ripple_canvas_after:
canvas = self.canvas.before
else:
canvas = self.canvas.after
canvas.remove_group("circular_ripple_behavior")
canvas.remove_group("rectangular_ripple_behavior")
def on_touch_down(self, touch):
# FIXME: in fact, the output of the super method is extra.
# But without this, the list (`ScrollView`) placed in the `MDCard`
# widget will not scroll.
super().on_touch_down(touch)
if touch.is_mouse_scrolling:
return False
if not self.collide_point(touch.x, touch.y):
return False
if not self.disabled:
self.call_ripple_animation_methods(touch)
# FIXME: this check is needed for the `MDTabsLabel` object.
# With the normal `return True`, events for tabs from the `MDTabs`
# class are not processed.
# There may be problems with other widgets.
# Status: requires check.
if isinstance(self, ToggleButtonBehavior):
return super().on_touch_down(touch)
else:
return True
def call_ripple_animation_methods(self, touch) -> None:
if self._doing_ripple:
Animation.cancel_all(
self, "_ripple_rad", "ripple_color", "rect_color"
)
self.anim_complete()
self._ripple_rad = self.ripple_rad_default
self.ripple_pos = (touch.x, touch.y)
if self.ripple_color:
pass
elif hasattr(self, "theme_cls"):
self.ripple_color = self.theme_cls.ripple_color
else:
# If no theme, set Gray 300.
self.ripple_color = [
0.8784313725490196,
0.8784313725490196,
0.8784313725490196,
self.ripple_alpha,
]
self.ripple_color[3] = self.ripple_alpha
self.lay_canvas_instructions()
self.finish_rad = max(self.width, self.height) * self.ripple_scale
self.start_ripple()
def on_touch_move(self, touch, *args):
if not self.collide_point(touch.x, touch.y):
if not self._finishing_ripple and self._doing_ripple:
self.finish_ripple()
return super().on_touch_move(touch, *args)
def on_touch_up(self, touch):
if self.collide_point(touch.x, touch.y) and self._doing_ripple:
self.finish_ripple()
return super().on_touch_up(touch)
def _set_ellipse(self, instance, value):
self.ellipse.size = (self._ripple_rad, self._ripple_rad)
# Adjust ellipse pos here
def _set_color(self, instance, value):
self.col_instruction.a = value[3]
class RectangularRippleBehavior(CommonRipple):
"""
Class implements a rectangular ripple effect.
For more information, see in the :class:`~kivymd.uix.behavior.CommonRipple`
class documentation.
"""
ripple_scale = NumericProperty(2.75)
"""
See :class:`~CommonRipple.ripple_scale`.
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2.75`.
"""
def lay_canvas_instructions(self) -> None:
if self._no_ripple_effect:
return
with self.canvas.after if self.ripple_canvas_after else self.canvas.before:
if hasattr(self, "radius"):
if isinstance(self.radius, (float, int)):
self.radius = [
self.radius,
]
self._round_rad = self.radius
StencilPush(group="rectangular_ripple_behavior")
RoundedRectangle(
pos=self.pos,
size=self.size,
radius=self._round_rad,
group="rectangular_ripple_behavior",
)
StencilUse(group="rectangular_ripple_behavior")
self.col_instruction = Color(
rgba=self.ripple_color, group="rectangular_ripple_behavior"
)
self.ellipse = Ellipse(
size=(self._ripple_rad, self._ripple_rad),
pos=(
self.ripple_pos[0] - self._ripple_rad / 2.0,
self.ripple_pos[1] - self._ripple_rad / 2.0,
),
group="rectangular_ripple_behavior",
)
StencilUnUse(group="rectangular_ripple_behavior")
RoundedRectangle(
pos=self.pos,
size=self.size,
radius=self._round_rad,
group="rectangular_ripple_behavior",
)
StencilPop(group="rectangular_ripple_behavior")
self.bind(ripple_color=self._set_color, _ripple_rad=self._set_ellipse)
def _set_ellipse(self, instance, value):
super()._set_ellipse(instance, value)
self.ellipse.pos = (
self.ripple_pos[0] - self._ripple_rad / 2.0,
self.ripple_pos[1] - self._ripple_rad / 2.0,
)
class CircularRippleBehavior(CommonRipple):
"""
Class implements a circular ripple effect.
For more information, see in the :class:`~kivymd.uix.behavior.CommonRipple`
class documentation.
"""
ripple_scale = NumericProperty(1)
"""
See :class:`~CommonRipple.ripple_scale`.
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
def lay_canvas_instructions(self) -> None:
if self._no_ripple_effect:
return
with self.canvas.after if self.ripple_canvas_after else self.canvas.before:
StencilPush(group="circular_ripple_behavior")
self.stencil = Ellipse(
size=(
self.width * self.ripple_scale,
self.height * self.ripple_scale,
),
pos=(
self.center_x - (self.width * self.ripple_scale) / 2,
self.center_y - (self.height * self.ripple_scale) / 2,
),
group="circular_ripple_behavior",
)
StencilUse(group="circular_ripple_behavior")
self.col_instruction = Color(rgba=self.ripple_color)
self.ellipse = Ellipse(
size=(self._ripple_rad, self._ripple_rad),
pos=(
self.center_x - self._ripple_rad / 2.0,
self.center_y - self._ripple_rad / 2.0,
),
group="circular_ripple_behavior",
)
StencilUnUse(group="circular_ripple_behavior")
Ellipse(
pos=self.pos, size=self.size, group="circular_ripple_behavior"
)
StencilPop(group="circular_ripple_behavior")
self.bind(
ripple_color=self._set_color, _ripple_rad=self._set_ellipse
)
def _set_ellipse(self, instance, value):
super()._set_ellipse(instance, value)
if self.ellipse.size[0] > self.width * 0.6 and not self._fading_out:
self.fade_out()
self.ellipse.pos = (
self.center_x - self._ripple_rad / 2.0,
self.center_y - self._ripple_rad / 2.0,
)

View file

@ -0,0 +1,137 @@
"""
Behaviors/Rotate
================
.. versionadded:: 1.1.0
Base class for controlling the rotate of the widget.
.. note:: See `kivy.graphics.Rotate
<https://kivy.org/doc/stable/api-kivy.graphics.html#kivy.graphics.Rotate>`_
for more information.
Kivy
----
.. code-block:: python
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.app import App
from kivy.properties import NumericProperty
from kivy.uix.button import Button
KV = '''
Screen:
RotateButton:
size_hint: .5, .5
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.change_rotate(self)
canvas.before:
PushMatrix
Rotate:
angle: self.rotate_value_angle
axis: 0, 0, 1
origin: self.center
canvas.after:
PopMatrix
'''
class RotateButton(Button):
rotate_value_angle = NumericProperty(0)
class Test(App):
def build(self):
return Builder.load_string(KV)
def change_rotate(self, instance_button: Button) -> None:
Animation(rotate_value_angle=45, d=0.3).start(instance_button)
Test().run()
KivyMD
------
.. code-block:: python
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import RotateBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
MDScreen:
RotateBox:
size_hint: .5, .5
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.change_rotate(self)
md_bg_color: "red"
'''
class RotateBox(ButtonBehavior, RotateBehavior, MDBoxLayout):
pass
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
def change_rotate(self, instance_button: RotateBox) -> None:
Animation(rotate_value_angle=45, d=0.3).start(instance_button)
Test().run()
.. warning:: Do not use `RotateBehavior` class with classes that inherited`
from `CommonElevationBehavior` class. `CommonElevationBehavior` classes
by default contains attributes for rotate widget.
"""
__all__ = ("RotateBehavior",)
from kivy.lang import Builder
from kivy.properties import ListProperty, NumericProperty
Builder.load_string(
"""
<RotateBehavior>
canvas.before:
PushMatrix
Rotate:
angle: self.rotate_value_angle
axis: tuple(self.rotate_value_axis)
origin: self.center
canvas.after:
PopMatrix
"""
)
class RotateBehavior:
"""Base class for controlling the rotate of the widget."""
rotate_value_angle = NumericProperty(0)
"""
Property for getting/setting the angle of the rotation.
:attr:`rotate_value_angle` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
rotate_value_axis = ListProperty((0, 0, 1))
"""
Property for getting/setting the axis of the rotation.
:attr:`rotate_value_axis` is an :class:`~kivy.properties.ListProperty`
and defaults to `(0, 0, 1)`.
"""

View file

@ -0,0 +1,175 @@
"""
Behaviors/Scale
===============
.. versionadded:: 1.1.0
Base class for controlling the scale of the widget.
.. note:: See `kivy.graphics.Rotate
<https://kivy.org/doc/stable/api-kivy.graphics.html#kivy.graphics.Scale>`_
for more information.
Kivy
----
.. code-block:: python
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.properties import NumericProperty
from kivy.uix.button import Button
from kivy.app import App
KV = '''
Screen:
ScaleButton:
size_hint: .5, .5
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.change_scale(self)
canvas.before:
PushMatrix
Scale:
x: self.scale_value_x
y: self.scale_value_y
z: self.scale_value_x
origin: self.center
canvas.after:
PopMatrix
'''
class ScaleButton(Button):
scale_value_x = NumericProperty(1)
scale_value_y = NumericProperty(1)
scale_value_z = NumericProperty(1)
class Test(App):
def build(self):
return Builder.load_string(KV)
def change_scale(self, instance_button: Button) -> None:
Animation(
scale_value_x=0.5,
scale_value_y=0.5,
scale_value_z=0.5,
d=0.3,
).start(instance_button)
Test().run()
KivyMD
------
.. code-block:: python
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import ScaleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
MDScreen:
ScaleBox:
size_hint: .5, .5
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.change_scale(self)
md_bg_color: "red"
'''
class ScaleBox(ButtonBehavior, ScaleBehavior, MDBoxLayout):
pass
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
def change_scale(self, instance_button: ScaleBox) -> None:
Animation(
scale_value_x=0.5,
scale_value_y=0.5,
scale_value_z=0.5,
d=0.3,
).start(instance_button)
Test().run()
.. warning:: Do not use `ScaleBehavior` class with classes that inherited`
from `CommonElevationBehavior` class. `CommonElevationBehavior` classes
by default contains attributes for scale widget.
"""
__all__ = ("ScaleBehavior",)
from kivy.lang import Builder
from kivy.properties import ListProperty, NumericProperty
Builder.load_string(
"""
<ScaleBehavior>
canvas.before:
PushMatrix
Scale:
x: self.scale_value_x
y: self.scale_value_y
z: self.scale_value_z
origin:
self.center \
if not self.scale_value_center else \
self.scale_value_center
canvas.after:
PopMatrix
"""
)
class ScaleBehavior:
"""Base class for controlling the scale of the widget."""
scale_value_x = NumericProperty(1)
"""
X-axis value.
:attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
scale_value_y = NumericProperty(1)
"""
Y-axis value.
:attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
scale_value_z = NumericProperty(1)
"""
Z-axis value.
:attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
scale_value_center = ListProperty()
"""
Origin of the scale.
.. versionadded:: 1.2.0
The format of the origin can be either (x, y) or (x, y, z).
:attr:`scale_value_center` is an :class:`~kivy.properties.NumericProperty`
and defaults to `[]`.
"""

View file

@ -0,0 +1,134 @@
"""
Behaviors/Stencil
=================
.. versionadded:: 1.1.0
Base class for controlling the stencil instructions of the widget.
.. note:: See `Stencil instructions
<https://kivy.org/doc/stable/api-kivy.graphics.stencil_instructions.html>`_
for more information.
Kivy
----
.. code-block:: python
from kivy.lang import Builder
from kivy.app import App
KV = '''
Carousel:
Button:
size_hint: .9, .8
pos_hint: {"center_x": .5, "center_y": .5}
canvas.before:
StencilPush
RoundedRectangle:
pos: root.pos
size: root.size
StencilUse
canvas.after:
StencilUnUse
RoundedRectangle:
pos: root.pos
size: root.size
StencilPop
'''
class Test(App):
def build(self):
return Builder.load_string(KV)
Test().run()
KivyMD
------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import StencilBehavior
from kivymd.uix.fitimage import FitImage
KV = '''
#:import os os
#:import images_path kivymd.images_path
MDCarousel:
StencilImage:
size_hint: .9, .8
pos_hint: {"center_x": .5, "center_y": .5}
source: os.path.join(images_path, "logo", "kivymd-icon-512.png")
'''
class StencilImage(FitImage, StencilBehavior):
pass
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
Test().run()
"""
__all__ = ("StencilBehavior",)
from kivy.lang import Builder
from kivy.properties import VariableListProperty
Builder.load_string(
"""
<StencilBehavior>
canvas.before:
StencilPush
RoundedRectangle:
pos: root.pos
size: root.size
# FIXME: Sometimes the radius has the value [], which get a
# `GraphicException: Invalid radius value, must be list of tuples/numerics` error
radius: root.radius if root.radius else [0, 0, 0, 0]
StencilUse
canvas.after:
StencilUnUse
RoundedRectangle:
pos: root.pos
size: root.size
# FIXME: Sometimes the radius has the value [], which get a
# `GraphicException: Invalid radius value, must be list of tuples/numerics` error
radius: root.radius if root.radius else [0, 0, 0, 0]
StencilPop
"""
)
class StencilBehavior:
"""Base class for controlling the stencil instructions of the widget."""
radius = VariableListProperty([0], length=4)
"""
Canvas radius.
.. versionadded:: 1.0.0
.. code-block:: python
# Top left corner slice.
MDWidget:
radius: [25, 0, 0, 0]
:attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
and defaults to `[0, 0, 0, 0]`.
"""

View file

@ -0,0 +1,252 @@
"""
Behaviors/ToggleButton
======================
This behavior must always be inherited after the button's Widget class since it
works with the inherited properties of the button class.
example:
.. code-block:: python
class MyToggleButtonWidget(MDFlatButton, MDToggleButton):
# [...]
pass
.. tabs::
.. tab:: Declarative KV style
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors.toggle_behavior import MDToggleButton
from kivymd.uix.button import MDFlatButton
KV = '''
MDScreen:
MDBoxLayout:
adaptive_size: True
spacing: "12dp"
pos_hint: {"center_x": .5, "center_y": .5}
MyToggleButton:
text: "Show ads"
group: "x"
MyToggleButton:
text: "Do not show ads"
group: "x"
MyToggleButton:
text: "Does not matter"
group: "x"
'''
class MyToggleButton(MDFlatButton, MDToggleButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.background_down = self.theme_cls.primary_color
class Test(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
Test().run()
.. tab:: Declarative python style
.. code-block:: python
from kivymd.app import MDApp
from kivymd.uix.behaviors.toggle_behavior import MDToggleButton
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDFlatButton
from kivymd.uix.screen import MDScreen
class MyToggleButton(MDFlatButton, MDToggleButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.background_down = self.theme_cls.primary_color
class Test(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return (
MDScreen(
MDBoxLayout(
MyToggleButton(
text="Show ads",
group="x",
),
MyToggleButton(
text="Do not show ads",
group="x",
),
MyToggleButton(
text="Does not matter",
group="x",
),
adaptive_size=True,
spacing="12dp",
pos_hint={"center_x": .5, "center_y": .5},
),
)
)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toggle-button-1.gif
:align: center
You can inherit the ``MyToggleButton`` class only from the following classes
----------------------------------------------------------------------------
- :class:`~kivymd.uix.button.MDRaisedButton`
- :class:`~kivymd.uix.button.MDFlatButton`
- :class:`~kivymd.uix.button.MDRectangleFlatButton`
- :class:`~kivymd.uix.button.MDRectangleFlatIconButton`
- :class:`~kivymd.uix.button.MDRoundFlatButton`
- :class:`~kivymd.uix.button.MDRoundFlatIconButton`
- :class:`~kivymd.uix.button.MDFillRoundFlatButton`
- :class:`~kivymd.uix.button.MDFillRoundFlatIconButton`
"""
__all__ = ("MDToggleButton",)
from kivy.properties import BooleanProperty, ColorProperty
from kivy.uix.behaviors import ToggleButtonBehavior
from kivymd.uix.button import (
ButtonContentsIconText,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
MDFlatButton,
MDRaisedButton,
MDRectangleFlatButton,
MDRectangleFlatIconButton,
MDRoundFlatButton,
MDRoundFlatIconButton,
)
class MDToggleButton(ToggleButtonBehavior):
background_normal = ColorProperty(None)
"""
Color of the button in ``rgba`` format for the 'normal' state.
:attr:`background_normal` is a :class:`~kivy.properties.ColorProperty`
and is defaults to `None`.
"""
background_down = ColorProperty(None)
"""
Color of the button in ``rgba`` format for the 'down' state.
:attr:`background_down` is a :class:`~kivy.properties.ColorProperty`
and is defaults to `None`.
"""
font_color_normal = ColorProperty(None)
"""
Color of the font's button in ``rgba`` format for the 'normal' state.
:attr:`font_color_normal` is a :class:`~kivy.properties.ColorProperty`
and is defaults to `None`.
"""
font_color_down = ColorProperty([1, 1, 1, 1])
"""
Color of the font's button in ``rgba`` format for the 'down' state.
:attr:`font_color_down` is a :class:`~kivy.properties.ColorProperty`
and is defaults to `[1, 1, 1, 1]`.
"""
__is_filled = BooleanProperty(False)
def __init__(self, **kwargs):
super().__init__(**kwargs)
classinfo = (
MDRaisedButton,
MDFlatButton,
MDRectangleFlatButton,
MDRectangleFlatIconButton,
MDRoundFlatButton,
MDRoundFlatIconButton,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
)
# Do the object inherited from the "supported" buttons?
if not issubclass(self.__class__, classinfo):
raise ValueError(
f"Class {self.__class__} must be inherited from one of the "
f"classes in the list {classinfo}"
)
if (
not self.background_normal
): # This means that if the value == [] or None will return True.
# If the object inherits from buttons with background:
if isinstance(
self,
(
MDRaisedButton,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
),
):
self.__is_filled = True
self.background_normal = self.theme_cls.primary_color
# If not background_normal must be the same as the inherited one.
else:
self.background_normal = (
self.md_bg_color[:] if self.md_bg_color else (0, 0, 0, 0)
)
# If no background_down is setter.
if (
not self.background_down
): # This means that if the value == [] or None will return True.
self.background_down = (
self.theme_cls.primary_dark
) # get the primary_color dark from theme_cls
if not self.font_color_normal:
self.font_color_normal = self.theme_cls.primary_color
# Alternative to bind the function to the property.
# self.bind(state=self._update_bg)
self.fbind("state", self._update_bg)
def _update_bg(self, ins, val):
"""Updates the color of the background."""
if val == "down":
self.md_bg_color = self.background_down
if (
self.__is_filled is False
): # If the background is transparent, and the button it toggled,
# the font color must be withe [1, 1, 1, 1].
self.text_color = self.font_color_down
if self.group:
self._release_group(self)
else:
self.md_bg_color = self.background_normal
if (
self.__is_filled is False
): # If the background is transparent, the font color must be the
# primary color.
self.text_color = self.font_color_normal
if issubclass(self.__class__, ButtonContentsIconText):
self.icon_color = self.text_color

View file

@ -0,0 +1,100 @@
"""
Behaviors/Touch
===============
.. rubric:: Provides easy access to events.
The following events are available:
- on_long_touch
- on_double_tap
- on_triple_tap
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import TouchBehavior
from kivymd.uix.button import MDRaisedButton
KV = '''
MDScreen:
MyButton:
text: "PRESS ME"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class MyButton(MDRaisedButton, TouchBehavior):
def on_long_touch(self, *args):
print("<on_long_touch> event")
def on_double_tap(self, *args):
print("<on_double_tap> event")
def on_triple_tap(self, *args):
print("<on_triple_tap> event")
class MainApp(MDApp):
def build(self):
return Builder.load_string(KV)
MainApp().run()
"""
__all__ = ("TouchBehavior",)
from functools import partial
from kivy.clock import Clock
from kivy.properties import NumericProperty
class TouchBehavior:
duration_long_touch = NumericProperty(0.4)
"""
Time for a long touch.
:attr:`duration_long_touch` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.4`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
on_touch_down=self.create_clock, on_touch_up=self.delete_clock
)
def create_clock(self, widget, touch, *args):
if self.collide_point(touch.x, touch.y):
if "event" not in touch.ud:
callback = partial(self.on_long_touch, touch)
Clock.schedule_once(callback, self.duration_long_touch)
touch.ud["event"] = callback
if touch.is_double_tap:
self.on_double_tap(touch, *args)
if touch.is_triple_tap:
self.on_triple_tap(touch, *args)
def delete_clock(self, widget, touch, *args):
if self.collide_point(touch.x, touch.y):
if "event" in touch.ud:
Clock.unschedule(touch.ud["event"])
del touch.ud["event"]
def on_long_touch(self, touch, *args):
"""Called when the widget is pressed for a long time."""
def on_double_tap(self, touch, *args):
"""Called by double clicking on the widget."""
def on_triple_tap(self, touch, *args):
"""Called by triple clicking on the widget."""

View file

@ -0,0 +1,2 @@
# NOQA F401
from .bottomnavigation import MDBottomNavigation, MDBottomNavigationItem

View file

@ -0,0 +1,118 @@
#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT
<MDBottomNavigation>
orientation: "vertical"
height:
STANDARD_INCREMENT if app.theme_cls.material_style == "M2" else "80dp"
ScreenManager:
id: tab_manager
transition: root.transition(duration=root.transition_duration)
on_current:
root.dispatch( \
"on_switch_tabs", \
root._get_switchig_tab(self.current), \
self.current \
)
MDBottomNavigationBar:
id: bottom_panel
size_hint_y: None
radius: root.radius
height:
STANDARD_INCREMENT \
if app.theme_cls.material_style == "M2" else \
"80dp"
md_bg_color:
root.theme_cls.bg_dark \
if not root.panel_color \
else root.panel_color
MDBoxLayout:
id: tab_bar
pos_hint: {"center_x": .5, "center_y": .5}
size_hint: None, None
height:
STANDARD_INCREMENT \
if app.theme_cls.material_style == "M2" else \
"80dp"
<MDBottomNavigationHeader>
md_bg_color: root.panel_color
on_press: self.tab.dispatch("on_tab_press")
on_release: self.tab.dispatch("on_tab_release")
on_touch_down: self.tab.dispatch("on_tab_touch_down", *args)
on_touch_move: self.tab.dispatch("on_tab_touch_move", *args)
on_touch_up: self.tab.dispatch("on_tab_touch_up", *args)
width:
root.panel.width / len(root.panel.ids.tab_manager.screens) \
if len(root.panel.ids.tab_manager.screens) != 0 \
else root.panel.width
padding:
0, "12dp", 0, "12dp" if app.theme_cls.material_style == "M2" else "16dp"
RelativeLayout:
id: item_container
MDIcon:
id: _label_icon
icon: root.tab.icon
height: self.height
badge_icon: root.tab.badge_icon
theme_text_color: "Custom"
text_color: root._text_color_normal
opposite_colors: root.opposite_colors
pos: [self.pos[0], self.pos[1]]
font_size: "24dp"
y: item_container.height - self.height
pos_hint:
{"center_x": .5, "center_y": .5} \
if not root.panel.use_text else \
{"center_x": .5, "top": 1}
on_icon:
if self.icon not in md_icons.keys(): \
self.size_hint = (None, None); \
self.width = self.font_size; \
self.height = self.font_size
canvas.before:
Color:
rgba:
( \
( \
app.theme_cls.disabled_hint_text_color \
if not root.selected_color_background else \
root.selected_color_background \
) \
if root.active else \
(0, 0, 0, 0) \
) \
if app.theme_cls.material_style == "M3" else \
(0, 0, 0, 0)
RoundedRectangle:
radius: [16,]
size: root._selected_region_width, dp(32)
pos:
self.center_x - root._selected_region_width / 2, \
self.center_y - (dp(16))
MDLabel:
id: _label
text: root.tab.text
size_hint_x: None
text_size: None, root.height
adaptive_height: True
theme_text_color: "Custom"
text_color: root._text_color_normal
opposite_colors: root.opposite_colors
font_size: root._label_font_size
pos_hint: {"center_x": .5}
y: -dp(4) if app.theme_cls.material_style == "M2" else 0
font_style:
"Button" if app.theme_cls.material_style == "M2" else "Body2"
<MDTab>
md_bg_color: root.theme_cls.bg_normal

View file

@ -0,0 +1,880 @@
"""
Components/BottomNavigation
===========================
.. seealso::
`Material Design 2 spec, Bottom navigation <https://material.io/components/bottom-navigation>`_ and
`Material Design 3 spec, Bottom navigation <https://m3.material.io/components/navigation-bar/overview>`_
.. rubric:: Bottom navigation bars allow movement between primary destinations in an app:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation.png
:align: center
Usage
-----
.. code-block:: kv
<Root>
MDBottomNavigation:
MDBottomNavigationItem:
name: "screen 1"
YourContent:
MDBottomNavigationItem:
name: "screen 2"
YourContent:
MDBottomNavigationItem:
name: "screen 3"
YourContent:
For ease of understanding, this code works like this:
.. code-block:: kv
<Root>
ScreenManager:
Screen:
name: "screen 1"
YourContent:
Screen:
name: "screen 2"
YourContent:
Screen:
name: "screen 3"
YourContent:
Example
-------
.. tabs::
.. tab:: Declarative KV style
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
class Test(MDApp):
def build(self):
self.theme_cls.material_style = "M3"
self.theme_cls.theme_style = "Dark"
return Builder.load_string(
'''
MDScreen:
MDBottomNavigation:
#panel_color: "#eeeaea"
selected_color_background: "orange"
text_color_active: "lightgrey"
MDBottomNavigationItem:
name: 'screen 1'
text: 'Mail'
icon: 'gmail'
badge_icon: "numeric-10"
MDLabel:
text: 'Mail'
halign: 'center'
MDBottomNavigationItem:
name: 'screen 2'
text: 'Twitter'
icon: 'twitter'
badge_icon: "numeric-5"
MDLabel:
text: 'Twitter'
halign: 'center'
MDBottomNavigationItem:
name: 'screen 3'
text: 'LinkedIN'
icon: 'linkedin'
MDLabel:
text: 'LinkedIN'
halign: 'center'
'''
)
Test().run()
.. tab:: Declarative python style
.. code-block:: python
from kivymd.app import MDApp
from kivymd.uix.bottomnavigation import MDBottomNavigation, MDBottomNavigationItem
from kivymd.uix.label import MDLabel
from kivymd.uix.screen import MDScreen
class Test(MDApp):
def build(self):
self.theme_cls.material_style = "M3"
self.theme_cls.theme_style = "Dark"
return (
MDScreen(
MDBottomNavigation(
MDBottomNavigationItem(
MDLabel(
text='Mail',
halign='center',
),
name='screen 1',
text='Mail',
icon='gmail',
badge_icon="numeric-10",
),
MDBottomNavigationItem(
MDLabel(
text='Twitter',
halign='center',
),
name='screen 1',
text='Twitter',
icon='twitter',
badge_icon="numeric-10",
),
MDBottomNavigationItem(
MDLabel(
text='LinkedIN',
halign='center',
),
name='screen 1',
text='LinkedIN',
icon='linkedin',
badge_icon="numeric-10",
),
selected_color_background="orange",
text_color_active="lightgrey",
)
)
)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation.gif
:align: center
.. rubric:: :class:`~MDBottomNavigationItem` provides the following events for use:
.. code-block:: python
__events__ = (
"on_tab_touch_down",
"on_tab_touch_move",
"on_tab_touch_up",
"on_tab_press",
"on_tab_release",
)
.. code-block:: kv
Root:
MDBottomNavigation:
MDBottomNavigationItem:
on_tab_touch_down: print("on_tab_touch_down")
on_tab_touch_move: print("on_tab_touch_move")
on_tab_touch_up: print("on_tab_touch_up")
on_tab_press: print("on_tab_press")
on_tab_release: print("on_tab_release")
YourContent:
How to automatically switch a tab?
----------------------------------
Use method :attr:`~MDBottomNavigation.switch_tab` which takes as argument
the name of the tab you want to switch to.
Use custom icon
---------------
.. code-block:: kv
MDBottomNavigation:
MDBottomNavigationItem:
icon: "icon.png"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-custom-icon.png
:align: center
"""
__all__ = (
"TabbedPanelBase",
"MDBottomNavigationItem",
"MDBottomNavigation",
"MDTab",
)
import os
from typing import Union
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.core.window.window_sdl2 import WindowSDL
from kivy.lang import Builder
from kivy.metrics import dp, sp
from kivy.properties import (
BooleanProperty,
ColorProperty,
ListProperty,
NumericProperty,
ObjectProperty,
StringProperty,
)
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import FadeTransition, ScreenManagerException
from kivymd import uix_path
from kivymd.material_resources import STANDARD_INCREMENT
from kivymd.theming import ThemableBehavior, ThemeManager
from kivymd.uix.anchorlayout import MDAnchorLayout
from kivymd.uix.behaviors import CommonElevationBehavior, DeclarativeBehavior
from kivymd.uix.behaviors.backgroundcolor_behavior import (
SpecificBackgroundColorBehavior,
)
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.screen import MDScreen
from kivymd.utils.set_bars_colors import set_bars_colors
with open(
os.path.join(uix_path, "bottomnavigation", "bottomnavigation.kv"),
encoding="utf-8",
) as kv_file:
Builder.load_string(kv_file.read())
class MDBottomNavigationHeader(ButtonBehavior, MDAnchorLayout):
"""
Bottom navigation header class.
For more information, see in the
:class:`~kivy.uix.behaviors.ButtonBehavior` and
:class:`~kivymd.uix.anchorlayout.MDAnchorLayout`
classes documentation.
"""
panel_color = ColorProperty([1, 1, 1, 0])
"""
Panel color of bottom navigation in (r, g, b, a) or string format.
:attr:`panel_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 0]`.
"""
tab = ObjectProperty()
"""
:attr:`tab` is an :class:`~MDBottomNavigationItem`
and defaults to `None`.
"""
panel = ObjectProperty()
"""
:attr:`panel` is an :class:`~MDBottomNavigation`
and defaults to `None`.
"""
active = BooleanProperty(False)
text = StringProperty()
"""
:attr:`text` is an :class:`~MDTab.text`
and defaults to `''`.
"""
text_color_normal = ColorProperty([1, 1, 1, 1])
"""
Text color in (r, g, b, a) or string format of the label when it is not
selected.
:attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
"""
text_color_active = ColorProperty([1, 1, 1, 1])
"""
Text color in (r, g, b, a) or string format of the label when it is selected.
:attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
"""
selected_color_background = ColorProperty(None)
"""
The background color in (r, g, b, a) or string format of the highlighted
item when using Material Design v3.
.. versionadded:: 1.0.0
:attr:`selected_color_background` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
opposite_colors = BooleanProperty(True)
_label = ObjectProperty()
_label_font_size = NumericProperty("12sp")
_text_color_normal = ColorProperty([1, 1, 1, 1])
_text_color_active = ColorProperty([1, 1, 1, 1])
_selected_region_width = NumericProperty(dp(64))
def __init__(self, panel, tab):
self.panel = panel
self.tab = tab
super().__init__()
self._text_color_normal = (
self.theme_cls.disabled_hint_text_color
if self.text_color_normal == [1, 1, 1, 1]
else self.text_color_normal
)
self._label = self.ids._label
self._label_font_size = sp(12)
self.theme_cls.bind(disabled_hint_text_color=self._update_theme_style)
self.active = False
def on_press(self) -> None:
"""Called when clicking on a panel item."""
if self.theme_cls.material_style == "M2":
Animation(_label_font_size=sp(14), d=0.1).start(self)
elif self.theme_cls.material_style == "M3":
Animation(
_selected_region_width=dp(64),
t="in_out_sine",
d=0,
).start(self)
Animation(
_text_color_normal=self.theme_cls.primary_color
if self.text_color_active == [1, 1, 1, 1]
else self.text_color_active,
d=0.1,
).start(self)
def _update_theme_style(
self, instance_theme_manager: ThemeManager, color: list
):
"""Called when the application theme style changes (White/Black)."""
if not self.active:
self._text_color_normal = (
color
if self.text_color_normal == [1, 1, 1, 1]
else self.text_color_normal
)
class MDTab(MDScreen):
"""
A tab is simply a screen with meta information that defines the content
that goes in the tab header.
For more information, see in the
:class:`~kivymd.uix.screen.MDScreen` class documentation.
"""
__events__ = (
"on_tab_touch_down",
"on_tab_touch_move",
"on_tab_touch_up",
"on_tab_press",
"on_tab_release",
)
"""Events provided."""
text = StringProperty()
"""
Tab header text.
:attr:`text` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
icon = StringProperty("checkbox-blank-circle")
"""
Tab header icon.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'checkbox-blank-circle'`.
"""
badge_icon = StringProperty()
"""
Tab header badge icon.
.. versionadded:: 1.0.0
:attr:`badge_icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.index = 0
self.parent_widget = None
self.register_event_type("on_tab_touch_down")
self.register_event_type("on_tab_touch_move")
self.register_event_type("on_tab_touch_up")
self.register_event_type("on_tab_press")
self.register_event_type("on_tab_release")
def on_tab_touch_down(self, *args):
pass
def on_tab_touch_move(self, *args):
pass
def on_tab_touch_up(self, *args):
pass
def on_tab_press(self, *args):
par = self.parent_widget
if par.previous_tab is not self:
if par.previous_tab.index > self.index:
par.ids.tab_manager.transition.direction = "right"
elif par.previous_tab.index < self.index:
par.ids.tab_manager.transition.direction = "left"
par.ids.tab_manager.current = self.name
par.previous_tab = self
def on_tab_release(self, *args):
pass
def __repr__(self):
return f"<MDTab name='{self.name}', text='{self.text}'>"
class MDBottomNavigationItem(MDTab):
header = ObjectProperty()
"""
:attr:`header` is an :class:`~MDBottomNavigationHeader`
and defaults to `None`.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def animate_header(
self, bottom_navigation_object, bottom_navigation_header_object
) -> None:
if bottom_navigation_object.use_text:
Animation(_label_font_size=sp(12), d=0.1).start(
bottom_navigation_object.previous_tab.header
)
Animation(
_selected_region_width=0,
t="in_out_sine",
d=0,
).start(bottom_navigation_header_object)
Animation(
_text_color_normal=bottom_navigation_header_object.text_color_normal
if bottom_navigation_object.previous_tab.header.text_color_normal
!= [1, 1, 1, 1]
else self.theme_cls.disabled_hint_text_color,
d=0.1,
).start(bottom_navigation_object.previous_tab.header)
bottom_navigation_object.previous_tab.header.active = False
self.header.active = True
def on_tab_press(self, *args) -> None:
"""Called when clicking on a panel item."""
bottom_navigation_object = self.parent_widget
bottom_navigation_header_object = (
bottom_navigation_object.previous_tab.header
)
if bottom_navigation_object.previous_tab is not self:
self.animate_header(
bottom_navigation_object, bottom_navigation_header_object
)
super().on_tab_press(*args)
def on_disabled(
self, instance_bottom_navigation_item, disabled_value: bool
) -> None:
self.header.disabled = disabled_value
def on_leave(self, *args):
pass
class TabbedPanelBase(
ThemableBehavior, SpecificBackgroundColorBehavior, BoxLayout
):
"""
A class that contains all variables a :class:`~kivy.properties.TabPannel`
must have. It is here so I (zingballyhoo) don't get mad about
the :class:`~kivy.properties.TabbedPannels` not being DRY.
For more information, see in the :class:`~kivymd.theming.ThemableBehavior`
and :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior`
and :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation.
"""
current = StringProperty(None)
"""
Current tab name.
:attr:`current` is an :class:`~kivy.properties.StringProperty`
and defaults to `None`.
"""
previous_tab = ObjectProperty(None, aloownone=True)
"""
:attr:`previous_tab` is an :class:`~MDTab` and defaults to `None`.
"""
panel_color = ColorProperty(None)
"""
Panel color of bottom navigation.
:attr:`panel_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
tabs = ListProperty()
class MDBottomNavigation(DeclarativeBehavior, TabbedPanelBase):
"""
A bottom navigation that is implemented by delegating all items to a
:class:`~kivy.uix.screenmanager.ScreenManager`.
For more information, see in the
:class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
:class:`~TabbedPanelBase` classes documentation.
:Events:
:attr:`on_switch_tabs`
Called when switching tabs. Returns the object of the tab to be
opened.
.. versionadded:: 1.0.0
"""
transition = ObjectProperty(FadeTransition)
"""
Transition animation of bottom navigation screen manager.
.. versionadded:: 1.1.0
:attr:`transition` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `FadeTransition`.
"""
transition_duration = NumericProperty(0.2)
"""
Duration animation of bottom navigation screen manager.
.. versionadded:: 1.1.0
:attr:`transition_duration` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
text_color_normal = ColorProperty([1, 1, 1, 1])
"""
Text color of the label when it is not selected.
.. code-block:: kv
MDBottomNavigation:
text_color_normal: 1, 0, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-text_color_normal.png
:attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
"""
text_color_active = ColorProperty([1, 1, 1, 1])
"""
Text color of the label when it is selected.
.. code-block:: kv
MDBottomNavigation:
text_color_active: 0, 0, 0, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-text_color_active.png
:attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
"""
use_text = BooleanProperty(True)
"""
Use text for :class:`~MDBottomNavigationItem` or not.
If ``True``, the :class:`~MDBottomNavigation` panel height will be reduced
by the text height.
.. versionadded:: 1.0.0
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-use-text.png
:align: center
:attr:`use_text` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
selected_color_background = ColorProperty(None)
"""
The background color of the highlighted item when using Material Design v3.
.. versionadded:: 1.0.0
.. code-block:: kv
MDBottomNavigation:
selected_color_background: 0, 0, 1, .4
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation=selected-color-background.png
:attr:`selected_color_background` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
font_name = StringProperty("Roboto")
"""
Font name of the label.
.. versionadded:: 1.0.0
.. code-block:: kv
MDBottomNavigation:
font_name: "path/to/font.ttf"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-font-name.png
:attr:`font_name` is an :class:`~kivy.properties.StringProperty`
and defaults to `'Roboto'`.
"""
first_widget = ObjectProperty()
"""
:attr:`first_widget` is an :class:`~MDBottomNavigationItem`
and defaults to `None`.
"""
tab_header = ObjectProperty()
"""
:attr:`tab_header` is an :class:`~MDBottomNavigationHeader`
and defaults to `None`.
"""
set_bars_color = BooleanProperty(False)
"""
If `True` the background color of the navigation bar will be set
automatically according to the current color of the toolbar.
.. versionadded:: 1.0.0
:attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
widget_index = NumericProperty(0)
# Text active color if it is selected.
_active_color = ColorProperty([1, 1, 1, 1])
def __init__(self, *args, **kwargs):
self.previous_tab = None
self.register_event_type("on_switch_tabs")
super().__init__(*args, **kwargs)
self.theme_cls.bind(material_style=self.refresh_tabs)
Window.bind(on_resize=self.on_resize)
Clock.schedule_once(lambda x: self.on_resize())
Clock.schedule_once(self.set_status_bar_color)
def set_status_bar_color(self, interval: Union[int, float]) -> None:
if self.set_bars_color:
set_bars_colors(self.panel_color, None, self.theme_cls.theme_style)
def switch_tab(self, name_tab) -> None:
"""Switching the tab by name."""
if not self.ids.tab_manager.has_screen(name_tab):
raise ScreenManagerException(f"No Screen with name '{name_tab}'.")
self.ids.tab_manager.get_screen(name_tab).dispatch("on_tab_press")
count_index_screen = [
self.ids.tab_manager.screens.index(screen)
for screen in self.ids.tab_manager.screens
if screen.name == name_tab
][0]
numbers_screens = list(range(len(self.ids.tab_manager.screens)))
numbers_screens.reverse()
self.ids.tab_bar.children[
numbers_screens.index(count_index_screen)
].dispatch("on_press")
def refresh_tabs(self, *args) -> None:
"""Refresh all tabs."""
if self.ids:
tab_bar = self.ids.tab_bar
tab_bar.clear_widgets()
tab_manager = self.ids.tab_manager
self._active_color = self.theme_cls.primary_color
if self.text_color_active != [1, 1, 1, 1]:
self._active_color = self.text_color_active
for tab in tab_manager.screens:
self.tab_header = MDBottomNavigationHeader(tab=tab, panel=self)
tab.header = self.tab_header
tab_bar.add_widget(self.tab_header)
if tab is self.first_widget:
self.tab_header._text_color_normal = self._active_color
self.tab_header._label_font_size = sp(14)
self.tab_header.active = True
else:
self.tab_header.ids._label.font_size = sp(12)
self.tab_header._label_font_size = sp(12)
def on_font_name(self, instance_bottom_navigation, font_name: str) -> None:
for tab in self.ids.tab_bar.children:
tab.ids._label.font_name = font_name
def on_selected_color_background(
self, instance_bottom_navigation, color: list
) -> None:
def on_selected_color_background(*args):
for tab in self.ids.tab_bar.children:
tab.selected_color_background = color
Clock.schedule_once(on_selected_color_background)
def on_use_text(
self, instance_bottom_navigation, use_text_value: bool
) -> None:
if not use_text_value:
for instance_bottom_navigation_header in self.ids.tab_bar.children:
instance_bottom_navigation_header.ids.item_container.remove_widget(
instance_bottom_navigation_header.ids._label
)
if self.theme_cls.material_style == "M2":
height = dp(42)
else:
height = dp(80)
self.height = height
self.ids.bottom_panel.height = height
self.ids.tab_bar.height = height
else:
if self.theme_cls.material_style == "M2":
height = STANDARD_INCREMENT
else:
height = dp(80)
self.height = height
self.ids.bottom_panel.height = height
self.ids.tab_bar.height = height
def on_text_color_normal(
self, instance_bottom_navigation, color: list
) -> None:
MDBottomNavigationHeader.text_color_normal = color
for tab in self.ids.tab_bar.children:
if not tab.active:
tab._text_color_normal = color
def on_text_color_active(
self, instance_bottom_navigation, color: list
) -> None:
def on_text_color_active(*args):
MDBottomNavigationHeader.text_color_active = color
self.text_color_active = color
for tab in self.ids.tab_bar.children:
tab.text_color_active = color
if tab.active:
tab._text_color_normal = color
Clock.schedule_once(on_text_color_active)
def on_switch_tabs(self, bottom_navigation_item, name_tab: str) -> None:
"""
Called when switching tabs. Returns the object of the tab to be opened.
"""
def on_size(self, *args) -> None:
self.on_resize()
def on_resize(
self,
instance: Union[WindowSDL, None] = None,
width: Union[int, None] = None,
do_again: bool = True,
) -> None:
"""Called when the application window is resized."""
full_width = 0
for tab in self.ids.tab_manager.screens:
full_width += tab.header.width
tab.header.text_color_normal = self.text_color_normal
self.ids.tab_bar.width = full_width
if do_again:
Clock.schedule_once(lambda x: self.on_resize(do_again=False), 0.1)
def add_widget(self, widget, **kwargs):
if isinstance(widget, MDBottomNavigationItem):
self.widget_index += 1
widget.index = self.widget_index
widget.parent_widget = self
self.ids.tab_manager.add_widget(widget)
if self.widget_index == 1:
self.previous_tab = widget
self.first_widget = widget
self.refresh_tabs()
else:
super().add_widget(widget)
def remove_widget(self, widget):
if isinstance(widget, MDBottomNavigationItem):
self.ids.tab_manager.remove_widget(widget)
self.refresh_tabs()
else:
super().remove_widget(widget)
def _get_switchig_tab(self, name_tab: str) -> MDBottomNavigationItem:
bottom_navigation_item = None
for bottom_navigation_header_instance in self.ids.tab_bar.children:
if bottom_navigation_header_instance.tab.name == name_tab:
bottom_navigation_item = bottom_navigation_header_instance.tab
break
return bottom_navigation_item
class MDBottomNavigationBar(CommonElevationBehavior, MDFloatLayout):
pass

View file

@ -0,0 +1,11 @@
# NOQA F401
from .bottomsheet import (
MDBottomSheet,
MDBottomSheetContent,
MDBottomSheetDragHandle,
MDBottomSheetDragHandleButton,
MDBottomSheetDragHandleTitle,
MDCustomBottomSheet,
MDGridBottomSheet,
MDListBottomSheet,
)

View file

@ -0,0 +1,42 @@
<MDBottomSheetContent>
size_hint_y: None
height: self.minimum_height
<MDBottomSheetDragHandle>
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
padding: "16dp", "8dp", "16dp", "16dp"
BottomSheetDragHandle:
md_bg_color:
app.theme_cls.disabled_hint_text_color \
if not root.drag_handle_color else \
root.drag_handle_color
size_hint: None, None
size: "32dp", "4dp"
radius: 4
pos_hint: {"center_x": .5}
BottomSheetDragHandleContainer:
id: header_container
size_hint_y: None
height: self.minimum_height
<MDBottomSheet>
orientation: "vertical"
md_bg_color: root.bg_color if root.bg_color else app.theme_cls.bg_darkest
radius: 16, 16, 0, 0
padding: 0, "8dp", 0, 0
MDBoxLayout:
id: drag_handle_container
size_hint_y: None
height: self.minimum_height
MDBoxLayout:
id: container
size_hint_y: None
height: self.minimum_height

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
"""
Components/BoxLayout
====================
:class:`~kivy.uix.boxlayout.BoxLayout` class equivalent. Simplifies working
with some widget properties. For example:
BoxLayout
---------
.. code-block:: kv
BoxLayout:
size_hint_y: None
height: self.minimum_height
canvas:
Color:
rgba: app.theme_cls.primary_color
Rectangle:
pos: self.pos
size: self.size
MDBoxLayout
-----------
.. code-block:: kv
MDBoxLayout:
adaptive_height: True
md_bg_color: app.theme_cls.primary_color
Available options are:
----------------------
- adaptive_height_
- adaptive_width_
- adaptive_size_
.. adaptive_height:
adaptive_height
---------------
.. code-block:: kv
adaptive_height: True
Equivalent
.. code-block:: kv
size_hint_y: None
height: self.minimum_height
.. adaptive_width:
adaptive_width
--------------
.. code-block:: kv
adaptive_width: True
Equivalent
.. code-block:: kv
size_hint_x: None
height: self.minimum_width
.. adaptive_size:
adaptive_size
-------------
.. code-block:: kv
adaptive_size: True
Equivalent
.. code-block:: kv
size_hint: None, None
size: self.minimum_size
"""
__all__ = ("MDBoxLayout",)
from kivy.uix.boxlayout import BoxLayout
from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
class MDBoxLayout(
DeclarativeBehavior, ThemableBehavior, BoxLayout, MDAdaptiveWidget
):
"""
Box layout class.
For more information, see in the
:class:`~kivy.uix.boxlayout.BoxLayout` class documentation.
"""

View file

@ -0,0 +1,17 @@
# NOQA F401
from .button import (
BaseButton,
ButtonContentsIconText,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
MDFlatButton,
MDFloatingActionButton,
MDFloatingActionButtonSpeedDial,
MDIconButton,
MDRaisedButton,
MDRectangleFlatButton,
MDRectangleFlatIconButton,
MDRoundFlatButton,
MDRoundFlatIconButton,
MDTextButton,
)

View file

@ -0,0 +1,225 @@
<BaseButton>
canvas:
Clear
Color:
group: "bg-color"
rgba:
self._md_bg_color \
if not self.disabled else \
self._md_bg_color_disabled
RoundedRectangle:
size: self.size
pos: self.pos
source: self.source if hasattr(self, "source") else ""
radius: [root._radius, ]
Color:
group: "outline-color"
rgba:
root._line_color \
if not root.disabled else \
(root._line_color_disabled or self._disabled_color)
Line:
width: root.line_width
rounded_rectangle:
( \
self.x, self.y, self.width, self.height, \
root._radius, root._radius, root._radius, root._radius, \
self.height \
)
size_hint: None, None
anchor_x: root.halign
anchor_y: root.valign
_round_rad: [self._radius] * 4
<ButtonContentsText>
lbl_txt: lbl_txt
width:
max( \
root._min_width, \
root.padding[0] + lbl_txt.texture_size[0] + root.padding[2] \
)
size_hint_min_x:
max( \
root._min_width, \
root.padding[0] + lbl_txt.texture_size[0] + root.padding[2] \
)
height:
max( \
root._min_height, \
root.padding[1] + lbl_txt.texture_size[1] + root.padding[3] \
)
size_hint_min_y:
max( \
root._min_height, \
root.padding[1] + lbl_txt.texture_size[1] + root.padding[3] \
)
MDLabel:
id: lbl_txt
text: root.text
font_size: root.font_size
font_style: root.font_style
halign: 'center'
valign: 'middle'
adaptive_size: True
-text_size: None, None
theme_text_color: root._theme_text_color
text_color: root._text_color
markup: True
disabled: root.disabled
opposite_colors: root.opposite_colors
font_name: root.font_name if root.font_name else self.font_name
<ButtonContentsIcon>
lbl_ic: lbl_ic
size: "48dp", "48dp"
padding: "12dp" if root.icon in md_icons else (0, 0, 0, 0)
# Backwards compatibility.
theme_icon_color: root.theme_icon_color or root.theme_text_color
MDIcon:
id: lbl_ic
icon: root.icon
font_size: root.icon_size if root.icon_size else self.font_size
font_name: root.font_name if root.font_name else self.font_name
opposite_colors: root.opposite_colors
text_color:
# FIXME: ValueError: None is not allowed for MDIcon.text_color.
# This is only a temporary fix and does not fix the cause of the error.
(root._icon_color if root._icon_color else root.theme_cls.text_color) \
if not root.disabled else \
root.theme_cls.disabled_hint_text_color \
if not root.disabled_color else \
root.disabled_color
# Fix https://github.com/kivymd/KivyMD/issues/1448
# TODO: Perhaps this change may affect other widgets.
# You need to create tests.
# on_icon:
# if self.icon not in md_icons.keys(): self.size_hint = (1, 1)
theme_text_color: root._theme_icon_color
<ButtonContentsIconText>
lbl_txt: lbl_txt
lbl_ic: lbl_ic
width:
max( \
root._min_width, \
root.padding[0] \
+ lbl_ic.texture_size[0] \
+ box.spacing \
+ lbl_txt.texture_size[0] \
+ root.padding[2] \
)
size_hint_min_x:
max( \
root._min_width, \
root.padding[0] \
+ lbl_ic.texture_size[0] \
+ box.spacing \
+ lbl_txt.texture_size[0] \
+ root.padding[2] \
)
height:
max( \
root._min_height, \
root.padding[1] \
+ max(lbl_ic.texture_size[1], lbl_txt.texture_size[1]) \
+ root.padding[3] \
)
size_hint_min_y:
max( \
root._min_height, \
root.padding[1] \
+ max(lbl_ic.texture_size[1], lbl_txt.texture_size[1]) \
+ root.padding[3] \
)
MDBoxLayout:
id: box
adaptive_size: True
padding: 0
spacing: "8dp"
MDIcon:
id: lbl_ic
size_hint_x: None
pos_hint: {"center_y": .5}
icon: root.icon
opposite_colors: root.opposite_colors
font_size:
root.icon_size \
if root.icon_size else \
(18 / 14 * lbl_txt.font_size)
text_color:
root._icon_color \
if not root.disabled else \
root.theme_cls.disabled_hint_text_color
theme_text_color: root._theme_icon_color
MDLabel:
id: lbl_txt
adaptive_size: True
-text_size: None, None
pos_hint: {"center_y": .5}
halign: 'center'
valign: 'middle'
text: root.text
font_size: root.font_size
font_style: root.font_style
font_name: root.font_name if root.font_name else self.font_name
theme_text_color: root._theme_text_color
text_color: root._text_color
markup: True
disabled: root.disabled
opposite_colors: root.opposite_colors
<MDTextButton>
adaptive_size: True
color: root.theme_cls.primary_color if not root.color else root.color
opacity: 1
<BaseFloatingBottomButton>
theme_text_color: "Custom"
md_bg_color: self.theme_cls.primary_color
canvas.before:
Color:
rgba:
self.theme_cls.primary_color \
if not self._bg_color else \
self._bg_color
RoundedRectangle:
pos:
(self.x - self._canvas_width + dp(1.5)) + self._padding_right / 2, \
self.y - self._padding_right / 2 + dp(1.5)
size:
self.width + self._canvas_width - dp(3), \
self.height + self._padding_right - dp(3)
radius: [self.height / 2]
<MDFloatingRootButton>
theme_text_color: "Custom"
md_bg_color: self.theme_cls.primary_color
<MDFloatingLabel>
padding_x: "8dp"
padding_y: "8dp"
adaptive_size: True
theme_text_color: "Custom"
canvas.before:
Color:
rgba: self.bg_color
RoundedRectangle:
size: self.size
pos: self.pos
radius: self.radius

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
# NOQA F401
from .card import (
MDCard,
MDCardSwipe,
MDCardSwipeFrontBox,
MDCardSwipeLayerBox,
MDSeparator,
)

View file

@ -0,0 +1,9 @@
<MDCardSwipeLayerBox>:
md_bg_color: app.theme_cls.divider_color
<MDSeparator>
md_bg_color:
self.theme_cls.divider_color \
if not root.color \
else root.color

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,218 @@
"""
Components/Carousel
===================
:class:`~kivy.uix.boxlayout.Carousel` class equivalent. Simplifies working
with some widget properties. For example:
Carousel
---------
.. code-block:: python
kv='''
YourCarousel:
BoxLayout:
[...]
BoxLayout:
[...]
BoxLayout:
[...]
'''
builder.load_string(kv)
class YourCarousel(Carousel):
def __init__(self,*kwargs):
self.register_event_type("on_slide_progress")
self.register_event_type("on_slide_complete")
def on_touch_down(self, *args):
["Code to detect when the slide changes"]
def on_touch_up(self, *args):
["Code to detect when the slide changes"]
def Calculate_slide_pos(self, *args):
["Code to calculate the current position of the slide"]
def do_custom_animation(self, *args):
["Code to recreate an animation"]
MDCarousel
-----------
.. code-block:: kv
MDCarousel:
on_slide_progress:
do_something()
on_slide_complete:
do_something()
"""
# TODO: Add documentation.
from kivy.animation import Animation
from kivy.uix.carousel import Carousel
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import DeclarativeBehavior
class MDCarousel(DeclarativeBehavior, ThemableBehavior, Carousel):
"""
based on kivy's carousel.
.. seealso::
`kivy.uix.carousel.Carousel <https://kivy.org/doc/stable/api-kivy.uix.carousel.html>`_
"""
_scrolling = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.register_event_type("on_slide_progress")
self.register_event_type("on_slide_complete")
def on_slide_progress(self, *args):
"""
Event launched when the Slide animation is progress.
remember to bind and unbid to this method.
"""
def on_slide_complete(self, *args):
"""
Event launched when the Slide animation is complete.
remember to bind and unbid to this method.
"""
def _position_visible_slides(self, *args):
slides, index = self.slides, self.index
no_of_slides = len(slides) - 1
if not slides:
return
x, y, width, height = self.x, self.y, self.width, self.height
_offset, direction = self._offset, self.direction
_prev, _next, _current = self._prev, self._next, self._current
get_slide_container = self.get_slide_container
last_slide = get_slide_container(slides[-1])
first_slide = get_slide_container(slides[0])
skip_next = False
_loop = self.loop
if direction[0] in ["r", "l"]:
xoff = x + _offset
x_prev = {"l": xoff + width, "r": xoff - width}
x_next = {"l": xoff - width, "r": xoff + width}
if _prev:
_prev.pos = (x_prev[direction[0]], y)
elif _loop and _next and index == 0:
if (_offset > 0 and direction[0] == "r") or (
_offset < 0 and direction[0] == "l"
):
last_slide.pos = (x_prev[direction[0]], y)
skip_next = True
if _current:
_current.pos = (xoff, y)
if self._scrolling:
self.dispatch("on_slide_progress", xoff)
if skip_next:
return
if _next:
_next.pos = (x_next[direction[0]], y)
elif _loop and _prev and index == no_of_slides:
if (_offset < 0 and direction[0] == "r") or (
_offset > 0 and direction[0] == "l"
):
first_slide.pos = (x_next[direction[0]], y)
if direction[0] in ["t", "b"]:
yoff = y + _offset
y_prev = {"t": yoff - height, "b": yoff + height}
y_next = {"t": yoff + height, "b": yoff - height}
if _prev:
_prev.pos = (x, y_prev[direction[0]])
elif _loop and _next and index == 0:
if (_offset > 0 and direction[0] == "t") or (
_offset < 0 and direction[0] == "b"
):
last_slide.pos = (x, y_prev[direction[0]])
skip_next = True
if _current:
_current.pos = (x, yoff)
if skip_next:
return
if _next:
_next.pos = (x, y_next[direction[0]])
elif _loop and _prev and index == no_of_slides:
if (_offset < 0 and direction[0] == "t") or (
_offset > 0 and direction[0] == "b"
):
first_slide.pos = (x, y_next[direction[0]])
def on_touch_down(self, touch):
self._scrolling = True
return super().on_touch_down(touch)
def on_touch_up(self, touch):
self._scrolling = False
return super().on_touch_up(touch)
def _start_animation(self, *args, **kwargs):
# compute target offset for ease back, next or prev
new_offset = 0
direction = kwargs.get("direction", self.direction)[0]
is_horizontal = direction in "rl"
extent = self.width if is_horizontal else self.height
min_move = kwargs.get("min_move", self.min_move)
_offset = kwargs.get("offset", self._offset)
if _offset < min_move * -extent:
new_offset = -extent
elif _offset > min_move * extent:
new_offset = extent
# if new_offset is 0, it wasnt enough to go next/prev
dur = self.anim_move_duration
if new_offset == 0:
dur = self.anim_cancel_duration
# detect edge cases if not looping
len_slides = len(self.slides)
index = self.index
if not self.loop or len_slides == 1:
is_first = index == 0
is_last = index == len_slides - 1
if direction in "rt":
towards_prev = new_offset > 0
towards_next = new_offset < 0
else:
towards_prev = new_offset < 0
towards_next = new_offset > 0
if (is_first and towards_prev) or (is_last and towards_next):
new_offset = 0
anim = Animation(_offset=new_offset, d=dur, t=self.anim_type)
anim.cancel_all(self)
def _cmp(*args):
self.dispatch(
"on_slide_complete",
self.previous_slide,
self.current_slide,
self.next_slide,
)
if self._skip_slide is not None:
self.index = self._skip_slide
self._skip_slide = None
anim.bind(
on_complete=_cmp,
on_progress=lambda *args: self.dispatch(
"on_slide_progress", self._offset
),
)
anim.start(self)

View file

@ -0,0 +1 @@
from .chip import MDChip, MDChipText # NOQA F401

View file

@ -0,0 +1,38 @@
<MDChip>
size_hint_y: None
height: "32dp"
adaptive_width: True
radius:
16 \
if self.radius == [0, 0, 0, 0] else \
(max(self.radius) if max(self.radius) < self.height / 2 else 16)
md_bg_color:
( \
( \
app.theme_cls.bg_darkest \
if app.theme_cls.theme_style == "Light" else \
app.theme_cls.bg_light \
) \
if not self._origin_md_bg_color else \
self._origin_md_bg_color
) \
if not self.disabled else app.theme_cls.disabled_primary_color
line_color:
app.theme_cls.disabled_hint_text_color \
if self.disabled else ( \
self._origin_line_color \
if self._origin_line_color else \
self.line_color \
)
LeadingIconContainer:
id: leading_icon_container
adaptive_width: True
LabelTextContainer:
id: label_container
adaptive_width: True
TrailingIconContainer:
id: trailing_icon_container
adaptive_width: True

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,207 @@
"""
Components/CircularLayout
=========================
CircularLayout is a special layout that places widgets around a circle.
MDCircularLayout
----------------
.. rubric:: Usage
.. code-block::
from kivy.lang.builder import Builder
from kivy.uix.label import Label
from kivymd.app import MDApp
kv = '''
MDScreen:
MDCircularLayout:
id: container
pos_hint: {"center_x": .5, "center_y": .5}
row_spacing: min(self.size) * 0.1
'''
class Main(MDApp):
def build(self):
return Builder.load_string(kv)
def on_start(self):
for x in range(1, 49):
self.root.ids.container.add_widget(
Label(text=f"{x}", color=[0, 0, 0, 1])
)
Main().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-layout.png
:align: center
"""
__all__ = ("MDCircularLayout",)
from math import atan2, cos, degrees, radians, sin
from kivy.properties import BooleanProperty, NumericProperty
from kivymd.uix.floatlayout import MDFloatLayout
class MDCircularLayout(MDFloatLayout):
degree_spacing = NumericProperty(30)
"""
The space between children in degree.
:attr:`degree_spacing` is an :class:`~kivy.properties.NumericProperty`
and defaults to `30`.
"""
circular_radius = NumericProperty(None, allownone=True)
"""
Radius of circle. Radius will be the greatest value in the layout if `circular_radius` if not specified.
:attr:`circular_radius` is an :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
start_from = NumericProperty(60)
"""
The positon of first child in degree.
:attr:`start_from` is an :class:`~kivy.properties.NumericProperty`
and defaults to `60`.
"""
max_degree = NumericProperty(360)
"""
Maximum range in degree allowed for each row of widgets before jumping to the next row.
:attr:`max_degree` is an :class:`~kivy.properties.NumericProperty`
and defaults to `360`.
"""
circular_padding = NumericProperty("25dp")
"""
Padding between outer widgets and the edge of the biggest circle.
:attr:`circular_padding` is an :class:`~kivy.properties.NumericProperty`
and defaults to `25dp`.
"""
row_spacing = NumericProperty("50dp")
"""
Space between each row of widget.
:attr:`row_spacing` is an :class:`~kivy.properties.NumericProperty`
and defaults to `50dp`.
"""
clockwise = BooleanProperty(True)
"""
Direction of widgets in circular direction.
:attr:`clockwise` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
row_spacing=self._update_layout,
)
def get_angle(self, pos: tuple) -> float:
"""Returns the angle of given pos."""
center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2]
(dx, dy) = (center[0] - pos[0], center[1] - pos[1])
angle = degrees(atan2(float(dy), float(dx)))
angle += 180
return angle
def remove_widget(self, widget, **kwargs):
super().remove_widget(widget, **kwargs)
self._update_layout()
def do_layout(self, *largs, **kwargs):
self._update_layout()
return super().do_layout(*largs, **kwargs)
def _max_per_row(self):
return int(self.max_degree / self.degree_spacing)
def _update_layout(self, *args):
for index, child in enumerate(reversed(self.children)):
pos = self._point_on_circle(
self._calculate_radius(index),
self._calculate_degree(index),
)
child.center = pos
def _calculate_radius(self, index):
"""Calculates the radius for given index."""
idx = int(index / self._max_per_row())
if not self.circular_radius:
init_radius = (
min([self.width / 2, self.height / 2]) - self.circular_padding
)
else:
init_radius = self.circular_radius
if idx != 0:
space = self.row_spacing * idx
init_radius -= space
return init_radius
def _calculate_degree(self, index):
"""Calculates the angle for given index."""
if self.clockwise:
degree = self.start_from - index * self.degree_spacing
else:
degree = self.start_from + index * self.degree_spacing
return degree
def _point_on_circle(self, radius, degree):
angle = radians(degree)
center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2]
x = center[0] + (radius * cos(angle))
y = center[1] + (radius * sin(angle))
return [x, y]
if __name__ == "__main__":
from kivy.lang.builder import Builder
from kivy.uix.label import Label
from kivymd.app import MDApp
kv = """
MDScreen:
MDCircularLayout:
id: container
pos_hint: {"center_x": .5, "center_y": .5}
row_spacing: min(self.size) * 0.1
"""
class Main(MDApp):
def build(self):
return Builder.load_string(kv)
def on_start(self):
for x in range(1, 49):
self.root.ids.container.add_widget(
Label(text=f"{x}", color=[0, 0, 0, 1])
)
Main().run()

View file

@ -0,0 +1,11 @@
"""
Controllers
===========
.. versionadded:: 1.0.0
Modules and classes that implement useful methods for getting information
about the state of the current application window.
"""
from .windowcontroller import WindowController

View file

@ -0,0 +1,80 @@
"""
Controllers/WindowController
============================
.. versionadded:: 1.0.0
Modules and classes that implement useful methods for getting information
about the state of the current application window.
Controlling the resizing direction of the application window
------------------------------------------------------------
.. code-block:: python
# When resizing the application window, the direction of change will be
# printed - 'left' or 'right'.
from kivymd.app import MDApp
from kivymd.uix.controllers import WindowController
from kivymd.uix.screen import MDScreen
class MyScreen(MDScreen, WindowController):
def on_width(self, *args):
print(self.get_window_width_resizing_direction())
class Test(MDApp):
def build(self):
return MyScreen()
Test().run()
"""
from kivy.core.window import Window
from kivy.core.window.window_sdl2 import WindowSDL
from kivy.metrics import dp
class WindowController:
def __init__(self):
self.window_resizing_direction = "unknown"
self.real_device_type = "unknown"
self.__width = Window.width
Window.bind(on_resize=self._on_resize)
def on_size(self, instance, size: list) -> None:
"""Called when the application screen size changes."""
window_width = size[0]
if window_width < dp(500):
self.real_device_type = "mobile"
elif window_width < dp(1100):
self.real_device_type = "tablet"
else:
self.real_device_type = "desktop"
def get_real_device_type(self) -> str:
"""Returns the device type - 'mobile', 'tablet' or 'desktop'."""
return self.real_device_type
def get_window_width_resizing_direction(self) -> str:
"""Return window width resizing direction - 'left' or 'right'."""
return self.window_resizing_direction
def _set_window_width_resizing_direction(self, width: int) -> None:
if self.__width > width:
self.window_resizing_direction = "left"
elif self.__width < width:
self.window_resizing_direction = "right"
def _on_resize(
self, window_sdl2: WindowSDL, width: int, height: int
) -> None:
self._set_window_width_resizing_direction(width)
self.__width = width

View file

@ -0,0 +1 @@
from .datatables import MDDataTable # NOQA F401

View file

@ -0,0 +1,241 @@
#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE
<CellRow>
orientation: "vertical"
md_bg_color:
( \
( \
root.theme_cls.bg_darkest \
if root.theme_cls.theme_style == "Light" else \
root.theme_cls.bg_light \
) \
if not root.background_color_selected_cell \
else root.background_color_selected_cell \
) \
if self.selected \
else \
( \
root.theme_cls.bg_normal \
if not root.background_color_cell \
else root.background_color_cell \
)
on_press: if DEVICE_TYPE != "desktop": root.table.on_mouse_select(self)
on_enter: if DEVICE_TYPE == "desktop": root.table.on_mouse_select(self)
MDBoxLayout:
id: box
padding: "8dp", "8dp", 0, "8dp"
spacing: "16dp"
MDCheckbox:
id: check
size_hint: None, None
size: 0, 0
opacity: 0
MDBoxLayout:
id: inner_box
MDIcon:
id: icon
size_hint: None, None
pos_hint: {"center_y": 0.5}
size: ("24dp", "24dp") if root.icon else (0, 0)
icon: root.icon if root.icon else ""
theme_text_color: "Custom"
text_color:
root.icon_color if root.icon_color else \
root.theme_cls.primary_color
MDLabel:
id: label
text: " " + root.text
markup: True
color:
(1, 1, 1, 1) \
if root.theme_cls.theme_style == "Dark" else \
(0, 0, 0, 1)
MDSeparator:
<CellHeader>
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
spacing: "4dp"
tooltip_text: root.tooltip if root.tooltip else root.text
BoxLayout:
id: box
size_hint_y: None
height: lbl.height
MDLabel:
id: lbl
text: " " + root.text
adaptive_height: True
bold: True
markup: True
color:
(1, 1, 1, 1) \
if root.theme_cls.theme_style == "Dark" else \
(0, 0, 0, 1)
MDSeparator:
id: separator
<SortButton>
id: sort_btn
icon: "arrow-up"
pos_hint: {"center_y": 0.5}
size: [dp(24), dp(0)]
theme_text_color: "Custom"
text_color: self.theme_cls.secondary_text_color
opacity: 0
<TableHeader>
bar_width: 0
do_scroll: False
size_hint: 1, None
height: header.height
MDGridLayout:
id: header
rows: 1
cols_minimum: root.cols_minimum
adaptive_size: True
padding: 0, "8dp", 0, 0
md_bg_color:
root.theme_cls.bg_light \
if not root.background_color_header \
else root.background_color_header
MDBoxLayout:
orientation: "vertical"
MDBoxLayout:
id: box
padding: "8dp", "8dp", "4dp", 0
spacing: "16dp"
MDCheckbox:
id: check
size_hint: None, None
size: 0, 0
opacity: 0
on_release: root.table_data.select_all(self.state)
CellHeader:
id: first_cell
MDSeparator:
<TableData>
data: root.recycle_data
data_first_cells: root.data_first_cells
key_viewclass: "viewclass"
TableRecycleGridLayout:
id: row_controller
key_selection: "selectable"
cols: root.total_col_headings
cols_minimum: root.cols_minimum
default_size: None, dp(52)
default_size_hint: 1, None
size_hint: None, None
height: self.minimum_height
width: self.minimum_width
multiselect: True
touch_multiselect: True
<TablePagination>
adaptive_height: True
spacing: "8dp"
MDLabel:
text: "Rows per page"
shorten: True
halign: "right"
font_style: "Caption"
color:
(1, 1, 1, 1) \
if root.theme_cls.theme_style == "Dark" else \
(0, 0, 0, 1)
MDDropDownItem:
id: drop_item
pos_hint: {'center_y': .5}
font_size: "14sp"
on_release: root.table_data.open_pagination_menu()
text:
"{}".format( \
root.table_data.rows_num \
if root.table_data.rows_num < len(root.table_data.row_data) else \
len(root.table_data.row_data) \
)
Widget:
size_hint_x: None
width: "32dp" if DEVICE_TYPE != "mobile" else "8dp"
MDLabel:
id: label_rows_per_page
adaptive_size: True
-text_size: None, None
pos_hint: {"center_y": .5}
font_style: "Caption"
color:
(1, 1, 1, 1) \
if root.theme_cls.theme_style == "Dark" else \
(0, 0, 0, 1)
text:
"1-{} of {}".format( \
root.table_data.rows_num \
if root.table_data.rows_num > len(root.table_data.row_data) else \
len(root.table_data.row_data), len(root.table_data.row_data) \
)
MDIconButton:
id: button_back
icon: "chevron-left"
user_font_size: "20sp" if DEVICE_TYPE != "mobile" else "16dp"
ripple_scale: .5 if DEVICE_TYPE == "mobile" else 1
pos_hint: {'center_y': .5}
disabled: True
md_bg_color_disabled: 0, 0, 0, 0
on_release: root.table_data.set_next_row_data_parts("back")
MDIconButton:
id: button_forward
icon: "chevron-right"
user_font_size: "20sp" if DEVICE_TYPE != "mobile" else "16dp"
ripple_scale: .5 if DEVICE_TYPE == "mobile" else 1
pos_hint: {'center_y': .5}
disabled: True
md_bg_color_disabled: 0, 0, 0, 0
on_release: root.table_data.set_next_row_data_parts("forward")
<TableContainer@MDCard>
<MDDataTable>
TableContainer:
id: container
orientation: "vertical"
elevation: root.elevation
shadow_radius: root.shadow_radius
shadow_softness: root.shadow_softness
shadow_offset: root.shadow_offset
shadow_color: root.shadow_color
shadow_color: root.shadow_color
shadow_softness_size: root.shadow_softness_size
padding: "24dp", "24dp", "8dp", "8dp"
md_bg_color: app.theme_cls.bg_normal

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
from .dialog import BaseDialog, MDDialog # NOQA F401

View file

@ -0,0 +1,90 @@
#:import images_path kivymd.images_path
<BaseDialog>
background: '{}/transparent.png'.format(images_path)
canvas.before:
PushMatrix
RoundedRectangle:
pos: self.pos
size: self.size
radius: root.radius
Scale:
origin: self.center
x: root._scale_x
y: root._scale_y
canvas.after:
PopMatrix
<DialogContainer@MDCard>
shadow_color: 0.0, 0.0, 0.0, 0.0
elevation: 0
shadow_softness: 0
shadow_offset: 0, 0
<MDDialog>
DialogContainer:
id: container
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
padding: "24dp", "24dp", "8dp", "8dp"
radius: root.radius
md_bg_color:
root.theme_cls.bg_dark \
if not root.md_bg_color else root.md_bg_color
MDLabel:
id: title
text: root.title
font_style: "H6"
bold: True
markup: True
size_hint_y: None
height: self.texture_size[1]
valign: "top"
BoxLayout:
id: spacer_top_box
size_hint_y: None
height: root._spacer_top
MDLabel:
id: text
text: root.text
font_style: "Body1"
theme_text_color: "Custom"
text_color: root.theme_cls.disabled_hint_text_color
size_hint_y: None
height: self.texture_size[1]
markup: True
ScrollView:
id: scroll
size_hint_y: None
height: root._scroll_height
MDGridLayout:
id: box_items
adaptive_height: True
cols: 1
BoxLayout:
id: spacer_bottom_box
size_hint_y: None
height: self.minimum_height
AnchorLayout:
id: root_button_box
size_hint_y: None
height: "52dp"
anchor_x: "right"
MDBoxLayout:
id: button_box
adaptive_size: True
spacing: "8dp"

View file

@ -0,0 +1,690 @@
"""
Components/Dialog
=================
.. seealso::
`Material Design spec, Dialogs <https://material.io/components/dialogs>`_
.. rubric:: Dialogs inform users about a task and can contain critical
information, require decisions, or involve multiple tasks.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialogs.png
:align: center
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
KV = '''
MDFloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_alert_dialog()
'''
class Example(MDApp):
dialog = None
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def show_alert_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
text="Discard draft?",
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
MDFlatButton(
text="DISCARD",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/alert-dialog.png
:align: center
"""
__all__ = ("MDDialog", "BaseDialog")
import os
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
ColorProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.modalview import ModalView
from kivymd import uix_path
from kivymd.material_resources import DEVICE_TYPE
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import CommonElevationBehavior, MotionDialogBehavior
from kivymd.uix.button import BaseButton
from kivymd.uix.card import MDSeparator
from kivymd.uix.list import BaseListItem
with open(
os.path.join(uix_path, "dialog", "dialog.kv"), encoding="utf-8"
) as kv_file:
Builder.load_string(kv_file.read())
class BaseDialog(
ThemableBehavior, MotionDialogBehavior, ModalView, CommonElevationBehavior
):
elevation = NumericProperty(3)
"""
See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation`
attribute for more information.
.. versionadded:: 1.1.0
:attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
and defaults to `3`.
"""
shadow_softness = NumericProperty(24)
"""
See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness`
attribute for more information.
.. versionadded:: 1.1.0
:attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty`
and defaults to `24`.
"""
shadow_offset = ListProperty((0, 4))
"""
See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset`
attribute for more information.
.. versionadded:: 1.1.0
:attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 4]`.
"""
radius = ListProperty([dp(7), dp(7), dp(7), dp(7)])
"""
Dialog corners rounding value.
.. code-block:: python
[...]
self.dialog = MDDialog(
text="Oops! Something seems to have gone wrong!",
radius=[20, 7, 20, 7],
)
[...]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-radius.png
:align: center
:attr:`radius` is an :class:`~kivy.properties.ListProperty`
and defaults to `[7, 7, 7, 7]`.
"""
_scale_x = NumericProperty(1)
_scale_y = NumericProperty(1)
class MDDialog(BaseDialog):
"""
Dialog class.
For more information, see in the
:class:`~kivymd.theming.ThemableBehavior` and
:class:`~kivy.uix.modalview.ModalView` and
:class:`~kivymd.uix.behaviors.CommonElevationBehavior`
classes documentation.
"""
title = StringProperty()
"""
Title dialog.
.. code-block:: python
[...]
self.dialog = MDDialog(
title="Reset settings?",
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
MDFlatButton(
text="ACCEPT",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
],
)
[...]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-title.png
:align: center
:attr:`title` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
text = StringProperty()
"""
Text dialog.
.. code-block:: python
[...]
self.dialog = MDDialog(
title="Reset settings?",
text="This will reset your device to its default factory settings.",
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
MDFlatButton(
text="ACCEPT",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
],
)
[...]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-text.png
:align: center
:attr:`text` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
buttons = ListProperty()
"""
List of button objects for dialog.
Objects must be inherited from :class:`~kivymd.uix.button.BaseButton` class.
.. code-block:: python
[...]
self.dialog = MDDialog(
text="Discard draft?",
buttons=[
MDFlatButton(text="CANCEL"), MDRaisedButton(text="DISCARD"),
],
)
[...]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-buttons.png
:align: center
:attr:`buttons` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
items = ListProperty()
"""
List of items objects for dialog.
Objects must be inherited from :class:`~kivymd.uix.list.BaseListItem` class.
With type 'simple'
~~~~~~~~~~~~~~~~~~
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import OneLineAvatarListItem
KV = '''
<Item>
ImageLeftWidget:
source: root.source
MDFloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_simple_dialog()
'''
class Item(OneLineAvatarListItem):
divider = None
source = StringProperty()
class Example(MDApp):
dialog = None
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def show_simple_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
title="Set backup account",
type="simple",
items=[
Item(text="user01@gmail.com", source="kivymd/images/logo/kivymd-icon-128.png"),
Item(text="user02@gmail.com", source="data/logo/kivy-icon-128.png"),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-items.png
:align: center
With type 'confirmation'
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import OneLineAvatarIconListItem
KV = '''
<ItemConfirm>
on_release: root.set_icon(check)
CheckboxLeftWidget:
id: check
group: "check"
MDFloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_confirmation_dialog()
'''
class ItemConfirm(OneLineAvatarIconListItem):
divider = None
def set_icon(self, instance_check):
instance_check.active = True
check_list = instance_check.get_widgets(instance_check.group)
for check in check_list:
if check != instance_check:
check.active = False
class Example(MDApp):
dialog = None
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def show_confirmation_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
title="Phone ringtone",
type="confirmation",
items=[
ItemConfirm(text="Callisto"),
ItemConfirm(text="Luna"),
ItemConfirm(text="Night"),
ItemConfirm(text="Solo"),
ItemConfirm(text="Phobos"),
ItemConfirm(text="Diamond"),
ItemConfirm(text="Sirena"),
ItemConfirm(text="Red music"),
ItemConfirm(text="Allergio"),
ItemConfirm(text="Magic"),
ItemConfirm(text="Tic-tac"),
],
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-confirmation.png
:align: center
:attr:`items` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
width_offset = NumericProperty(dp(48))
"""
Dialog offset from device width.
:attr:`width_offset` is an :class:`~kivy.properties.NumericProperty`
and defaults to `dp(48)`.
"""
type = OptionProperty(
"alert", options=["alert", "simple", "confirmation", "custom"]
)
"""
Dialog type.
Available option are `'alert'`, `'simple'`, `'confirmation'`, `'custom'`.
:attr:`type` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'alert'`.
"""
content_cls = ObjectProperty()
"""
Custom content class. This attribute is only available when :attr:`type` is
set to `'custom'`.
.. tabs::
.. tab:: Declarative KV style
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
KV = '''
<Content>
orientation: "vertical"
spacing: "12dp"
size_hint_y: None
height: "120dp"
MDTextField:
hint_text: "City"
MDTextField:
hint_text: "Street"
MDFloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_confirmation_dialog()
'''
class Content(BoxLayout):
pass
class Example(MDApp):
dialog = None
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def show_confirmation_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
title="Address:",
type="custom",
content_cls=Content(),
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
],
)
self.dialog.open()
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 MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.textfield import MDTextField
class Example(MDApp):
dialog = None
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return (
MDFloatLayout(
MDFlatButton(
text="ALERT DIALOG",
pos_hint={'center_x': 0.5, 'center_y': 0.5},
on_release=self.show_confirmation_dialog,
)
)
)
def show_confirmation_dialog(self, *args):
if not self.dialog:
self.dialog = MDDialog(
title="Address:",
type="custom",
content_cls=MDBoxLayout(
MDTextField(
hint_text="City",
),
MDTextField(
hint_text="Street",
),
orientation="vertical",
spacing="12dp",
size_hint_y=None,
height="120dp",
),
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-custom.png
:align: center
:attr:`content_cls` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `'None'`.
"""
md_bg_color = ColorProperty(None)
"""
Background color in the (r, g, b, a) or string format.
:attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
_scroll_height = NumericProperty("28dp")
_spacer_top = NumericProperty("24dp")
def __init__(self, **kwargs):
super().__init__(**kwargs)
Window.bind(on_resize=self.update_width)
if self.size_hint == [1, 1] and (
DEVICE_TYPE == "desktop" or DEVICE_TYPE == "tablet"
):
self.size_hint = (None, None)
self.width = min(dp(560), Window.width - self.width_offset)
elif self.size_hint == [1, 1] and DEVICE_TYPE == "mobile":
self.size_hint = (None, None)
self.width = min(dp(280), Window.width - self.width_offset)
if not self.title:
self._spacer_top = 0
if not self.buttons:
self.ids.root_button_box.height = 0
else:
self.create_buttons()
update_height = False
if self.type in ("simple", "confirmation"):
if self.type == "confirmation":
self.ids.spacer_top_box.add_widget(MDSeparator())
self.ids.spacer_bottom_box.add_widget(MDSeparator())
self.create_items()
if self.type == "custom":
if self.content_cls:
self.ids.container.remove_widget(self.ids.scroll)
self.ids.container.remove_widget(self.ids.text)
self.ids.spacer_top_box.add_widget(self.content_cls)
self.ids.spacer_top_box.padding = (0, "24dp", "16dp", 0)
update_height = True
if self.type == "alert":
self.ids.scroll.bar_width = 0
if update_height:
Clock.schedule_once(self.update_height)
def update_width(self, *args) -> None:
self.width = max(
self.height + self.width_offset,
min(
dp(560) if DEVICE_TYPE != "mobile" else dp(280),
Window.width - self.width_offset,
),
)
def update_height(self, *args) -> None:
self._spacer_top = self.content_cls.height + dp(24)
def update_items(self, items: list) -> None:
self.ids.box_items.clear_widgets()
self.items = items
self.create_items()
def on_open(self) -> None:
# TODO: Add scrolling text.
self.height = self.ids.container.height
super().on_open()
def get_normal_height(self) -> float:
return (
(Window.height * 80 / 100)
- self._spacer_top
- dp(52)
- self.ids.container.padding[1]
- self.ids.container.padding[-1]
- 100
)
def edit_padding_for_item(self, instance_item) -> None:
instance_item.ids._left_container.x = 0
instance_item._txt_left_pad = "56dp"
def create_items(self) -> None:
if not self.text:
self.ids.container.remove_widget(self.ids.text)
height = 0
else:
height = self.ids.text.height
for item in self.items:
if issubclass(item.__class__, BaseListItem):
height += item.height # calculate height contents
self.edit_padding_for_item(item)
self.ids.box_items.add_widget(item)
if height > Window.height:
self.ids.scroll.height = self.get_normal_height()
else:
self.ids.scroll.height = height
def create_buttons(self) -> None:
for button in self.buttons:
if issubclass(button.__class__, BaseButton):
self.ids.button_box.add_widget(button)

Some files were not shown because too many files have changed in this diff Show more