working condition

This commit is contained in:
Yura 2024-09-15 20:57:02 +03:00
parent 417e54da96
commit 511e0b0379
517 changed files with 29187 additions and 32696 deletions

View file

@ -5,21 +5,11 @@ Behaviors
Modules and classes implementing various behaviors for buttons etc.
"""
from .backgroundcolor_behavior import (
BackgroundColorBehavior,
SpecificBackgroundColorBehavior,
)
from .backgroundcolor_behavior import BackgroundColorBehavior
# flake8: NOQA
from .declarative_behavior import DeclarativeBehavior
from .elevation import (
CircularElevationBehavior,
CommonElevationBehavior,
FakeCircularElevationBehavior,
FakeRectangularElevationBehavior,
RectangularElevationBehavior,
RoundedRectangularElevationBehavior,
)
from .elevation import CommonElevationBehavior
from .motion_behavior import (
MotionDialogBehavior,
MotionShackBehavior,

View file

@ -7,7 +7,7 @@ Behaviors/Background Color
from __future__ import annotations
__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior")
__all__ = ("BackgroundColorBehavior",)
from kivy.animation import Animation
from kivy.lang import Builder
@ -15,15 +15,10 @@ 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(
"""
@ -37,8 +32,9 @@ Builder.load_string(
angle: self.angle
origin: self._background_origin
Color:
group: "backgroundcolor-behavior-bg-color"
rgba: self._md_bg_color
RoundedRectangle:
SmoothRoundedRectangle:
group: "Background_instruction"
size: self.size
pos: self.pos if not isinstance(self, RelativeLayout) else (0, 0)
@ -49,11 +45,17 @@ Builder.load_string(
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:
SmoothLine:
width: root.line_width
rounded_rectangle:
[ \
0,
0, \
self.width, \
self.height, \
*self.radius, \
] \
if isinstance(self, RelativeLayout) else \
[ \
self.x,
self.y, \
@ -73,7 +75,7 @@ class BackgroundColorBehavior:
Background image path.
:attr:`background` is a :class:`~kivy.properties.StringProperty`
and defaults to `None`.
and defaults to `''`.
"""
radius = VariableListProperty([0], length=4)
@ -93,7 +95,7 @@ class BackgroundColorBehavior:
# 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])
md_bg_color = ColorProperty([0, 0, 0, 0])
"""
The background color of the widget (:class:`~kivy.uix.widget.Widget`)
that will be inherited from the :attr:`BackgroundColorBehavior` class.
@ -118,12 +120,12 @@ class 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]`.
and defaults to `[0, 0, 0, 0]`.
"""
line_color = ColorProperty([0, 0, 0, 0])
"""
If a custom value is specified for the `line_color parameter`, the border
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
@ -157,37 +159,18 @@ class BackgroundColorBehavior:
_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,
)
self.bind(pos=self.update_background_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."""
def on_md_bg_color(self, instance, color: list | str):
"""Fired when the values of :attr:`md_bg_color` change."""
if (
hasattr(self, "theme_cls")
and self.theme_cls.theme_style_switch_animation
and self.__class__.__name__ != "MDDropdownMenu"
):
Animation(
_md_bg_color=color,
@ -197,94 +180,10 @@ class BackgroundColorBehavior:
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."""
def update_background_origin(self, instance, pos: list) -> None:
"""Fired 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

@ -33,31 +33,35 @@ 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.navigationbar import (
MDNavigationBar,
MDNavigationItem,
MDNavigationItemIcon,
MDNavigationItemLabel,
)
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",
)
bottom_navigation = MDNavigationBar()
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"]
datas = [
{"text": "Mail", "icon": "gmail"},
{"text": "GitHub", "icon": "git"},
{"text": "LinkedIN", "icon": "linkedin"},
]
for data in datas:
text = data["text"]
navigation_item = MDNavigationItem(
MDNavigationItemIcon(
icon=data["icon"],
),
MDNavigationItemLabel(
text=text,
),
)
navigation_item.add_widget(MDLabel(text=text, halign="center"))
bottom_navigation.add_widget(navigation_item)
screen.add_widget(bottom_navigation)
@ -66,9 +70,6 @@ Imperative style
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,
@ -87,51 +88,42 @@ Declarative style with KV language
from kivymd.app import MDApp
class Test(MDApp):
class Example(MDApp):
def build(self):
return Builder.load_string(
'''
MDScreen:
MDBottomNavigation:
panel_color: "#eeeaea"
selected_color_background: "#97ecf8"
text_color_active: "white"
MDNavigationBar:
MDBottomNavigationItem:
name: "screen 1"
text: "Mail"
icon: "gmail"
MDNavigationItem:
MDLabel:
MDNavigationItemIcon:
icon: "gmail"
MDNavigationItemLabel:
text: "Mail"
halign: "center"
MDBottomNavigationItem:
name: "screen 2"
text: "Discord"
icon: "discord"
MDNavigationItem:
MDLabel:
text: "Discord"
halign: "center"
MDNavigationItemIcon:
icon: "git"
MDBottomNavigationItem:
name: "screen 3"
text: "LinkedIN"
icon: "linkedin"
MDNavigationItemLabel:
text: "GitHub"
MDLabel:
MDNavigationItem:
MDNavigationItemIcon:
icon: "linkedin"
MDNavigationItemLabel:
text: "LinkedIN"
halign: "center"
'''
)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-styles-programming.png
:align: center
Example().run()
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
@ -146,48 +138,41 @@ 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
from kivymd.uix.navigationbar import (
MDNavigationBar,
MDNavigationItemIcon,
MDNavigationItem,
MDNavigationItemLabel,
)
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",
)
)
return MDNavigationBar(
MDNavigationItem(
MDNavigationItemIcon(
icon="gmail",
),
MDNavigationItemLabel(
text="Mail",
),
),
MDNavigationItem(
MDNavigationItemIcon(
icon="twitter",
),
MDNavigationItemLabel(
text="Twitter",
),
),
MDNavigationItem(
MDNavigationItemIcon(
icon="linkedin",
),
MDNavigationItemLabel(
text="LinkedIN",
),
),
)
@ -239,7 +224,7 @@ get to the desired id:
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.button import MDButton, MDButtonText
from kivymd.uix.floatlayout import MDFloatLayout
@ -248,18 +233,22 @@ get to the desired id:
return (
MDBoxLayout(
MDFloatLayout(
MDRaisedButton(
MDButton(
MDButtonText(
text="Button 1",
),
id="button_1",
text="Button 1",
pos_hint={"center_x": 0.5, "center_y": 0.5},
),
id="box_container_1",
),
MDBoxLayout(
MDFloatLayout(
MDRaisedButton(
MDButton(
MDButtonText(
text="Button 2",
),
id="button_2",
text="Button 2",
pos_hint={"center_x": 0.5, "center_y": 0.5},
),
id="float_container",
@ -271,13 +260,13 @@ get to the desired id:
def on_start(self):
# {
# 'box_container_1': <kivymd.uix.floatlayout.MDFloatLayout>,
# 'box_container_2': <kivymd.uix.boxlayout.MDBoxLayout object>
# 'button_1': <kivymd.uix.button.button.MDButton object at 0x11d93c9e0>,
# 'button_2': <kivymd.uix.button.button.MDButton object at 0x11da128f0>,
# 'float_container': <kivymd.uix.floatlayout.MDFloatLayout object at 0x11da228f0>,
# 'box_container_1': <kivymd.uix.floatlayout.MDFloatLayout object at 0x11d9fc3c0>,
# 'box_container_2': <kivymd.uix.boxlayout.MDBoxLayout object at 0x11dbf06d0>,
# }
print(self.root.ids)
# <kivymd.uix.button.button.MDRaisedButton>
print(self.root.ids.box_container_2.ids.float_container.ids.button_2)
print(self.root.get_ids())
Example().run()
@ -293,6 +282,15 @@ from kivy.properties import StringProperty
from kivy.uix.widget import Widget
class _Dict(dict):
"""Implements access to dictionary values via a dot."""
def __getattr__(self, name):
return self[name]
# TODO: Add cleaning of the `__ids` collection when removing child widgets
# from the parent.
class DeclarativeBehavior:
"""
Implements the creation and addition of child widgets as declarative
@ -307,6 +305,8 @@ class DeclarativeBehavior:
and defaults to `''`.
"""
__ids = _Dict()
def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
@ -314,4 +314,12 @@ class DeclarativeBehavior:
if issubclass(child.__class__, Widget):
self.add_widget(child)
if hasattr(child, "id") and child.id:
self.ids[child.id] = child
self.__ids[child.id] = child
def get_ids(self) -> dict:
"""
Returns a dictionary of widget IDs defined in Python
code that is written in a declarative style.
"""
return self.__ids

View file

@ -31,7 +31,7 @@ For example, let's create a button with a rectangular elevation effect:
)
KV = '''
<RectangularElevationButton>
<ElevationWidget>
size_hint: None, None
size: "250dp", "50dp"
@ -39,19 +39,19 @@ For example, let's create a button with a rectangular elevation effect:
MDScreen:
# With elevation effect
RectangularElevationButton:
ElevationWidget:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 4
shadow_offset: 0, -6
shadow_softness: 4
# Without elevation effect
RectangularElevationButton:
ElevationWidget:
pos_hint: {"center_x": .5, "center_y": .4}
'''
class RectangularElevationButton(
class ElevationWidget(
RectangularRippleBehavior,
CommonElevationBehavior,
ButtonBehavior,
@ -84,7 +84,7 @@ For example, let's create a button with a rectangular elevation effect:
from kivymd.uix.screen import MDScreen
class RectangularElevationButton(
class ElevationWidget(
RectangularRippleBehavior,
CommonElevationBehavior,
ButtonBehavior,
@ -101,13 +101,13 @@ For example, let's create a button with a rectangular elevation effect:
def build(self):
return (
MDScreen(
RectangularElevationButton(
ElevationWidget(
pos_hint={"center_x": .5, "center_y": .6},
elevation=4,
shadow_softness=4,
shadow_offset=(0, -6),
),
RectangularElevationButton(
ElevationWidget(
pos_hint={"center_x": .5, "center_y": .4},
),
)
@ -271,7 +271,7 @@ Animating the elevation
size: 100, 100
md_bg_color: 0, 0, 1, 1
elevation: 2
radius: 18
radius: dp(18)
'''
@ -306,6 +306,7 @@ Animating the elevation
from kivy.animation import Animation
from kivy.uix.behaviors import ButtonBehavior
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.behaviors import CommonElevationBehavior, RectangularRippleBehavior
@ -341,7 +342,7 @@ Animating the elevation
size=(100, 100),
md_bg_color="blue",
elevation=2,
radius=18,
radius=dp(18),
)
)
)
@ -355,23 +356,17 @@ Animating the elevation
from __future__ import annotations
__all__ = (
"CommonElevationBehavior",
"RectangularElevationBehavior",
"CircularElevationBehavior",
"RoundedRectangularElevationBehavior",
"FakeRectangularElevationBehavior",
"FakeCircularElevationBehavior",
)
__all__ = ("CommonElevationBehavior",)
from kivy import Logger
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BoundedNumericProperty,
ColorProperty,
ListProperty,
NumericProperty,
VariableListProperty,
DictProperty,
)
from kivy.uix.widget import Widget
@ -393,18 +388,15 @@ Builder.load_string(
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
rgba: root.shadow_color
BoxShadow:
pos: self.pos
pos: self.pos if not isinstance(self, RelativeLayout) else (0, 0)
size: self.size
offset: root.shadow_offset
spread_radius: -(root.shadow_softness), -(root.shadow_softness)
blur_radius: root.elevation * 10
blur_radius: root.elevation_levels[root.elevation_level]
border_radius:
(root.radius if hasattr(self, "radius") else [0, 0, 0, 0]) \
(root.radius if hasattr(self, "radius") and root.radius else [0, 0, 0, 0]) \
if root.shadow_radius == [0.0, 0.0, 0.0, 0.0] else \
root.shadow_radius
canvas.after:
@ -421,6 +413,36 @@ class CommonElevationBehavior(Widget):
class documentation.
"""
elevation_level = BoundedNumericProperty(0, min=0, max=5)
"""
Elevation level (values from 0 to 5)
.. versionadded:: 1.2.0
:attr:`elevation_level` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `0`.
"""
elevation_levels = DictProperty(
{
0: 0,
1: dp(8),
2: dp(12),
3: dp(16),
4: dp(20),
5: dp(24),
}
)
"""
Elevation is measured as the distance between components along the z-axis
in density-independent pixels (dps).
.. versionadded:: 1.2.0
:attr:`elevation_levels` is an :class:`~kivy.properties.DictProperty`
and defaults to `{0: dp(0), 1: dp(8), 2: dp(23), 3: dp(16), 4: dp(20), 5: dp(24)}`.
"""
elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
"""
Elevation of the widget.
@ -449,7 +471,7 @@ class CommonElevationBehavior(Widget):
MDScreen:
MDCard:
radius: 12, 46, 12, 46
radius: dp(12), dp(46), dp(12), dp(46)
size_hint: .5, .3
pos_hint: {"center_x": .5, "center_y": .5}
elevation: 2
@ -486,26 +508,26 @@ class CommonElevationBehavior(Widget):
from kivymd.uix.behaviors import BackgroundColorBehavior, CommonElevationBehavior
KV = '''
<RectangularElevationButton>
<ElevationWidget>
size_hint: None, None
size: "250dp", "50dp"
MDScreen:
RectangularElevationButton:
ElevationWidget:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 6
shadow_softness: 6
RectangularElevationButton:
ElevationWidget:
pos_hint: {"center_x": .5, "center_y": .4}
elevation: 6
shadow_softness: 12
'''
class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior):
class ElevationWidget(CommonElevationBehavior, BackgroundColorBehavior):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = "blue"
@ -522,19 +544,7 @@ class CommonElevationBehavior(Widget):
: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`.
and defaults to `0.0`.
"""
shadow_offset = ListProperty((0, 0))
@ -551,23 +561,23 @@ class CommonElevationBehavior(Widget):
from kivymd.uix.behaviors import BackgroundColorBehavior, CommonElevationBehavior
KV = '''
<RectangularElevationButton>
<ElevationWidget>
size_hint: None, None
size: "100dp", "100dp"
MDScreen:
RectangularElevationButton:
ElevationWidget:
pos_hint: {"center_x": .5, "center_y": .5}
elevation: 6
shadow_radius: 6
shadow_radius: dp(6)
shadow_softness: 12
shadow_offset: -12, -12
'''
class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior):
class ElevationWidget(CommonElevationBehavior, BackgroundColorBehavior):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = "blue"
@ -585,7 +595,7 @@ class CommonElevationBehavior(Widget):
.. code-block:: kv
RectangularElevationButton:
ElevationWidget:
shadow_offset: 12, -12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-2.png
@ -593,7 +603,7 @@ class CommonElevationBehavior(Widget):
.. code-block:: kv
RectangularElevationButton:
ElevationWidget:
shadow_offset: 12, 12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-3.png
@ -601,7 +611,7 @@ class CommonElevationBehavior(Widget):
.. code-block:: kv
RectangularElevationButton:
ElevationWidget:
shadow_offset: -12, 12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-4.png
@ -619,7 +629,7 @@ class CommonElevationBehavior(Widget):
.. code-block:: python
RectangularElevationButton:
ElevationWidget:
shadow_color: 0, 0, 1, .8
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-color.png
@ -691,82 +701,10 @@ class CommonElevationBehavior(Widget):
and defaults to `(0, 0, 1)`.
"""
_elevation = 0
# _elevation = 0
_elevation_level = 0
_shadow_softness = 0
_shadow_color = (0, 0, 0, 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."
)
# def on_elevation(self, instance, value) -> None:
# self._elevation = value

View file

@ -15,7 +15,7 @@ Usage
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularElevationBehavior
from kivymd.uix.behaviors import CommonElevationBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.behaviors.focus_behavior import FocusBehavior
@ -36,7 +36,7 @@ Usage
'''
class FocusWidget(MDBoxLayout, RectangularElevationBehavior, FocusBehavior):
class FocusWidget(MDBoxLayout, CommonElevationBehavior, FocusBehavior):
pass
@ -65,25 +65,26 @@ Color change at focus/defocus
__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):
class FocusBehavior(HoverBehavior):
"""
Focus behavior class.
For more information, see in the :class:`~kivymd.uix.behavior.HoverBehavior`
and :class:`~kivy.uix.button.ButtonBehavior` classes documentation.
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
Fired 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
Fired when the mouse exits the widget AND the widget is visible.
"""
focus_behavior = BooleanProperty(True)
@ -109,39 +110,3 @@ class FocusBehavior(HoverBehavior, ButtonBehavior):
: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

@ -27,9 +27,13 @@ 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.
: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
RelativeLayout, 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`.
To get the legacy behavior that the events are always triggered, you can
set `detect_visible` on the Widget to `False`.
.. code-block:: python
@ -40,13 +44,14 @@ the widget.
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
Screen
MDScreen
md_bg_color: self.theme_cls.backgroundColor
MDBoxLayout:
id: box
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: .8, .8
md_bg_color: app.theme_cls.bg_darkest
md_bg_color: self.theme_cls.secondaryContainerColor
'''
@ -54,19 +59,23 @@ the widget.
'''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.'''
'''
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)
self.md_bg_color = "white"
def on_leave(self, *args):
'''The method will be called when the mouse cursor goes beyond
the borders of the current widget.'''
'''
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
self.md_bg_color = self.theme_cls.secondaryContainerColor
class Test(MDApp):
class Example(MDApp):
def build(self):
self.screen = Builder.load_string(KV)
for i in range(5):
@ -74,10 +83,9 @@ the widget.
return self.screen
Test().run()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hover-behavior.gif
:width: 250 px
:align: center
"""
@ -85,23 +93,25 @@ __all__ = ("HoverBehavior",)
from kivy.core.window import Window
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.widget import Widget
class HoverBehavior(object):
class HoverBehavior:
"""
:Events:
:attr:`on_enter`
Called when mouse enters the bbox of the widget AND the widget is visible
Fired 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
Fired 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
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`.
@ -109,7 +119,7 @@ class HoverBehavior(object):
hover_visible = BooleanProperty(False)
"""
`True` if hovering is True AND is the current widget is visible
`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`.
@ -118,7 +128,7 @@ class HoverBehavior(object):
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
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`.
@ -132,22 +142,24 @@ class HoverBehavior(object):
and defaults to `True`.
"""
def __init__(self, **kwargs):
def __init__(self, *args, **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)
super().__init__(*args, **kwargs)
def on_mouse_update(self, *args):
# If the Widget currently has no parent, do nothing
# 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)):
# 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)
if not isinstance(self, RelativeLayout)
else (pos[0], pos[1])
):
self.hovering = False
self.enter_point = None
if self.hover_visible:
@ -155,72 +167,59 @@ class HoverBehavior(object):
self.dispatch("on_leave")
return
#
# The pointer is in the same position as the widget
#
# 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
#
# 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
#
# 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
# 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
# using both local and global coordinates to cover absolute
# 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
# The collide_point will error when you reach the root
# Window.
break
if not pinside:
self.hover_visible = False
break
# Iterate upwards
# 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
# 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 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
# 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
# This means that the current widget is covered by a modal
# or popup.
self.hover_visible = False
break
if self.hover_visible:
@ -228,7 +227,7 @@ class HoverBehavior(object):
self.dispatch("on_enter")
def on_enter(self):
"""Called when mouse enters the bbox of the widget AND the widget is visible."""
"""Fired when mouse enter the bbox of the widget."""
def on_leave(self):
"""Called when the mouse exits the widget AND the widget is visible."""
"""Fired when the mouse goes outside the widget border."""

View file

@ -15,17 +15,20 @@ as dialogs, dropdown menu, snack bars, and so on.
__all__ = (
"MotionBase",
"MotionExtendedFabButtonBehavior",
"MotionDropDownMenuBehavior",
"MotionDialogBehavior",
"MotionShackBehavior",
"MotionDatePickerBehavior",
"MotionTimePickerBehavior",
)
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
from kivy.metrics import dp
from kivy.properties import StringProperty, NumericProperty, ObjectProperty
from kivymd.uix.behaviors.scale_behavior import ScaleBehavior
class MotionBase:
@ -166,80 +169,288 @@ class MotionDropDownMenuBehavior(MotionBase):
self.scale_value_y = value
class MotionDialogBehavior(MotionBase):
class MotionExtendedFabButtonBehavior(MotionBase):
"""
Base class for dialog movement behavior.
Base class for extended Fab button movement behavior.
For more information, see in the :class:`~MotionBase` class documentation.
"""
show_duration = NumericProperty(0.1)
show_transition = StringProperty("out_circ")
"""
The type of transition of the widget opening.
:attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_circ'`.
"""
shift_transition = StringProperty("out_sine")
"""
Text label transition.
:attr:`shift_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_sine'`.
"""
show_duration = NumericProperty(0.3)
"""
Duration of widget display transition.
:attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.1`.
and defaults to `0.3`.
"""
scale_x = NumericProperty(1.5)
hide_transition = StringProperty("out_sine")
"""
Default X-axis scaling values.
The type of transition of the widget closing.
:attr:`scale_x` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1.5`.
:attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'linear'`.
"""
scale_y = NumericProperty(1.5)
hide_duration = NumericProperty(0.2)
"""
Default Y-axis scaling values.
Duration of widget closing transition.
:attr:`scale_y` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1.5`.
:attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_default_values()
_x = NumericProperty(0)
_anim_opacity = None
def set_default_values(self):
"""Sets default scaled and transparency values."""
def collapse(self, *args) -> None:
"""Collapses the button."""
self.scale_value_x = self.scale_x
self.scale_value_y = self.scale_y
self.opacity = 0
def collapse(*args):
if self._label and self._icon:
Animation(_x=0, d=self.hide_duration).start(self)
anim = Animation(
width=dp(56), d=self.hide_duration, t=self.hide_transition
)
anim.bind(on_progress=self._check_collapse_progress)
anim.start(self)
Clock.schedule_once(collapse)
def expand(self, *args) -> None:
"""Expands the button."""
def expand(*args):
if self._label and self._icon:
anim = Animation(
width=self.width
+ self._label.texture_size[0]
+ (dp(18) if self._icon else 0),
d=self.show_duration,
t=self.show_transition,
)
anim.bind(on_progress=self._check_expand_progress)
anim.start(self)
Animation(
_x=dp(12), d=self.show_duration, t=self.shift_transition
).start(self)
Clock.schedule_once(expand)
def set_opacity_text_button(self, value: int) -> None:
if self._label:
self._anim_opacity = Animation(
opacity=value,
d=self.show_duration * 16.666666666666668 / 100
if value
else self.show_duration * 1.6666666666666667 / 100,
)
self._anim_opacity.bind(
on_complete=lambda *x: setattr(self, "_anim_opacity", None)
)
self._anim_opacity.start(self._label)
def _check_collapse_progress(self, animation, instance, progress) -> None:
if progress > 0.1:
if not self._anim_opacity:
self.set_opacity_text_button(0)
def _check_expand_progress(self, animation, instance, progress) -> None:
if progress > 0.3:
if not self._anim_opacity:
self.set_opacity_text_button(1)
class MotionDialogBehavior(ScaleBehavior, MotionBase):
"""
Base class for dialog movement behavior.
For more information, see in the
:class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior`
:class:`~MotionBase`
classes documentation.
"""
show_transition = StringProperty("out_expo")
"""
The type of transition of the widget opening.
:attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_expo'`.
"""
show_button_container_transition = StringProperty("out_circ")
"""
The type of transition of the widget opening.
:attr:`show_button_container_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_circ'`.
"""
hide_transition = StringProperty("out_circ")
"""
The type of transition of the widget opening.
:attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'hide_transition'`.
"""
show_duration = NumericProperty(0.4)
"""
Duration of widget display transition.
:attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
def on_dismiss(self, *args):
"""Called when a dialog closed."""
"""Fired when a dialog closed."""
self.set_default_values()
def remove_dialog(*args):
Window.remove_widget(self)
if self._scrim:
Window.remove_widget(self._scrim)
def on_open(self, *args):
"""Called when a dialog opened."""
if self._scrim:
Animation(alpha=0, d=self.hide_duration).start(self._scrim)
Animation(
opacity=1,
scale_value_x=1,
scale_value_y=1,
t=self.show_transition,
d=self.show_duration,
).start(self)
y=self.ids.content_container.y,
t=self.hide_transition,
d=self.hide_duration,
).start(self.ids.button_container)
anim = Animation(
opacity=0,
scale_value_y=0,
t=self.hide_transition,
d=self.hide_duration,
)
anim.bind(on_complete=remove_dialog)
anim.start(self)
def on_open(self, *args):
"""Fired when a dialog opened."""
def open(*args):
self.scale_value_y = 0
self.scale_value_center = (0, self.center[1] + self.height / 2)
Animation(
opacity=1,
scale_value_y=1,
t=self.show_transition,
d=self.show_duration,
).start(self)
Animation(
y=dp(24),
t=self.show_button_container_transition,
d=self.show_duration + 0.15,
).start(self.ids.button_container)
if self._scrim:
Animation(alpha=0.4, d=self.show_duration).start(self._scrim)
Clock.schedule_once(open)
class MotionShackBehavior(StencilBehavior, MotionBase):
class MotionPickerBehavior(MotionDialogBehavior):
"""
Base class for date/time pickers movement behavior.
For more information, see in the
:class:`~MotionDialogBehavior` class documentation.
"""
_scrim = ObjectProperty()
def on_open(self, *args):
"""Fired when a dialog opened."""
def open(*args):
self.scale_value_y = 0
self.scale_value_center = (0, self.center[1] + self.height / 2)
Animation(
opacity=1,
scale_value_y=1,
t=self.show_transition,
d=self.show_duration,
).start(self)
if self._scrim:
Animation(alpha=0.4, d=self.show_duration).start(self._scrim)
Clock.schedule_once(open)
def on_dismiss(self, *args):
"""Fired when a dialog closed."""
def remove_dialog(*args):
Window.remove_widget(self)
if self._scrim:
Window.remove_widget(self._scrim)
self.dispatch("on_dismiss")
if self._scrim:
Animation(alpha=0, d=self.hide_duration).start(self._scrim)
anim = Animation(
opacity=0,
scale_value_y=0,
t=self.hide_transition,
d=self.hide_duration,
)
anim.bind(on_complete=remove_dialog)
anim.start(self)
class MotionTimePickerBehavior(MotionPickerBehavior):
"""
Base class for time picker movement behavior.
For more information, see in the
:class:`~MotionPickerBehavior` class documentation.
"""
class MotionDatePickerBehavior(MotionPickerBehavior):
"""
Base class for date picker movement behavior.
For more information, see in the
:class:`~MotionPickerBehavior` class documentation.
"""
class MotionShackBehavior(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.
:class:`~MotionBase` class documentation.
"""
_interval = 0
_height = 0
def on_dismiss(self, *args):
"""Called when a snackbar closed."""
"""Fired when a snackbar closed."""
def remove_snackbar(*args):
Window.parent.remove_widget(self)
@ -257,7 +468,7 @@ class MotionShackBehavior(StencilBehavior, MotionBase):
anim.start(self)
def on_open(self, *args):
"""Called when a snackbar opened."""
"""Fired when a snackbar opened."""
def open(*args):
self._height = self.height
@ -274,9 +485,9 @@ class MotionShackBehavior(StencilBehavior, MotionBase):
)
)
anim.start(self)
self.dispatch("on_open")
Clock.schedule_once(open)
self.dispatch("on_open")
Clock.schedule_once(open, 0.2)
def _wait_interval(self, interval):
self._interval += interval

View file

@ -285,11 +285,18 @@ class CommonRipple:
and defaults to `'ripple_func_out'`.
"""
ripple_effect = BooleanProperty(True)
"""
Should I use the ripple effect.
:attr:`ripple_effect` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
_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:
@ -333,6 +340,8 @@ class CommonRipple:
anim.start(self)
def anim_complete(self, *args) -> None:
"""Fired when the "fade_out" animation complete."""
self._doing_ripple = False
self._finishing_ripple = False
self._fading_out = False
@ -378,7 +387,7 @@ class CommonRipple:
if self.ripple_color:
pass
elif hasattr(self, "theme_cls"):
self.ripple_color = self.theme_cls.ripple_color
self.ripple_color = self.theme_cls.rippleColor
else:
# If no theme, set Gray 300.
self.ripple_color = [
@ -429,7 +438,11 @@ class RectangularRippleBehavior(CommonRipple):
"""
def lay_canvas_instructions(self) -> None:
if self._no_ripple_effect:
"""
Adds graphic instructions to the canvas to implement ripple animation.
"""
if not self.ripple_effect:
return
with self.canvas.after if self.ripple_canvas_after else self.canvas.before:
@ -493,7 +506,7 @@ class CircularRippleBehavior(CommonRipple):
"""
def lay_canvas_instructions(self) -> None:
if self._no_ripple_effect:
if not self.ripple_effect:
return
with self.canvas.after if self.ripple_canvas_after else self.canvas.before:

View file

@ -0,0 +1,537 @@
# TODO: Add docs.
"""
Behaviors/State Layer
=====================
.. seealso::
`Material Design spec, State layers <https://m3.material.io/foundations/interaction/states/state-layers>`_
"""
from kivy import platform
from kivy.lang import Builder
from kivy.properties import ColorProperty, NumericProperty
from kivymd.uix.behaviors.focus_behavior import FocusBehavior
Builder.load_string(
"""
<StateLayerBehavior>
canvas.after:
Color
rgba: self.state_layer_color
RoundedRectangle:
group: "State_layer_instruction"
size: self.size
pos: self.pos
radius: self.radius if hasattr(self, "radius") else [0, ]
""",
filename="StateLayerBehavior.kv",
)
# TODO: Add methods `set_text_color` and `set_icon_color`
# (methods that set the color of text and icons in the state
# `on_enter` and `on_leave` and `pressed`).
class StateLayerBehavior(FocusBehavior):
state_layer_color = ColorProperty([0, 0, 0, 0])
"""
The color of the layer state.
:attr:`state_layer_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0]`.
"""
state_hover = NumericProperty(0.08)
"""
The transparency level of the layer as a percentage when hovering.
:attr:`state_hover` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.08`.
"""
state_press = NumericProperty(0.12)
"""
The transparency level of the layer as a percentage when pressed.
:attr:`state_press` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.12`.
"""
state_drag = NumericProperty(0.16)
"""
The transparency level of the layer as a percentage when dragged.
:attr:`state_drag` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.16`.
"""
# The transparency value of the disabled state.
# These values are specified in the M3 specification.
# -------------------------------------------------------------------------
# MDIconButton
# -------------------------------------------------------------------------
# Filled.
icon_button_filled_opacity_value_disabled_container = NumericProperty(0.12)
icon_button_filled_opacity_value_disabled_icon = NumericProperty(0.38)
# Tonal.
icon_button_tonal_opacity_value_disabled_container = NumericProperty(0.12)
icon_button_tonal_opacity_value_disabled_icon = NumericProperty(0.38)
# Outlined.
icon_button_outlined_opacity_value_disabled_container = NumericProperty(
0.12
)
icon_button_outlined_opacity_value_disabled_line = NumericProperty(0.12)
icon_button_outlined_opacity_value_disabled_icon = NumericProperty(0.38)
# Standard.
icon_button_standard_opacity_value_disabled_icon = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDFabButton
# -------------------------------------------------------------------------
fab_button_opacity_value_disabled_container = NumericProperty(0.12)
fab_button_opacity_value_disabled_icon = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDButton
# -------------------------------------------------------------------------
# Filled.
button_filled_opacity_value_disabled_container = NumericProperty(0.12)
button_filled_opacity_value_disabled_icon = NumericProperty(0.38)
button_filled_opacity_value_disabled_text = NumericProperty(0.38)
# Tonal.
button_tonal_opacity_value_disabled_container = NumericProperty(0.12)
button_tonal_opacity_value_disabled_icon = NumericProperty(0.38)
button_tonal_opacity_value_disabled_text = NumericProperty(0.38)
# Outlined.
button_outlined_opacity_value_disabled_container = NumericProperty(0.12)
button_outlined_opacity_value_disabled_line = NumericProperty(0.12)
button_outlined_opacity_value_disabled_icon = NumericProperty(0.38)
button_outlined_opacity_value_disabled_text = NumericProperty(0.38)
# Elevated.
button_elevated_opacity_value_disabled_container = NumericProperty(0.12)
button_elevated_opacity_value_disabled_icon = NumericProperty(0.38)
button_elevated_opacity_value_disabled_text = NumericProperty(0.38)
# Text.
button_text_opacity_value_disabled_icon = NumericProperty(0.38)
button_text_opacity_value_disabled_text = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDLabel
# -------------------------------------------------------------------------
label_opacity_value_disabled_text = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDCard
# -------------------------------------------------------------------------
card_filled_opacity_value_disabled_state_container = NumericProperty(0.38)
card_outlined_opacity_value_disabled_state_container = NumericProperty(0.12)
card_opacity_value_disabled_state_elevated_container = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDSegmentedButton
# -------------------------------------------------------------------------
segmented_button_opacity_value_disabled_container = NumericProperty(0.12)
segmented_button_opacity_value_disabled_container_active = NumericProperty(
0.38
)
segmented_button_opacity_value_disabled_line = NumericProperty(0.12)
segmented_button_opacity_value_disabled_icon = NumericProperty(0.38)
segmented_button_opacity_value_disabled_text = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDChip
# -------------------------------------------------------------------------
chip_opacity_value_disabled_container = NumericProperty(0.12)
chip_opacity_value_disabled_text = NumericProperty(0.38)
chip_opacity_value_disabled_icon = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDSwitch
# -------------------------------------------------------------------------
switch_opacity_value_disabled_line = NumericProperty(0.12)
switch_opacity_value_disabled_container = NumericProperty(0.12)
switch_thumb_opacity_value_disabled_container = NumericProperty(0.38)
switch_opacity_value_disabled_icon = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDCheckbox
# -------------------------------------------------------------------------
checkbox_opacity_value_disabled_container = NumericProperty(0.38)
# -------------------------------------------------------------------------
# List
# -------------------------------------------------------------------------
list_opacity_value_disabled_container = NumericProperty(0.38)
list_opacity_value_disabled_leading_avatar = NumericProperty(0.38)
# -------------------------------------------------------------------------
# MDTextField
# -------------------------------------------------------------------------
text_field_filled_opacity_value_disabled_state_container = NumericProperty(
0.18
)
text_field_outlined_opacity_value_disabled_state_container = (
NumericProperty(0)
)
text_field_opacity_value_disabled_max_length_label = NumericProperty(0.60)
text_field_opacity_value_disabled_helper_text_label = NumericProperty(0.60)
text_field_opacity_value_disabled_hint_text_label = NumericProperty(0.60)
text_field_opacity_value_disabled_leading_icon = NumericProperty(0.60)
text_field_opacity_value_disabled_trailing_icon = NumericProperty(0.60)
text_field_opacity_value_disabled_line = NumericProperty(0.12)
_state = 0.0
_bg_color = (0, 0, 0, 0)
_is_already_disabled = False
_shadow_softness = [0, 0]
_elevation_level = 0
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
def set_properties_widget(self) -> None:
"""Fired `on_release/on_press/on_enter/on_leave` events."""
if not self.disabled:
self._restore_properties()
self._set_state_layer_color()
def on_disabled(self, instance, value) -> None:
"""Fired when the `disabled` value changes."""
from kivymd.uix.card import MDCard
from kivymd.uix.button import (
MDIconButton,
MDButton,
MDFabButton,
MDExtendedFabButton,
)
from kivymd.uix.segmentedbutton import MDSegmentedButtonItem
from kivymd.uix.segmentedbutton.segmentedbutton import (
MDSegmentButtonSelectedIcon,
)
from kivymd.uix.selectioncontrol import MDSwitch
from kivymd.uix.list import BaseListItem
from kivymd.uix.textfield import MDTextField
if value and not self._is_already_disabled:
self._is_already_disabled = True
if isinstance(self, MDCard):
self.state_layer_color = (
{
"filled": self.theme_cls.surfaceColor[:-1]
+ [
self.card_filled_opacity_value_disabled_state_container
],
"outlined": self.theme_cls.outlineColor[:-1]
+ [
self.card_outlined_opacity_value_disabled_state_container
],
"elevated": self.theme_cls.surfaceVariantColor[:-1]
+ [
self.card_opacity_value_disabled_state_elevated_container
],
}[self.style]
if not self.md_bg_color_disabled
else self.md_bg_color_disabled
)
elif isinstance(self, MDIconButton):
self.state_layer_color = (
{
"tonal": self.theme_cls.onSurfaceColor[:-1]
+ [
self.icon_button_tonal_opacity_value_disabled_container
],
"filled": self.theme_cls.onSurfaceColor[:-1]
+ [
self.icon_button_filled_opacity_value_disabled_container
],
"outlined": self.theme_cls.onSurfaceColor[:-1]
+ [
self.icon_button_outlined_opacity_value_disabled_container
],
"standard": self.theme_cls.transparentColor,
}[self.style]
if not self.md_bg_color_disabled
else self.md_bg_color_disabled
)
elif isinstance(self, MDButton):
self.state_layer_color = (
{
"elevated": self.theme_cls.onSurfaceColor[:-1]
+ [
self.button_elevated_opacity_value_disabled_container
],
"tonal": self.theme_cls.onSurfaceColor[:-1]
+ [self.button_tonal_opacity_value_disabled_container],
"filled": self.theme_cls.onSurfaceColor[:-1]
+ [self.button_filled_opacity_value_disabled_container],
"outlined": self.theme_cls.onSurfaceColor[:-1]
+ [
self.button_outlined_opacity_value_disabled_container
],
"text": self.theme_cls.transparentColor,
}[self.style]
if not self.md_bg_color_disabled
else self.md_bg_color_disabled
)
elif isinstance(self, (MDFabButton, MDExtendedFabButton)):
self.state_layer_color = (
self.theme_cls.onSurfaceColor[:-1]
+ [self.fab_button_opacity_value_disabled_container]
if not self.md_bg_color_disabled
else self.md_bg_color_disabled
)
elif isinstance(self, MDTextField):
if self.mode == "filled":
self.state_layer_color = self.theme_cls.onSurfaceColor[
:-1
] + [
self.text_field_filled_opacity_value_disabled_state_container
]
else:
self.state_layer_color = self.theme_cls.transparentColor
elif isinstance(self.parent, MDSegmentedButtonItem):
self.state_layer_color = (
self.theme_cls.onSurfaceColor[:-1]
+ [self.segmented_button_opacity_value_disabled_container]
if not self.parent.md_bg_color_disabled
else self.parent.md_bg_color_disabled
)
elif isinstance(self, MDSwitch):
self.state_layer_color = (
(
self.theme_cls.surfaceContainerHighestColor
if not self.active
else self.theme_cls.onSurfaceColor
)[:-1]
+ [self.switch_opacity_value_disabled_container]
if not self.md_bg_color_disabled
else self.md_bg_color_disabled
)
elif isinstance(self, BaseListItem):
self.state_layer_color = (
self.theme_cls.onSurfaceColor[:-1]
+ [self.list_opacity_value_disabled_container]
if not self.md_bg_color_disabled
else self.md_bg_color_disabled
)
elif not value and self._is_already_disabled:
self.state_layer_color = self.theme_cls.transparentColor
self._is_already_disabled = False
def on_enter(self) -> None:
"""Fired when mouse enter the bbox of the widget."""
self._state = self.state_hover
self.set_properties_widget()
def on_leave(self) -> None:
"""Fired when the mouse goes outside the widget border."""
self._state = 0.0
self.set_properties_widget()
def _on_release(self, *args):
"""
Fired when the button is released
(i.e. the touch/click that pressed the button goes away).
"""
if platform in ["android", "ios"]:
self._state = 0.0
self.set_properties_widget()
else:
self.on_enter()
def _on_press(self, *args):
"""Fired when the button is pressed."""
self._state = self.state_press
self.set_properties_widget()
def _restore_properties(self):
if self._state == self.state_hover and self.focus_behavior:
if hasattr(self, "elevation_level"):
self._elevation_level = self.elevation_level
if hasattr(self, "shadow_softness"):
self._shadow_softness = self.shadow_softness
if hasattr(self, "md_bg_color"):
self._bg_color = self.md_bg_color
elif not self._state:
if hasattr(self, "elevation_level"):
self.elevation_level = self._elevation_level
if hasattr(self, "shadow_softness"):
self.shadow_softness = self._shadow_softness
if hasattr(self, "bg_color"):
self.bg_color = self._md_bg_color
# FIXME: For some widgets, the color of the state of its elements is
# ignored. For example, for the `MDSwitch` widget, the color of the status
# of the `Thumb` element and the color of the icon are ignored.
def _get_target_color(self):
from kivymd.uix.card import MDCard
from kivymd.uix.button import (
MDIconButton,
MDButton,
MDFabButton,
MDExtendedFabButton,
)
from kivymd.uix.segmentedbutton.segmentedbutton import (
MDSegmentedButtonContainer,
)
from kivymd.uix.chip import MDChip
from kivymd.uix.selectioncontrol import MDSwitch, MDCheckbox
from kivymd.uix.list import BaseListItem
from kivymd.uix.textfield import MDTextField
from kivymd.uix.navigationdrawer import MDNavigationDrawerItem
target_color = None
if not self.disabled:
self._restore_properties()
if isinstance(self, MDTextField):
if self.mode == "filled":
target_color = self.theme_cls.onSurfaceColor
else:
target_color = self.theme_cls.transparentColor
elif isinstance(self, (MDCard, BaseListItem)) and not isinstance(
self, MDNavigationDrawerItem
):
target_color = self.theme_cls.onSurfaceColor
elif isinstance(self, MDNavigationDrawerItem):
target_color = self.theme_cls.onSecondaryContainerColor
elif isinstance(self.parent, MDSegmentedButtonContainer):
target_color = (
self.theme_cls.onSurfaceColor
if not self.active
else self.theme_cls.onSecondaryContainerColor
)
elif isinstance(self, MDChip):
# Here, depending on the widget state (focus/pressed...)
# we set the target color of the widget's layer.
# For example:
#
# if self._state == self.state_press:
# target_color = [., ., ., .]
# else:
# ...
if self.type == "assist":
target_color = self.theme_cls.onSurfaceColor
elif self.type in ["filter", "input", "suggestion"]:
target_color = self.theme_cls.onSurfaceVariantColor
elif isinstance(self, MDIconButton):
if self.style == "filled":
target_color = self.theme_cls.onPrimaryColor
elif self.style == "tonal":
target_color = self.theme_cls.onSecondaryContainerColor
elif self.style in ["outlined", "standard"]:
target_color = self.theme_cls.onSurfaceVariantColor
elif isinstance(self, MDButton):
target_color = (
self.theme_cls.onPrimaryColor
if self.style == "filled"
else self.theme_cls.primaryColor
)
elif isinstance(self, MDCheckbox):
target_color = (
self.theme_cls.primaryColor
if self.active
else self.theme_cls.onSurfaceColor
)
elif isinstance(self, (MDFabButton, MDExtendedFabButton)):
target_color = self.theme_cls.onPrimaryContainerColor
elif isinstance(self, MDSwitch):
target_color = (
self.theme_cls.primaryColor
if self.active
else self.theme_cls.onSurfaceVariantColor
)
else:
target_color = self.theme_cls.onSurfaceColor
return target_color
def _set_state_layer_color(self):
from kivymd.uix.card import MDCard
from kivymd.uix.button import (
MDIconButton,
MDButton,
MDFabButton,
MDExtendedFabButton,
)
from kivymd.uix.segmentedbutton.segmentedbutton import (
MDSegmentedButtonContainer,
)
from kivymd.uix.chip import MDChip
from kivymd.uix.selectioncontrol import MDSwitch, MDCheckbox
from kivymd.uix.list import BaseListItem
from kivymd.uix.textfield import MDTextField
from kivymd.uix.tab.tab import MDTabsItemBase
target_color = self._get_target_color()
if (
isinstance(
self,
(
MDCard,
MDTextField,
MDIconButton,
MDButton,
MDFabButton,
MDExtendedFabButton,
MDChip,
MDSwitch,
MDCheckbox,
BaseListItem,
MDTabsItemBase,
),
)
or isinstance(self.parent, MDSegmentedButtonContainer)
and target_color
):
if self._state == self.state_hover and self.focus_behavior:
if (
not self.focus_color
or self.theme_cls.dynamic_color
and self.theme_focus_color == "Primary"
):
if (
isinstance(self, MDTextField)
and self.mode == "outlined"
):
self.state_layer_color = target_color
else:
self.state_layer_color = target_color[:-1] + [
self._state
]
else:
self.state_layer_color = self.focus_color
elif self._state == self.state_press:
self.state_layer_color = target_color[:-1] + [self._state]
elif not self._state:
self.state_layer_color = target_color[:-1] + [self._state]

View file

@ -63,7 +63,7 @@ KivyMD
#:import images_path kivymd.images_path
MDCarousel:
Carousel:
StencilImage:
size_hint: .9, .8

View file

@ -125,25 +125,16 @@ You can inherit the ``MyToggleButton`` class only from the following classes
- :class:`~kivymd.uix.button.MDFillRoundFlatIconButton`
"""
__all__ = ("MDToggleButton",)
__all__ = ("MDToggleButtonBehavior",)
from kivy import Logger
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,
)
from kivymd.uix.button import MDButton, MDIconButton, MDFabButton, BaseButton
class MDToggleButton(ToggleButtonBehavior):
class MDToggleButtonBehavior(ToggleButtonBehavior):
background_normal = ColorProperty(None)
"""
Color of the button in ``rgba`` format for the 'normal' state.
@ -180,38 +171,29 @@ class MDToggleButton(ToggleButtonBehavior):
def __init__(self, **kwargs):
super().__init__(**kwargs)
classinfo = (
MDRaisedButton,
MDFlatButton,
MDRectangleFlatButton,
MDRectangleFlatIconButton,
MDRoundFlatButton,
MDRoundFlatIconButton,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
)
classinfo = (MDButton, MDIconButton, MDFabButton)
# 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}"
)
else:
print(666, self.md_bg_color)
# self.theme_bg_color = "Custom"
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,
),
):
if isinstance(self, BaseButton):
print(111)
self.__is_filled = True
self.background_normal = self.theme_cls.primary_color
self.background_normal = (
"yellow" # self.theme_cls.primary_color
)
# If not background_normal must be the same as the inherited one.
else:
print(222)
self.background_normal = (
self.md_bg_color[:] if self.md_bg_color else (0, 0, 0, 0)
)
@ -228,25 +210,44 @@ class MDToggleButton(ToggleButtonBehavior):
# self.bind(state=self._update_bg)
self.fbind("state", self._update_bg)
def _update_bg(self, ins, val):
def _update_bg(self, instance, value):
"""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.theme_bg_color == "Primary":
self.theme_bg_color = "Custom"
if self.theme_icon_color == "Primary":
self.theme_icon_color = "Custom"
if value == "down":
if isinstance(self, MDIconButton):
self.md_bg_color = self.theme_cls.primaryColor
self.icon_color = self.theme_cls.onPrimaryColor
# if (
# self.__is_filled is False
# ):
# 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 isinstance(self, MDIconButton):
self.md_bg_color = self.theme_cls.surfaceContainerHighestColor
self.icon_color = self.theme_cls.primaryColor
if issubclass(self.__class__, ButtonContentsIconText):
self.icon_color = self.text_color
# if (
# self.__is_filled is False
# ):
# self.text_color = self.font_color_normal
# if issubclass(self.__class__, ButtonContentsIconText):
# self.icon_color = self.text_color
class MDToggleButton(MDToggleButtonBehavior):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Logger.warning(
f"KivyMD: "
f"The `{self.__class__.__name__}` class has been deprecated. "
f"Use the `MDToggleButtonBehavior` class instead."
)

View file

@ -16,21 +16,32 @@ Usage
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.behaviors import TouchBehavior
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.button import MDButton
KV = '''
MDScreen:
<TouchBehaviorButton>
style: "elevated"
MyButton:
text: "PRESS ME"
MDButtonText:
text: root.text
MDScreen:
md_bg_color: self.theme_cls.backgroundColor
TouchBehaviorButton:
text: "TouchBehavior"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class MyButton(MDRaisedButton, TouchBehavior):
class TouchBehaviorButton(MDButton, TouchBehavior):
text = StringProperty()
def on_long_touch(self, *args):
print("<on_long_touch> event")
@ -41,12 +52,12 @@ Usage
print("<on_triple_tap> event")
class MainApp(MDApp):
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
MainApp().run()
Example().run()
"""
__all__ = ("TouchBehavior",)
@ -85,16 +96,18 @@ class TouchBehavior:
self.on_triple_tap(touch, *args)
def delete_clock(self, widget, touch, *args):
"""Removes a key event from `touch.ud`."""
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."""
"""Fired when the widget is pressed for a long time."""
def on_double_tap(self, touch, *args):
"""Called by double clicking on the widget."""
"""Fired by double-clicking on the widget."""
def on_triple_tap(self, touch, *args):
"""Called by triple clicking on the widget."""
"""Fired by triple clicking on the widget."""