diff --git a/.idea/KivyMD.iml b/.idea/KivyMD.iml index 8e5446ac9..e9f6a42c1 100644 --- a/.idea/KivyMD.iml +++ b/.idea/KivyMD.iml @@ -4,7 +4,7 @@ - + diff --git a/docs/sources/changelog/unreleased.rst b/docs/sources/changelog/unreleased.rst index 17d725f26..44d46909f 100644 --- a/docs/sources/changelog/unreleased.rst +++ b/docs/sources/changelog/unreleased.rst @@ -62,3 +62,4 @@ Unreleased * `MDDropdownMenu `_ `API break <>https://kivymd.readthedocs.io/en/latest/components/menu/#api-break`_; * Added the `motion_behavior `_ module to the `behaviors` package to control the display/hide behavior of widgets; * `Fixed `_ scaling and rotation of widgets with elevation behavior; +* Removed `MDBanner` widget; \ No newline at end of file diff --git a/examples/appbar.py b/examples/appbar.py new file mode 100644 index 000000000..bc94576f1 --- /dev/null +++ b/examples/appbar.py @@ -0,0 +1,91 @@ +from kivy.lang import Builder + +from kivymd.app import MDApp + +from examples.common_app import CommonApp + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.secondaryContainerColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + size_hint_x: .8 + spacing: "12dp" + pos_hint: {"center_x": .5, "center_y": .5} + + MDTopAppBar: + id: appbar + type: "small" + + MDTopAppBarLeadingButtonContainer: + + MDActionTopAppBarButton: + icon: "arrow-left" + + MDTopAppBarTitle: + text: "AppBar small" + + MDTopAppBarTrailingButtonContainer: + + MDActionTopAppBarButton: + icon: "attachment" + + MDActionTopAppBarButton: + icon: "calendar" + + MDActionTopAppBarButton: + icon: "dots-vertical" + + MDTopAppBar: + id: appbar_custom + type: "small" + + MDTopAppBarLeadingButtonContainer: + + MDActionTopAppBarButton: + icon: "arrow-left" + theme_icon_color: "Custom" + icon_color: "green" + + MDTopAppBarTitle: + text: "AppBar small" + theme_text_color: "Custom" + text_color: "green" + + MDTopAppBarTrailingButtonContainer: + + MDActionTopAppBarButton: + icon: "attachment" + theme_icon_color: "Custom" + icon_color: "green" + + MDActionTopAppBarButton: + icon: "calendar" + theme_icon_color: "Custom" + icon_color: "green" + + MDActionTopAppBarButton: + icon: "dots-vertical" + theme_icon_color: "Custom" + icon_color: "green" +""" + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def disabled_widgets(self): + self.root.ids.appbar.disabled = not self.root.ids.appbar.disabled + self.root.ids.appbar_custom.disabled = self.root.ids.appbar.disabled + + +Example().run() diff --git a/examples/badge.py b/examples/badge.py new file mode 100644 index 000000000..6e531190a --- /dev/null +++ b/examples/badge.py @@ -0,0 +1,35 @@ +from kivy.lang import Builder + +from kivymd.app import MDApp + +from examples.common_app import CommonApp + +KV = ''' +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDIcon: + id: icon + icon: "gmail" + pos_hint: {'center_x': .5, 'center_y': .5} + + MDBadge: + text: "12" +''' + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def disabled_widgets(self): + self.root.ids.icon.disabled = not self.root.ids.icon.disabled + + +Example().run() diff --git a/examples/button.py b/examples/button.py new file mode 100644 index 000000000..b6c71eea4 --- /dev/null +++ b/examples/button.py @@ -0,0 +1,359 @@ +from kivy.clock import Clock +from kivy.lang import Builder + +from kivymd.app import MDApp +from kivymd.uix.button import ( + MDIconButton, + MDButton, + MDFabButton, + MDButtonText, + MDButtonIcon, + MDExtendedFabButton, + MDExtendedFabButtonIcon, + MDExtendedFabButtonText, +) + +from examples.common_app import CommonApp + +KV = """ +MDScreen: + md_bg_color: app.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + ScrollView: + size_hint_y: None + height: root.height - dp(68) + + MDBoxLayout: + orientation: "vertical" + padding: "24dp", 0, "24dp", 0 + adaptive_height: True + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + + MDLabel: + text: "MDIconButton" + bold: True + adaptive_height: True + + MDBoxLayout: + id: icon_button_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + md_bg_color: app.theme_cls.secondaryContainerColor + radius: "12dp" + + MDLabel: + text: "MDIconButton (custom color)" + bold: True + adaptive_height: True + + MDBoxLayout: + id: custom_icon_button_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + + MDLabel: + text: "MDFabButton" + bold: True + adaptive_height: True + + MDBoxLayout: + id: fab_button_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + md_bg_color: app.theme_cls.secondaryContainerColor + radius: "12dp" + + MDLabel: + text: "MDFabButton (custom color)" + bold: True + adaptive_height: True + + MDBoxLayout: + id: custom_fab_button_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + + MDLabel: + text: "MDButton" + adaptive_height: True + bold: True + + MDBoxLayout: + id: md_button_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp", 0, "24dp", "24dp" + + MDLabel: + text: "MDButton (with icon)" + adaptive_height: True + bold: True + + MDBoxLayout: + id: md_button_icon_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + md_bg_color: app.theme_cls.secondaryContainerColor + radius: "12dp" + + MDLabel: + text: "MDButton (custom color)" + bold: True + adaptive_height: True + + MDBoxLayout: + id: custom_md_button_box + adaptive_height: True + spacing: "12dp" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "24dp" + padding: "24dp" + radius: "12dp" + + MDLabel: + text: "MDExtendedFabButton" + bold: True + adaptive_height: True + + MDBoxLayout: + id: extended_fab_button_box + adaptive_height: True + spacing: "12dp" + + Widget: +""" + + +class Example(MDApp, CommonApp): + def build(self): + self.theme_cls.dynamic_color = True + return Builder.load_string(KV) + + def disabled_widgets(self): + for button in ( + self.root.ids.fab_button_box.children + + self.root.ids.custom_fab_button_box.children + + self.root.ids.custom_icon_button_box.children + + self.root.ids.md_button_box.children + + self.root.ids.icon_button_box.children + + self.root.ids.md_button_icon_box.children + + self.root.ids.custom_md_button_box.children + + self.root.ids.extended_fab_button_box.children + ): + button.disabled = not button.disabled + + def on_start(self): + styles = ["standard", "filled", "outlined", "tonal"] + color_disabled = [ + 0.4627450980392157, + 0.47058823529411764, + 0.07450980392156863, + 0.38, + ] + + for style in styles: + self.root.ids.icon_button_box.add_widget( + MDIconButton(style=style, icon="heart") + ) + if style in ["filled", "tonal"]: + self.root.ids.custom_icon_button_box.add_widget( + MDIconButton( + style=style, + icon="heart", + theme_bg_color="Custom", + theme_icon_color="Custom", + md_bg_color={"filled": "brown", "tonal": "green"}[ + style + ], + icon_color={"filled": "green", "tonal": "brown"}[style], + icon_color_disabled="black", + md_bg_color_disabled=color_disabled, + ) + ) + elif style == "outlined": + self.root.ids.custom_icon_button_box.add_widget( + MDIconButton( + style=style, + icon="heart", + theme_icon_color="Custom", + theme_line_color="Custom", + line_color="brown", + icon_color="green", + icon_color_disabled="black", + md_bg_color_disabled=color_disabled, + ) + ) + elif style == "standard": + self.root.ids.custom_icon_button_box.add_widget( + MDIconButton( + style=style, + icon="heart", + theme_icon_color="Custom", + icon_color="green", + icon_color_disabled="black", + md_bg_color_disabled=color_disabled, + ) + ) + + styles = ["filled", "outlined", "tonal", "elevated", "text"] + for style in styles: + text = style.capitalize() + self.root.ids.md_button_box.add_widget( + MDButton( + MDButtonText( + text=text, + ), + style=style, + ) + ) + self.root.ids.md_button_icon_box.add_widget( + MDButton( + MDButtonIcon( + icon="heart", + ), + MDButtonText( + text=text, + ), + style=style, + ) + ) + self.root.ids.custom_md_button_box.add_widget( + MDButton( + MDButtonIcon( + icon="heart", + theme_icon_color="Custom", + icon_color="yellow", + icon_color_disabled="black", + ), + MDButtonText( + text=text, + theme_text_color="Custom", + text_color={ + "filled": "white", + "tonal": "white", + "outlined": "green", + "text": "green", + "elevated": "white", + }[style], + ), + style=style, + theme_bg_color="Custom", + theme_line_color="Custom" + if style == "outlined" + else "Primary", + md_bg_color={ + "filled": "brown", + "tonal": "brown", + "outlined": self.theme_cls.transparentColor, + "text": self.theme_cls.transparentColor, + "elevated": "red", + }[style], + line_color="green", + md_bg_color_disabled=color_disabled, + ) + ) + + styles = { + "standard": "surface", + "small": "secondary", + "large": "tertiary", + } + for style in styles.keys(): + self.root.ids.fab_button_box.add_widget( + MDFabButton( + style=style, icon="pencil-outline", color_map=styles[style] + ) + ) + self.root.ids.custom_fab_button_box.add_widget( + MDFabButton( + style=style, + icon="heart", + theme_bg_color="Custom", + md_bg_color="brown", + theme_icon_color="Custom", + icon_color="yellow", + icon_color_disabled="lightgrey", + md_bg_color_disabled=color_disabled, + ) + ) + button = MDExtendedFabButton( + MDExtendedFabButtonIcon( + icon="pencil-outline", + ), + MDExtendedFabButtonText( + text="Compose", + ), + fab_state="expand", + ) + button.bind(on_release=self.fab_button_expand) + self.root.ids.extended_fab_button_box.add_widget( + MDExtendedFabButton( + MDExtendedFabButtonText( + text="Compose", + theme_text_color="Custom", + text_color="red", + ), + fab_state="expand", + ) + ) + self.root.ids.extended_fab_button_box.add_widget(button) + + def fab_button_expand(self, instance): + def fab_button_expand(*args): + instance.fab_state = ( + "expand" if instance.fab_state == "collapse" else "collapse" + ) + + Clock.schedule_once(fab_button_expand, 0.3) + + +Example().run() diff --git a/examples/card.py b/examples/card.py new file mode 100644 index 000000000..575884cbb --- /dev/null +++ b/examples/card.py @@ -0,0 +1,106 @@ +from kivy.lang import Builder +from kivy.properties import StringProperty + +from kivymd.app import MDApp +from kivymd.uix.card import MDCard + +from examples.common_app import CommonApp, KV + +Builder.load_string( + """ + + padding: "4dp" + size_hint_y: None + height: "100dp" + + MDRelativeLayout: + + MDIconButton: + icon: "dots-vertical" + pos_hint: {"top": 1, "right": 1} + focus_behavior: False + + MDLabel: + id: label + text: root.text + adaptive_size: True + color: "grey" + pos: "12dp", "12dp" + bold: True +""" +) + + +class MyCustomCard(MDCard): + text = StringProperty() + + +class MyCard(MDCard): + text = StringProperty() + + +class Example(MDApp, CommonApp): + def build(self): + self.theme_cls.dynamic_color = True + self.theme_cls.primary_palette = "Red" + return Builder.load_string(KV) + + def on_start(self): + color_disabled = [ + 0.4627450980392157, + 0.47058823529411764, + 0.07450980392156863, + 0.38, + ] + for style in ("elevated", "filled", "outlined"): + self.root.ids.widget_box.add_widget( + MyCard( + style=style, + text=style.capitalize(), + ripple_behavior=True, + ) + ) + if style == "elevated": + card = MyCard( + style=style, + text=style.capitalize(), + theme_shadow_color="Custom", + shadow_color="red", + theme_bg_color="Custom", + md_bg_color="white", + md_bg_color_disabled=color_disabled, + theme_shadow_offset="Custom", + shadow_offset=(1, -2), + theme_focus_color="Custom", + focus_color=[1, 0, 0, 0.2], + theme_shadow_softness="Custom", + shadow_softness=1, + theme_elevation_level="Custom", + elevation_level=2, + ripple_behavior=True, + ) + self.root.ids.custom_widget_box.add_widget(card) + elif style == "filled": + self.root.ids.custom_widget_box.add_widget( + MyCard( + style=style, + text=style.capitalize(), + ripple_behavior=True, + theme_bg_color="Custom", + md_bg_color="brown", + ) + ) + elif style == "outlined": + self.root.ids.custom_widget_box.add_widget( + MyCard( + style=style, + text=style.capitalize(), + ripple_behavior=True, + theme_line_color="Custom", + line_color="brown", + md_bg_color_disabled=color_disabled, + ) + ) + + +Example().run() diff --git a/examples/checkbox.py b/examples/checkbox.py new file mode 100644 index 000000000..29f9c2e98 --- /dev/null +++ b/examples/checkbox.py @@ -0,0 +1,24 @@ +from kivy.lang import Builder + +from kivymd.uix.selectioncontrol import MDCheckbox +from kivymd.app import MDApp + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + self.root.ids.widget_box.add_widget(MDCheckbox()) + self.root.ids.custom_widget_box.add_widget( + MDCheckbox( + color_active="lightgreen", + color_inactive="red", + color_disabled="brown", + ) + ) + + +Example().run() diff --git a/examples/chip.py b/examples/chip.py new file mode 100644 index 000000000..9983f6922 --- /dev/null +++ b/examples/chip.py @@ -0,0 +1,51 @@ +from kivy.lang import Builder + +from kivymd.app import MDApp +from kivymd.uix.chip import MDChip, MDChipLeadingIcon, MDChipText + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + self.theme_cls.primary_palette = "Olive" + return Builder.load_string(KV) + + def on_start(self): + for chip_type in ["assist", "input", "suggestion", "filter"]: + self.root.ids.widget_box.add_widget( + MDChip( + MDChipLeadingIcon( + icon="account", + ), + MDChipText( + text=chip_type.capitalize(), + ), + type=chip_type, + ) + ) + + for chip_type in ["assist", "input", "suggestion", "filter"]: + self.root.ids.custom_widget_box.add_widget( + MDChip( + MDChipLeadingIcon( + icon="account", + theme_icon_color="Custom", + icon_color="brown", + icon_color_disabled="black", + ), + MDChipText( + text=chip_type.capitalize(), + theme_text_color="Custom", + text_color="red", + text_color_disabled="black", + ), + type=chip_type, + theme_line_color="Custom", + line_color="green", + line_color_disabled="black", + ) + ) + + +Example().run() diff --git a/examples/common_app.py b/examples/common_app.py new file mode 100644 index 000000000..275cd21ec --- /dev/null +++ b/examples/common_app.py @@ -0,0 +1,119 @@ +from kivy.utils import hex_colormap + +from kivymd.uix.menu import MDDropdownMenu + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDBoxLayout: + id: root_box + orientation: "vertical" + spacing: "12dp" + padding: "12dp" + + MDIconButton: + on_release: app.open_menu(self) + icon: "menu" + + ScrollView: + + MDBoxLayout: + orientation: "vertical" + padding: "32dp", 0, "32dp", "32dp" + spacing: "24dp" + adaptive_height: True + + MDLabel: + adaptive_height: True + text: "Standard widget" + + MDBoxLayout: + id: widget_box + adaptive_height: True + spacing: "24dp" + + Widget: + size_hint_y: None + height: "12dp" + + MDLabel: + adaptive_height: True + text: "Custom widget" + + MDBoxLayout: + id: custom_widget_box + adaptive_height: True + spacing: "24dp" +""" + + +class CommonApp: + menu: MDDropdownMenu = None + + def open_menu(self, menu_button): + menu_items = [] + for item, method in { + "Set palette": lambda: self.set_palette(), + "Switch theme style": lambda: self.theme_cls.switch_theme(), + "Disabled widgets": lambda: self.disabled_widgets(), + }.items(): + menu_items.append( + { + "text": item, + "on_release": method, + } + ) + self.menu = MDDropdownMenu( + caller=menu_button, + items=menu_items, + ) + self.menu.open() + + def switch_palette(self, selected_palette): + self.theme_cls.primary_palette = selected_palette + + def switch_palette(self, selected_palette): + self.theme_cls.primary_palette = selected_palette + + def set_palette(self): + instance_from_menu = self.get_instance_from_menu("Set palette") + available_palettes = [ + name_color.capitalize() for name_color in hex_colormap.keys() + ] + + menu_items = [] + for name_palette in available_palettes: + menu_items.append( + { + "text": name_palette, + "on_release": lambda x=name_palette: self.switch_palette(x), + } + ) + MDDropdownMenu( + caller=instance_from_menu, + items=menu_items, + ).open() + + def get_instance_from_menu(self, name_item): + index = 0 + rv = self.menu.ids.md_menu + opts = rv.layout_manager.view_opts + datas = rv.data[0] + + for data in rv.data: + if data["text"] == name_item: + index = rv.data.index(data) + break + + instance = rv.view_adapter.get_view( + index, datas, opts[index]["viewclass"] + ) + return instance + + def disabled_widgets(self): + for widget in self.root.ids.widget_box.children: + widget.disabled = not widget.disabled + + for widget in self.root.ids.custom_widget_box.children: + widget.disabled = not widget.disabled diff --git a/examples/dialog.py b/examples/dialog.py new file mode 100644 index 000000000..8cb55b220 --- /dev/null +++ b/examples/dialog.py @@ -0,0 +1,97 @@ +from kivy.lang import Builder +from kivy.uix.widget import Widget + +from kivymd.app import MDApp +from kivymd.uix.button import MDButton, MDButtonText +from kivymd.uix.dialog import ( + MDDialog, + MDDialogIcon, + MDDialogHeadlineText, + MDDialogSupportingText, + MDDialogButtonContainer, + MDDialogContentContainer, +) +from kivymd.uix.divider import MDDivider +from kivymd.uix.list import ( + MDListItem, + MDListItemLeadingIcon, + MDListItemSupportingText, +) + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDButton: + pos_hint: {'center_x': .5, 'center_y': .5} + on_release: app.show_alert_dialog() + + MDButtonText: + text: "Show dialog" +""" + + +class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + def show_alert_dialog(self): + MDDialog( + # ----------------------------Icon----------------------------- + MDDialogIcon( + icon="refresh", + ), + # -----------------------Headline text------------------------- + MDDialogHeadlineText( + text="Reset settings?", + ), + # -----------------------Supporting text----------------------- + MDDialogSupportingText( + text="This will reset your app preferences back to their " + "default settings. The following accounts will also " + "be signed out:", + ), + # -----------------------Custom content------------------------ + MDDialogContentContainer( + MDDivider(), + MDListItem( + MDListItemLeadingIcon( + icon="gmail", + ), + MDListItemSupportingText( + text="KivyMD-library@yandex.com", + ), + theme_bg_color="Custom", + md_bg_color=self.theme_cls.transparentColor, + ), + MDListItem( + MDListItemLeadingIcon( + icon="gmail", + ), + MDListItemSupportingText( + text="kivydevelopment@gmail.com", + ), + theme_bg_color="Custom", + md_bg_color=self.theme_cls.transparentColor, + ), + MDDivider(), + orientation="vertical", + ), + # ---------------------Button container------------------------ + MDDialogButtonContainer( + Widget(), + MDButton( + MDButtonText(text="Cancel"), + style="text", + ), + MDButton( + MDButtonText(text="Accept"), + style="text", + ), + spacing="8dp", + ), + # ------------------------------------------------------------- + ).open() + + +Example().run() diff --git a/examples/dynamic_color_image.py b/examples/dynamic_color_image.py new file mode 100644 index 000000000..782b5ed5f --- /dev/null +++ b/examples/dynamic_color_image.py @@ -0,0 +1,91 @@ +# Drag the image to the test application window. + +import os + +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.properties import StringProperty, ColorProperty + +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.app import MDApp + +KV = """ + + orientation: "vertical" + + MDLabel: + text: root.text + color: "grey" + adaptive_height: True + + MDCard: + theme_bg_color: "Custom" + md_bg_color: root.bg_color + + +MDScreen: + md_bg_color: app.theme_cls.backgroundColor + + MDRecycleView: + id: card_list + viewclass: "ColorCard" + bar_width: 0 + + RecycleGridLayout: + cols: 3 + spacing: "16dp" + padding: "16dp" + default_size: None, dp(56) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height +""" + + +class ColorCard(MDBoxLayout): + text = StringProperty() + bg_color = ColorProperty() + + +class Example(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + Window.bind(on_dropfile=self.on_drop_file) + + def on_drop_file(self, sdl: WindowSDL, path_to_file: str) -> None: + ext = os.path.splitext(path_to_file)[1] + if isinstance(path_to_file, bytes): + path_to_file = path_to_file.decode() + if isinstance(ext, bytes): + ext = ext.decode() + if ext in [".png", ".jpg"]: + self.theme_cls.path_to_wallpaper = path_to_file + Clock.schedule_once(self.generate_cards, 0.5) + + def build(self): + self.theme_cls.dynamic_color = True + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + def theme_switch(self) -> None: + self.theme_cls.switch_theme() + Clock.schedule_once(self.generate_cards, 0.5) + + def generate_cards(self, *args): + self.root.ids.card_list.data = [] + for color in self.theme_cls.schemes_name_colors: + value = f"{color}Color" + self.root.ids.card_list.data.append( + { + "bg_color": getattr(self.theme_cls, value), + "text": value, + } + ) + + def on_start(self): + Clock.schedule_once(self.generate_cards) + + +Example().run() diff --git a/examples/dynamic_color_schemes.py b/examples/dynamic_color_schemes.py new file mode 100644 index 000000000..9bd4283d2 --- /dev/null +++ b/examples/dynamic_color_schemes.py @@ -0,0 +1,136 @@ +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import StringProperty, ColorProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.utils import hex_colormap + +from kivymd.uix.menu import MDDropdownMenu +from kivymd.app import MDApp + + +KV = """ + + orientation: "vertical" + + MDLabel: + text: root.text + color: "grey" + adaptive_height: True + + MDCard: + theme_bg_color: "Custom" + md_bg_color: root.bg_color + + +MDScreen: + md_bg_color: app.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDRecycleView: + id: card_list + viewclass: "ColorCard" + bar_width: 0 + size_hint_y: None + height: root.height - dp(68) + + RecycleGridLayout: + cols: 3 + spacing: "16dp" + padding: "16dp" + default_size: None, dp(56) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height +""" + + +class ColorCard(BoxLayout): + text = StringProperty() + bg_color = ColorProperty() + + +class Example(MDApp): + menu: MDDropdownMenu = None + + def build(self): + self.theme_cls.dynamic_color = True + return Builder.load_string(KV) + + def get_instance_from_menu(self, name_item): + index = 0 + rv = self.menu.ids.md_menu + opts = rv.layout_manager.view_opts + datas = rv.data[0] + + for data in rv.data: + if data["text"] == name_item: + index = rv.data.index(data) + break + + instance = rv.view_adapter.get_view( + index, datas, opts[index]["viewclass"] + ) + + return instance + + def open_menu(self, menu_button): + menu_items = [] + for item, method in { + "Set palette": lambda: self.set_palette(), + "Switch theme style": lambda: self.theme_switch(), + }.items(): + menu_items.append({"text": item, "on_release": method}) + self.menu = MDDropdownMenu( + caller=menu_button, + items=menu_items, + ) + self.menu.open() + + def set_palette(self): + instance_from_menu = self.get_instance_from_menu("Set palette") + available_palettes = [ + name_color.capitalize() for name_color in hex_colormap.keys() + ] + + menu_items = [] + for name_palette in available_palettes: + menu_items.append( + { + "text": name_palette, + "on_release": lambda x=name_palette: self.switch_palette(x), + } + ) + MDDropdownMenu( + caller=instance_from_menu, + items=menu_items, + ).open() + + def switch_palette(self, selected_palette): + self.theme_cls.primary_palette = selected_palette + Clock.schedule_once(self.generate_cards, 0.5) + + def theme_switch(self) -> None: + self.theme_cls.switch_theme() + Clock.schedule_once(self.generate_cards, 0.5) + + def generate_cards(self, *args): + self.root.ids.card_list.data = [] + for color in self.theme_cls.schemes_name_colors: + value = f"{color}Color" + self.root.ids.card_list.data.append( + { + "bg_color": getattr(self.theme_cls, value), + "text": value, + } + ) + + def on_start(self): + Clock.schedule_once(self.generate_cards) + + +Example().run() diff --git a/examples/label.py b/examples/label.py new file mode 100644 index 000000000..446016a3a --- /dev/null +++ b/examples/label.py @@ -0,0 +1,67 @@ +from kivy.lang import Builder + +from kivymd.font_definitions import theme_font_styles +from kivymd.app import MDApp + +from examples.common_app import CommonApp + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDRecycleView: + id: rv + key_viewclass: 'viewclass' + key_size: 'height' + size_hint_y: None + height: root.height - dp(48) + + RecycleBoxLayout: + padding: dp(18) + spacing: dp(10) + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: "vertical" +""" + + +class Example(MDApp, CommonApp): + _disabled = False + + def build(self): + return Builder.load_string(KV) + + def disabled_widgets(self): + self._disabled = not self._disabled + self.create_widgets() + + def create_widgets(self): + self.root.ids.rv.data = [] + for style in theme_font_styles: + if style != "Icon": + for role in theme_font_styles[style]: + font_size = int(theme_font_styles[style][role]["font-size"]) + self.root.ids.rv.data.append( + { + "viewclass": "MDLabel", + "text": f"{style} {role} {font_size} sp", + "adaptive_height": "True", + "font_style": style, + "role": role, + "disabled": self._disabled, + } + ) + + def on_start(self): + self.create_widgets() + + +Example().run() diff --git a/examples/list.py b/examples/list.py new file mode 100644 index 000000000..81a09df98 --- /dev/null +++ b/examples/list.py @@ -0,0 +1,67 @@ +from kivy.lang import Builder + +from kivymd import images_path +from kivymd.uix.list import ( + MDListItem, + MDListItemHeadlineText, + MDListItemSupportingText, + MDListItemTrailingCheckbox, + MDListItemLeadingAvatar, + MDListItemTertiaryText, + MDListItemLeadingIcon, +) + +from kivymd.app import MDApp + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def on_tap_list_item(self, list_item: MDListItem): + print("on_tap_list_item") + + def on_start(self): + self.root.ids.widget_box.orientation = "vertical" + self.root.ids.widget_box.add_widget( + MDListItem( + MDListItemLeadingIcon( + icon="account-outline", + ), + MDListItemHeadlineText( + text="Headline", + ), + MDListItemSupportingText( + text="Supporting text", + ), + MDListItemTertiaryText( + text="Tertiary text", + ), + MDListItemTrailingCheckbox(), + on_release=self.on_tap_list_item, + ) + ) + # Custom. + self.root.ids.custom_widget_box.add_widget( + MDListItem( + MDListItemLeadingAvatar( + source=f"{images_path}/logo/kivymd-icon-256.png", + ), + MDListItemHeadlineText( + text="Headline", + ), + MDListItemTrailingCheckbox( + color_disabled="red", + ), + divider=True, + theme_divider_color="Custom", + divider_color="red", + theme_bg_color="Custom", + md_bg_color=[1, 1, 0, 0.3], + ) + ) + + +Example().run() diff --git a/examples/navigation_bar.py b/examples/navigation_bar.py new file mode 100644 index 000000000..0de30fc80 --- /dev/null +++ b/examples/navigation_bar.py @@ -0,0 +1,111 @@ +from kivy.lang import Builder +from kivy.metrics import dp + +from kivymd.uix.navigationbar import ( + MDNavigationBar, + MDNavigationItem, + MDNavigationItemLabel, + MDNavigationItemIcon, +) +from kivymd.app import MDApp + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def on_switch_tabs( + self, + bar: MDNavigationBar, + item: MDNavigationItem, + item_icon: str, + item_text: str, + ): + ... + + def on_start(self): + self.root.ids.widget_box.height = dp(80) + self.root.ids.widget_box.add_widget( + MDNavigationBar( + MDNavigationItem( + MDNavigationItemIcon( + icon="gmail", + ), + MDNavigationItemLabel( + text="Mail", + ), + ), + MDNavigationItem( + MDNavigationItemIcon( + icon="twitter", + ), + MDNavigationItemLabel( + text="Twitter", + ), + ), + MDNavigationItem( + MDNavigationItemIcon( + icon="linkedin", + ), + MDNavigationItemLabel( + text="LinkedIN", + ), + ), + on_switch_tabs=self.on_switch_tabs, + ) + ) + + self.root.ids.custom_widget_box.add_widget( + MDNavigationBar( + MDNavigationItem( + MDNavigationItemIcon( + icon="gmail", + theme_icon_color="Custom", + icon_color_normal="brown", + icon_color_active="white", + ), + MDNavigationItemLabel( + text="Mail", + theme_text_color="Custom", + text_color_active="white", + ), + indicator_color="grey", + ), + MDNavigationItem( + MDNavigationItemIcon( + icon="twitter", + theme_icon_color="Custom", + icon_color_normal="brown", + icon_color_active="white", + ), + MDNavigationItemLabel( + text="Twitter", + theme_text_color="Custom", + text_color_active="white", + ), + indicator_color="grey", + ), + MDNavigationItem( + MDNavigationItemIcon( + icon="linkedin", + theme_icon_color="Custom", + icon_color_normal="brown", + icon_color_active="white", + ), + MDNavigationItemLabel( + text="LinkedIN", + theme_text_color="Custom", + text_color_active="white", + ), + indicator_color="grey", + ), + theme_bg_color="Custom", + md_bg_color="silver", + on_switch_tabs=self.on_switch_tabs, + ) + ) + + +Example().run() diff --git a/examples/navigation_rail.py b/examples/navigation_rail.py new file mode 100644 index 000000000..e9faabaeb --- /dev/null +++ b/examples/navigation_rail.py @@ -0,0 +1,61 @@ +from kivy.lang import Builder +from kivy.properties import StringProperty + +from kivymd.app import MDApp +from kivymd.uix.navigationrail import MDNavigationRailItem + +from examples.common_app import CommonApp + +KV = """ + + + MDNavigationRailItemIcon: + icon: root.icon + + MDNavigationRailItemLabel: + text: root.text + + +MDBoxLayout: + + MDNavigationRail: + id: rail + + MDNavigationRailMenuButton: + icon: "menu" + on_release: app.open_menu(self) + + MDNavigationRailFabButton: + icon: "home" + + CommonNavigationRailItem: + icon: "folder-outline" + text: "Files" + + CommonNavigationRailItem: + icon: "bookmark-outline" + text: "Bookmark" + + CommonNavigationRailItem: + icon: "library-outline" + text: "Library" + + MDScreen: + md_bg_color: self.theme_cls.secondaryContainerColor +""" + + +class CommonNavigationRailItem(MDNavigationRailItem): + text = StringProperty() + icon = StringProperty() + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def disabled_widgets(self): + self.root.ids.rail.disabled = not self.root.ids.rail.disabled + + +Example().run() diff --git a/examples/navigationdrawer.py b/examples/navigationdrawer.py new file mode 100644 index 000000000..c73b99607 --- /dev/null +++ b/examples/navigationdrawer.py @@ -0,0 +1,133 @@ +from kivy.lang import Builder + +from kivymd.app import MDApp + +from examples.common_app import CommonApp + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: root.width - (self.width + dp(24)) + icon: "menu" + + MDNavigationLayout: + + MDScreenManager: + + MDScreen: + + MDButton: + pos_hint: {"center_x": .5, "center_y": .5} + on_release: nav_drawer.set_state("toggle") + + MDButtonText: + text: "Open Drawer" + + MDNavigationDrawer: + id: nav_drawer + radius: 0, dp(16), dp(16), 0 + + MDNavigationDrawerMenu: + + MDNavigationDrawerHeader: + padding: "16dp", 0, "16dp", "16dp" + spacing: "16dp" + + MDIcon: + icon: "card-account-mail-outline" + theme_font_size: "Custom" + font_size: "56sp" + theme_icon_color: "Custom" + icon_color: self.theme_cls.primaryColor + + MDLabel: + text: "Gmail" + font_style: "Display" + role: "medium" + theme_line_height: "Custom" + line_height: 0 + adaptive_height: True + theme_text_color: "Custom" + text_color: self.theme_cls.primaryColor + + MDNavigationDrawerLabel: + text: "accaount@gmail.com" + theme_text_color: "Custom" + text_color: self.theme_cls.primaryColor + + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "inbox" + + MDNavigationDrawerItemText: + text: "Inbox" + + MDNavigationDrawerItemTrailingText: + text: "24" + theme_text_color: "Custom" + text_color: self.theme_cls.primaryColor + + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "send-outline" + + MDNavigationDrawerItemText: + text: "Outbox" + + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "heart-outline" + + MDNavigationDrawerItemText: + text: "Favorites" + + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "trash-can-outline" + + MDNavigationDrawerItemText: + text: "Trash" + + MDNavigationDrawerDivider: + + MDNavigationDrawerLabel: + text: "Personal folders" + theme_text_color: "Custom" + text_color: self.theme_cls.primaryColor + padding: "16dp", "8dp", 0, 0 + + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "folder-outline" + + MDNavigationDrawerItemText: + text: "Family" + + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "folder-outline" + + MDNavigationDrawerItemText: + text: "Wedding" +""" + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def disabled_widgets(self): + ... + + +Example().run() diff --git a/examples/segmented_button.py b/examples/segmented_button.py new file mode 100644 index 000000000..fe17c9be1 --- /dev/null +++ b/examples/segmented_button.py @@ -0,0 +1,100 @@ +from kivy.lang import Builder + +from kivymd.app import MDApp + +from examples.common_app import CommonApp + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDBoxLayout: + orientation: "vertical" + padding: "32dp", "72dp", "32dp", "32dp" + spacing: "24dp" + + MDLabel: + adaptive_height: True + text: "Segmented button" + + MDSegmentedButton: + id: segmented_button + + MDSegmentedButtonItem: + + MDSegmentButtonIcon: + icon: "language-python" + + MDSegmentButtonLabel: + text: "Python" + + MDSegmentedButtonItem: + + MDSegmentButtonIcon: + icon: "language-javascript" + + MDSegmentButtonLabel: + text: "Java-Script" + + MDLabel: + adaptive_height: True + text: "Custom Segmented button" + + MDSegmentedButton: + id: segmented_button_custom + selected_icon_color: "red" + + MDSegmentedButtonItem: + theme_line_color: "Custom" + line_color: "red" + selected_color: "#a655f240" + + MDSegmentButtonIcon: + icon: "language-python" + theme_icon_color: "Custom" + icon_color: "red" + + MDSegmentButtonLabel: + text: "Python" + theme_text_color: "Custom" + text_color: "red" + + MDSegmentedButtonItem: + theme_line_color: "Custom" + line_color: "red" + selected_color: "#a655f240" + + MDSegmentButtonIcon: + icon: "language-javascript" + theme_icon_color: "Custom" + icon_color: "red" + + MDSegmentButtonLabel: + text: "Java-Script" + theme_text_color: "Custom" + text_color: "red" + + MDWidget: +""" + + +class Example(MDApp, CommonApp): + def build(self): + self.theme_cls.primary_palette = "Olive" + return Builder.load_string(KV) + + def disabled_widgets(self): + for item in ( + self.root.ids.segmented_button.get_items() + + self.root.ids.segmented_button_custom.get_items() + ): + item.disabled = not item.disabled + + +Example().run() diff --git a/examples/slider.py b/examples/slider.py new file mode 100644 index 000000000..c88d298a9 --- /dev/null +++ b/examples/slider.py @@ -0,0 +1,40 @@ +from kivy.lang import Builder + +from kivymd.uix.slider import MDSlider, MDSliderHandle, MDSliderValueLabel +from kivymd.app import MDApp + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + self.theme_cls.primary_palette = "Red" + return Builder.load_string(KV) + + def on_start(self): + self.root.ids.widget_box.add_widget( + MDSlider( + MDSliderHandle(), + MDSliderValueLabel(), + step=10, + value=50, + ) + ) + self.root.ids.custom_widget_box.add_widget( + MDSlider( + MDSliderHandle( + theme_bg_color="Custom", + md_bg_color="teal", + state_layer_color="black", + ), + step=10, + value=50, + track_active_color="green", + track_inactive_color="lightgreen", + track_active_step_point_color="white", + track_inactive_step_point_color="green", + ) + ) + + +Example().run() diff --git a/examples/snackbar.py b/examples/snackbar.py new file mode 100644 index 000000000..a4ad67c00 --- /dev/null +++ b/examples/snackbar.py @@ -0,0 +1,164 @@ +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.uix.widget import Widget + +from kivymd.uix.button import MDButton, MDButtonText +from kivymd.uix.snackbar import ( + MDSnackbar, + MDSnackbarSupportingText, + MDSnackbarButtonContainer, + MDSnackbarActionButton, + MDSnackbarActionButtonText, + MDSnackbarCloseButton, + MDSnackbarText, +) +from kivymd.app import MDApp + +from examples.common_app import CommonApp + +KV = """ +MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDBoxLayout: + id: box + orientation: "vertical" + adaptive_size: True + spacing: "8dp" + pos_hint: {'center_x': .5, 'center_y': .5} +""" + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def disabled_widgets(self): + ... + + def on_start(self): + data = { + "Single-line snackbar": + self.show_snack_single_line, + "Single-line snackbar with action": + self.show_snack_single_line_with_action, + "Single-line snackbar with action and close buttons": + self.show_snack_single_line_with_action_and_close_button, + "Two-line snackbar with action and close buttons at the bottom": + self.show_snack_two_line_with_action_and_close_button_at_botton, + } + + for text_button, function in data.items(): + self.root.ids.box.add_widget( + MDButton( + MDButtonText( + text=text_button, + ), + on_release=lambda x, f=function: f(), + ) + ) + + def show_snack_single_line(self): + MDSnackbar( + MDSnackbarText( + text="Single-line snackbar", + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + ).open() + + def show_snack_single_line_with_action(self): + MDSnackbar( + MDSnackbarSupportingText( + text="Single-line snackbar with action", + ), + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText(text="Action button"), + ), + pos_hint={"center_y": 0.5}, + ), + y=dp(24), + orientation="horizontal", + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + ).open() + + def show_snack_single_line_with_action_and_close_button(self): + MDSnackbar( + MDSnackbarSupportingText( + text="Single-line snackbar with action and close buttons", + ), + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText( + text="Action button", + ), + ), + MDSnackbarCloseButton( + icon="close", + ), + pos_hint={"center_y": 0.5}, + ), + y=dp(24), + orientation="horizontal", + pos_hint={"center_x": 0.5}, + size_hint_x=0.8, + ).open() + + def show_snack_two_line_with_action_and_close_button(self): + MDSnackbar( + MDSnackbarText( + text="Single-line snackbar", + ), + MDSnackbarSupportingText( + text="with action and close buttons", + ), + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText(text="Action button"), + ), + MDSnackbarCloseButton( + icon="close", + ), + pos_hint={"center_y": 0.5}, + ), + y=dp(24), + orientation="horizontal", + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + ).open() + + def show_snack_two_line_with_action_and_close_button_at_botton(self): + MDSnackbar( + MDSnackbarText( + text="Single-line snackbar with action", + ), + MDSnackbarSupportingText( + text="and close buttons at the bottom", + padding=[0, 0, 0, dp(56)], + ), + MDSnackbarButtonContainer( + Widget(), + MDSnackbarActionButton( + MDSnackbarActionButtonText(text="Action button"), + ), + MDSnackbarCloseButton( + icon="close", + ), + ), + y=dp(124), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + padding=[0, 0, "8dp", "8dp"], + ).open() + + +Example().run() diff --git a/examples/switch.py b/examples/switch.py new file mode 100644 index 000000000..21405e130 --- /dev/null +++ b/examples/switch.py @@ -0,0 +1,40 @@ +from kivy.lang import Builder + +from kivymd.uix.selectioncontrol import MDSwitch +from kivymd.app import MDApp + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + self.root.ids.widget_box.add_widget( + MDSwitch( + icon_active="check", + icon_inactive="close", + ) + ) + + self.root.ids.custom_widget_box.add_widget( + MDSwitch( + icon_active="check", + icon_inactive="close", + md_bg_color_disabled="#b5b8b166", + thumb_color_disabled=[1, 0, 1, 0.4], + icon_active_color="white", + icon_inactive_color="red", + thumb_color_active="red", + thumb_color_inactive="white", + track_color_active="brown", + track_color_inactive="teal", + line_color_disabled="darkgrey", + theme_line_color="Custom", + line_color="red", + ) + ) + + +Example().run() diff --git a/examples/tab.py b/examples/tab.py new file mode 100644 index 000000000..f37b18460 --- /dev/null +++ b/examples/tab.py @@ -0,0 +1,34 @@ +# from kivymd.icon_definitions import md_icons +# +# from kivymd.uix.screen import MDScreen +# from kivymd.uix.tab import MDTabs, MDTabsItem, MDTabsIcon +# from kivymd.app import MDApp +# from kivymd.uix.tab.tab import MDTabsText +# +# +# class Example(MDApp): +# def build(self): +# screen = MDScreen( +# MDTabs( +# id="tabs", +# pos_hint={"top": 1}, +# ), +# md_bg_color=self.theme_cls.backgroundColor, +# ) +# for i, icon in enumerate(list(md_icons.keys())[0:30]): +# screen.get_ids()["tabs"].add_widget( +# MDTabsItem( +# MDTabsIcon( +# icon=icon, +# text_color="red", +# ), +# MDTabsText( +# text=f"Tab {i + 1}", +# ), +# theme_text_color="Custom", +# ) +# ) +# return screen +# +# +# Example().run() diff --git a/examples/textfield.py b/examples/textfield.py new file mode 100644 index 000000000..1c4a54314 --- /dev/null +++ b/examples/textfield.py @@ -0,0 +1,89 @@ +from kivy.lang import Builder + +from kivymd.uix.textfield import ( + MDTextField, + MDTextFieldHelperText, + MDTextFieldHintText, + MDTextFieldLeadingIcon, + MDTextFieldTrailingIcon, + MDTextFieldMaxLengthText, +) +from kivymd.app import MDApp + +from examples.common_app import CommonApp, KV + + +class Example(MDApp, CommonApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for mode in ["outlined", "filled", "required"]: + self.root.ids.widget_box.add_widget( + MDTextField( + MDTextFieldLeadingIcon( + icon="account", + ), + MDTextFieldHintText( + text="Hint text", + ), + MDTextFieldHelperText( + text="Helper text", + mode="persistent", + ), + MDTextFieldTrailingIcon( + icon="information", + ), + MDTextFieldMaxLengthText( + max_text_length=10, + ), + mode="filled" if mode == "required" else mode, + text=mode.capitalize() if mode != "required" else "", + required=mode == "required", + ) + ) + for mode in ["outlined", "filled", "required"]: + self.root.ids.custom_widget_box.add_widget( + MDTextField( + MDTextFieldLeadingIcon( + icon="account", + theme_icon_color="Custom", + icon_color_normal="mediumaquamarine", + icon_color_focus="tan", + ), + MDTextFieldHintText( + text="Hint text", + text_color_normal="mediumaquamarine", + text_color_focus="tan", + ), + MDTextFieldHelperText( + text="Helper text", + mode="persistent", + text_color_normal="mediumaquamarine", + text_color_focus="tan", + ), + MDTextFieldTrailingIcon( + icon="information", + theme_icon_color="Custom", + icon_color_normal="mediumaquamarine", + icon_color_focus="tan", + ), + MDTextFieldMaxLengthText( + max_text_length=10, + text_color_normal="mediumaquamarine", + text_color_focus="tan", + ), + mode="filled" if mode == "required" else mode, + text=mode.capitalize() if mode != "required" else "DDDD", + required=mode == "required", + theme_bg_color="Custom", + fill_color_normal="lightcyan", + fill_color_focus="lightsteelblue", + theme_line_color="Custom", + line_color_normal="mediumaquamarine", + line_color_focus="tan", + ) + ) + + +Example().run() diff --git a/kivymd/__init__.py b/kivymd/__init__.py index bb05e0f6d..53f093299 100644 --- a/kivymd/__init__.py +++ b/kivymd/__init__.py @@ -26,6 +26,7 @@ import kivy from kivy.logger import Logger + __version__ = "1.2.0.dev0" """KivyMD version.""" diff --git a/kivymd/color_definitions.py b/kivymd/color_definitions.py deleted file mode 100755 index e4a7de462..000000000 --- a/kivymd/color_definitions.py +++ /dev/null @@ -1,954 +0,0 @@ -""" -Themes/Color Definitions -======================== - -.. seealso:: - - `Material Design spec, The color system `_ - - `Material Design spec, The color tool `_ - -Material colors palette to use in :class:`kivymd.theming.ThemeManager`. -:data:`~colors` is a dict-in-dict where the first key is a value from -:data:`~palette` and the second key is a value from :data:`~hue`. Color is a hex -value, a string of 6 characters (0-9, A-F) written in uppercase. - -For example, ``colors["Red"]["900"]`` is ``"B71C1C"``. -""" - -colors = { - "Red": { - "50": "FFEBEE", - "100": "FFCDD2", - "200": "EF9A9A", - "300": "E57373", - "400": "EF5350", - "500": "F44336", - "600": "E53935", - "700": "D32F2F", - "800": "C62828", - "900": "B71C1C", - "A100": "FF8A80", - "A200": "FF5252", - "A400": "FF1744", - "A700": "D50000", - }, - "Pink": { - "50": "FCE4EC", - "100": "F8BBD0", - "200": "F48FB1", - "300": "F06292", - "400": "EC407A", - "500": "E91E63", - "600": "D81B60", - "700": "C2185B", - "800": "AD1457", - "900": "880E4F", - "A100": "FF80AB", - "A200": "FF4081", - "A400": "F50057", - "A700": "C51162", - }, - "Purple": { - "50": "F3E5F5", - "100": "E1BEE7", - "200": "CE93D8", - "300": "BA68C8", - "400": "AB47BC", - "500": "9C27B0", - "600": "8E24AA", - "700": "7B1FA2", - "800": "6A1B9A", - "900": "4A148C", - "A100": "EA80FC", - "A200": "E040FB", - "A400": "D500F9", - "A700": "AA00FF", - }, - "DeepPurple": { - "50": "EDE7F6", - "100": "D1C4E9", - "200": "B39DDB", - "300": "9575CD", - "400": "7E57C2", - "500": "673AB7", - "600": "5E35B1", - "700": "512DA8", - "800": "4527A0", - "900": "311B92", - "A100": "B388FF", - "A200": "7C4DFF", - "A400": "651FFF", - "A700": "6200EA", - }, - "Indigo": { - "50": "E8EAF6", - "100": "C5CAE9", - "200": "9FA8DA", - "300": "7986CB", - "400": "5C6BC0", - "500": "3F51B5", - "600": "3949AB", - "700": "303F9F", - "800": "283593", - "900": "1A237E", - "A100": "8C9EFF", - "A200": "536DFE", - "A400": "3D5AFE", - "A700": "304FFE", - }, - "Blue": { - "50": "E3F2FD", - "100": "BBDEFB", - "200": "90CAF9", - "300": "64B5F6", - "400": "42A5F5", - "500": "2196F3", - "600": "1E88E5", - "700": "1976D2", - "800": "1565C0", - "900": "0D47A1", - "A100": "82B1FF", - "A200": "448AFF", - "A400": "2979FF", - "A700": "2962FF", - }, - "LightBlue": { - "50": "E1F5FE", - "100": "B3E5FC", - "200": "81D4FA", - "300": "4FC3F7", - "400": "29B6F6", - "500": "03A9F4", - "600": "039BE5", - "700": "0288D1", - "800": "0277BD", - "900": "01579B", - "A100": "80D8FF", - "A200": "40C4FF", - "A400": "00B0FF", - "A700": "0091EA", - }, - "Cyan": { - "50": "E0F7FA", - "100": "B2EBF2", - "200": "80DEEA", - "300": "4DD0E1", - "400": "26C6DA", - "500": "00BCD4", - "600": "00ACC1", - "700": "0097A7", - "800": "00838F", - "900": "006064", - "A100": "84FFFF", - "A200": "18FFFF", - "A400": "00E5FF", - "A700": "00B8D4", - }, - "Teal": { - "50": "E0F2F1", - "100": "B2DFDB", - "200": "80CBC4", - "300": "4DB6AC", - "400": "26A69A", - "500": "009688", - "600": "00897B", - "700": "00796B", - "800": "00695C", - "900": "004D40", - "A100": "A7FFEB", - "A200": "64FFDA", - "A400": "1DE9B6", - "A700": "00BFA5", - }, - "Green": { - "50": "E8F5E9", - "100": "C8E6C9", - "200": "A5D6A7", - "300": "81C784", - "400": "66BB6A", - "500": "4CAF50", - "600": "43A047", - "700": "388E3C", - "800": "2E7D32", - "900": "1B5E20", - "A100": "B9F6CA", - "A200": "69F0AE", - "A400": "00E676", - "A700": "00C853", - }, - "LightGreen": { - "50": "F1F8E9", - "100": "DCEDC8", - "200": "C5E1A5", - "300": "AED581", - "400": "9CCC65", - "500": "8BC34A", - "600": "7CB342", - "700": "689F38", - "800": "558B2F", - "900": "33691E", - "A100": "CCFF90", - "A200": "B2FF59", - "A400": "76FF03", - "A700": "64DD17", - }, - "Lime": { - "50": "F9FBE7", - "100": "F0F4C3", - "200": "E6EE9C", - "300": "DCE775", - "400": "D4E157", - "500": "CDDC39", - "600": "C0CA33", - "700": "AFB42B", - "800": "9E9D24", - "900": "827717", - "A100": "F4FF81", - "A200": "EEFF41", - "A400": "C6FF00", - "A700": "AEEA00", - }, - "Yellow": { - "50": "FFFDE7", - "100": "FFF9C4", - "200": "FFF59D", - "300": "FFF176", - "400": "FFEE58", - "500": "FFEB3B", - "600": "FDD835", - "700": "FBC02D", - "800": "F9A825", - "900": "F57F17", - "A100": "FFFF8D", - "A200": "FFFF00", - "A400": "FFEA00", - "A700": "FFD600", - }, - "Amber": { - "50": "FFF8E1", - "100": "FFECB3", - "200": "FFE082", - "300": "FFD54F", - "400": "FFCA28", - "500": "FFC107", - "600": "FFB300", - "700": "FFA000", - "800": "FF8F00", - "900": "FF6F00", - "A100": "FFE57F", - "A200": "FFD740", - "A400": "FFC400", - "A700": "FFAB00", - }, - "Orange": { - "50": "FFF3E0", - "100": "FFE0B2", - "200": "FFCC80", - "300": "FFB74D", - "400": "FFA726", - "500": "FF9800", - "600": "FB8C00", - "700": "F57C00", - "800": "EF6C00", - "900": "E65100", - "A100": "FFD180", - "A200": "FFAB40", - "A400": "FF9100", - "A700": "FF6D00", - }, - "DeepOrange": { - "50": "FBE9E7", - "100": "FFCCBC", - "200": "FFAB91", - "300": "FF8A65", - "400": "FF7043", - "500": "FF5722", - "600": "F4511E", - "700": "E64A19", - "800": "D84315", - "900": "BF360C", - "A100": "FF9E80", - "A200": "FF6E40", - "A400": "FF3D00", - "A700": "DD2C00", - }, - "Brown": { - "50": "EFEBE9", - "100": "D7CCC8", - "200": "BCAAA4", - "300": "A1887F", - "400": "8D6E63", - "500": "795548", - "600": "6D4C41", - "700": "5D4037", - "800": "4E342E", - "900": "3E2723", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Gray": { - "50": "FAFAFA", - "100": "F5F5F5", - "200": "EEEEEE", - "300": "E0E0E0", - "400": "BDBDBD", - "500": "9E9E9E", - "600": "757575", - "700": "616161", - "800": "424242", - "900": "212121", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "BlueGray": { - "50": "ECEFF1", - "100": "CFD8DC", - "200": "B0BEC5", - "300": "90A4AE", - "400": "78909C", - "500": "607D8B", - "600": "546E7A", - "700": "455A64", - "800": "37474F", - "900": "263238", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Light": { - "StatusBar": "E0E0E0", - "AppBar": "F5F5F5", - "Background": "FAFAFA", - "CardsDialogs": "FFFFFF", - "FlatButtonDown": "cccccc", - }, - "Dark": { - "StatusBar": "000000", - "AppBar": "1f1f1f", - "Background": "121212", - "CardsDialogs": "212121", - "FlatButtonDown": "999999", - }, -} -""" -Color palette. Taken from `2014 Material Design color palettes -`_. - -To demonstrate the shades of the palette, you can run the following code: - -.. code-block:: python - - from kivy.lang import Builder - from kivy.properties import ListProperty, StringProperty - - from kivymd.color_definitions import colors - from kivymd.uix.tab import MDTabsBase - from kivymd.uix.boxlayout import MDBoxLayout - - demo = ''' - - orientation: 'vertical' - - MDTopAppBar: - title: app.title - - MDTabs: - id: android_tabs - on_tab_switch: app.on_tab_switch(*args) - size_hint_y: None - height: "48dp" - tab_indicator_anim: False - - RecycleView: - id: rv - key_viewclass: "viewclass" - key_size: "height" - - RecycleBoxLayout: - default_size: None, dp(48) - default_size_hint: 1, None - size_hint_y: None - height: self.minimum_height - orientation: "vertical" - - - - size_hint_y: None - height: "42dp" - - MDLabel: - text: root.text - halign: "center" - - - - ''' - - from kivy.factory import Factory - - from kivymd.app import MDApp - - - class Tab(MDBoxLayout, MDTabsBase): - pass - - - class ItemColor(MDBoxLayout): - text = StringProperty() - color = ListProperty() - - - class Palette(MDApp): - title = "Colors definitions" - - def build(self): - Builder.load_string(demo) - self.screen = Factory.Root() - - for name_tab in colors.keys(): - tab = Tab(title=name_tab) - self.screen.ids.android_tabs.add_widget(tab) - return self.screen - - def on_tab_switch( - self, instance_tabs, instance_tab, instance_tabs_label, tab_text - ): - self.screen.ids.rv.data = [] - if not tab_text: - tab_text = 'Red' - for value_color in colors[tab_text]: - self.screen.ids.rv.data.append( - { - "viewclass": "ItemColor", - "md_bg_color": colors[tab_text][value_color], - "title": value_color, - } - ) - - def on_start(self): - self.on_tab_switch( - None, - None, - None, - self.screen.ids.android_tabs.ids.layout.children[-1].text, - ) - - - Palette().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/palette.gif - :align: center -""" - -palette = [ - "Red", - "Pink", - "Purple", - "DeepPurple", - "Indigo", - "Blue", - "LightBlue", - "Cyan", - "Teal", - "Green", - "LightGreen", - "Lime", - "Yellow", - "Amber", - "Orange", - "DeepOrange", - "Brown", - "Gray", - "BlueGray", -] -"""Valid values for color palette selecting.""" - -hue = [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "A100", - "A200", - "A400", - "A700", -] -"""Valid values for color hue selecting.""" - - -light_colors = { - "Red": ["50", "100", "200", "300", "A100"], - "Pink": ["50", "100", "200", "A100"], - "Purple": ["50", "100", "200", "A100"], - "DeepPurple": ["50", "100", "200", "A100"], - "Indigo": ["50", "100", "200", "A100"], - "Blue": ["50", "100", "200", "300", "400", "A100"], - "LightBlue": [ - "50", - "100", - "200", - "300", - "400", - "500", - "A100", - "A200", - "A400", - ], - "Cyan": [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "A100", - "A200", - "A400", - "A700", - ], - "Teal": ["50", "100", "200", "300", "400", "A100", "A200", "A400", "A700"], - "Green": [ - "50", - "100", - "200", - "300", - "400", - "500", - "A100", - "A200", - "A400", - "A700", - ], - "LightGreen": [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "A100", - "A200", - "A400", - "A700", - ], - "Lime": [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "A100", - "A200", - "A400", - "A700", - ], - "Yellow": [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "A100", - "A200", - "A400", - "A700", - ], - "Amber": [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "A100", - "A200", - "A400", - "A700", - ], - "Orange": [ - "50", - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "A100", - "A200", - "A400", - "A700", - ], - "DeepOrange": ["50", "100", "200", "300", "400", "A100", "A200"], - "Brown": ["50", "100", "200"], - "Gray": ["50", "100", "200", "300", "400", "500"], - "BlueGray": ["50", "100", "200", "300"], - "Dark": [], - "Light": ["White", "MainBackground", "DialogBackground"], -} -"""Which colors are light. Other are dark.""" - -text_colors = { - "Red": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "Pink": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "FFFFFF", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "Purple": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "FFFFFF", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "DeepPurple": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "FFFFFF", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "Indigo": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "FFFFFF", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "Blue": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "LightBlue": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "FFFFFF", - }, - "Cyan": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "000000", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Teal": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Green": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "LightGreen": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "000000", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Lime": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "000000", - "700": "000000", - "800": "000000", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Yellow": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "000000", - "700": "000000", - "800": "000000", - "900": "000000", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Amber": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "000000", - "700": "000000", - "800": "000000", - "900": "000000", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "Orange": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "000000", - "700": "000000", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "000000", - "A700": "000000", - }, - "DeepOrange": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "000000", - "A200": "000000", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "Brown": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "FFFFFF", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "FFFFFF", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "Gray": { - "50": "FFFFFF", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "000000", - "500": "000000", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "FFFFFF", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, - "BlueGray": { - "50": "000000", - "100": "000000", - "200": "000000", - "300": "000000", - "400": "FFFFFF", - "500": "FFFFFF", - "600": "FFFFFF", - "700": "FFFFFF", - "800": "FFFFFF", - "900": "FFFFFF", - "A100": "FFFFFF", - "A200": "FFFFFF", - "A400": "FFFFFF", - "A700": "FFFFFF", - }, -} -""" -Text colors generated from :data:`~light_colors`. "000000" for light and -"FFFFFF" for dark. - -How to generate text_colors dict - -.. code-block:: python - - text_colors = {} - for p in palette: - text_colors[p] = {} - for h in hue: - if h in light_colors[p]: - text_colors[p][h] = "000000" - else: - text_colors[p][h] = "FFFFFF" -""" - -theme_colors = [ - "Primary", - "Secondary", - "Background", - "Surface", - "Error", - "On_Primary", - "On_Secondary", - "On_Background", - "On_Surface", - "On_Error", -] -"""Valid theme colors.""" diff --git a/kivymd/dynamic_color.py b/kivymd/dynamic_color.py new file mode 100644 index 000000000..bbe8b2679 --- /dev/null +++ b/kivymd/dynamic_color.py @@ -0,0 +1,632 @@ +""" +Components/Dynamic color +======================== + +.. seealso:: + + `Material Design spec, Dynamic color + `_ + +.. rubric:: Dynamic color can create accessible UI color schemes based on + content or user settings + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dynamic-color.png + :align: center + +Dynamic color experiences are built with M3 color schemes. Beginning with +Android 12, users can generate individualized schemes through wallpaper +selection and other customization settings. With M3 as a foundation, +user-generated colors can coexist with app colors, putting a range of +customizable visual experiences in the hands of users. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dynamic-color-preview.png + :align: center + +1. Baseline scheme +2. Colors extracted from a wallpaper +3. Colors extracted from content + +Example of dynamic color from the list of standard color schemes +---------------------------------------------------------------- + +.. code-block:: python + + from kivy.clock import Clock + from kivy.lang import Builder + from kivy.properties import StringProperty, ColorProperty + from kivy.uix.boxlayout import BoxLayout + from kivy.utils import hex_colormap + + from kivymd.uix.menu import MDDropdownMenu + from kivymd.app import MDApp + + + KV = ''' + + orientation: "vertical" + + MDLabel: + text: root.text + color: "grey" + adaptive_height: True + + MDCard: + theme_bg_color: "Custom" + md_bg_color: root.bg_color + + + MDScreen: + md_bg_color: app.theme_cls.backgroundColor + + MDIconButton: + on_release: app.open_menu(self) + pos_hint: {"top": .98} + x: "12dp" + icon: "menu" + + MDRecycleView: + id: card_list + viewclass: "ColorCard" + bar_width: 0 + size_hint_y: None + height: root.height - dp(68) + + RecycleGridLayout: + cols: 3 + spacing: "16dp" + padding: "16dp" + default_size: None, dp(56) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + ''' + + + class ColorCard(BoxLayout): + text = StringProperty() + bg_color = ColorProperty() + + + class Example(MDApp): + menu: MDDropdownMenu = None + + def build(self): + self.theme_cls.dynamic_color = True + return Builder.load_string(KV) + + def get_instance_from_menu(self, name_item): + index = 0 + rv = self.menu.ids.md_menu + opts = rv.layout_manager.view_opts + datas = rv.data[0] + + for data in rv.data: + if data["text"] == name_item: + index = rv.data.index(data) + break + + instance = rv.view_adapter.get_view( + index, datas, opts[index]["viewclass"] + ) + + return instance + + def open_menu(self, menu_button): + menu_items = [] + for item, method in { + "Set palette": lambda: self.set_palette(), + "Switch theme style": lambda: self.theme_switch(), + }.items(): + menu_items.append({"text": item, "on_release": method}) + self.menu = MDDropdownMenu( + caller=menu_button, + items=menu_items, + ) + self.menu.open() + + def set_palette(self): + instance_from_menu = self.get_instance_from_menu("Set palette") + available_palettes = [ + name_color.capitalize() for name_color in hex_colormap.keys() + ] + + menu_items = [] + for name_palette in available_palettes: + menu_items.append( + { + "text": name_palette, + "on_release": lambda x=name_palette: self.switch_palette(x), + } + ) + MDDropdownMenu( + caller=instance_from_menu, + items=menu_items, + ).open() + + def switch_palette(self, selected_palette): + self.theme_cls.primary_palette = selected_palette + Clock.schedule_once(self.generate_cards, 0.5) + + def theme_switch(self) -> None: + self.theme_cls.switch_theme() + Clock.schedule_once(self.generate_cards, 0.5) + + def generate_cards(self, *args): + self.root.ids.card_list.data = [] + for color in self.theme_cls.schemes_name_colors: + value = f"{color}Color" + self.root.ids.card_list.data.append( + { + "bg_color": getattr(self.theme_cls, value), + "text": value, + } + ) + + def on_start(self): + Clock.schedule_once(self.generate_cards) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dynamic-color.gif + :align: center + +Example of a dynamic color from an image +---------------------------------------- + +.. seealso:: + + :attr:`kivymd.theming.ThemeManager.path_to_wallpaper` + +.. code-block:: python + + import os + + 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.properties import StringProperty, ColorProperty + + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.app import MDApp + + + KV = ''' + + orientation: "vertical" + + MDLabel: + text: root.text + color: "grey" + adaptive_height: True + + MDCard: + theme_bg_color: "Custom" + md_bg_color: root.bg_color + + + MDScreen: + md_bg_color: app.theme_cls.backgroundColor + + MDRecycleView: + id: card_list + viewclass: "ColorCard" + bar_width: 0 + + RecycleGridLayout: + cols: 3 + spacing: "16dp" + padding: "16dp" + default_size: None, dp(56) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + ''' + + + class ColorCard(MDBoxLayout): + text = StringProperty() + bg_color = ColorProperty() + + + class Example(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + Window.bind(on_dropfile=self.on_drop_file) + + def on_drop_file(self, sdl: WindowSDL, path_to_file: str) -> None: + ext = os.path.splitext(path_to_file)[1] + if isinstance(path_to_file, bytes): + path_to_file = path_to_file.decode() + if isinstance(ext, bytes): + ext = ext.decode() + if ext in [".png", ".jpg"]: + self.theme_cls.path_to_wallpaper = path_to_file + Clock.schedule_once(self.generate_cards, 0.5) + + def build(self): + self.theme_cls.dynamic_color = True + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + def theme_switch(self) -> None: + self.theme_cls.switch_theme() + Clock.schedule_once(self.generate_cards, 0.5) + + def generate_cards(self, *args): + self.root.ids.card_list.data = [] + for color in self.theme_cls.schemes_name_colors: + value = f"{color}Color" + self.root.ids.card_list.data.append( + { + "bg_color": getattr(self.theme_cls, value), + "text": value, + } + ) + + def on_start(self): + Clock.schedule_once(self.generate_cards) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dynamic-color-path-to_wallpapper.gif + :align: center +""" + +from kivy.properties import ColorProperty + + +class DynamicColor: + """ + Dynamic color class. + + .. versionadded:: 2.0.0 + """ + + # Primary. + primaryColor = ColorProperty() + """ + Primary color. + + :attr:`primaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + primaryContainerColor = ColorProperty() + """ + Primary container color. + + :attr:`primaryContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # On Primary. + onPrimaryColor = ColorProperty() + """ + On primary color. + + :attr:`onPrimaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + onPrimaryContainerColor = ColorProperty() + """ + On primary container color. + + :attr:`onPrimaryContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Secondary. + secondaryColor = ColorProperty() + """ + Secondary color. + + :attr:`secondaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + secondaryContainerColor = ColorProperty() + """ + Secondary container color. + + :attr:`secondaryContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # On Secondary. + onSecondaryColor = ColorProperty() + """ + On secondary color. + + :attr:`onSecondaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + onSecondaryContainerColor = ColorProperty() + """ + On secondary container color. + + :attr:`onSecondaryContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Tertiary. + tertiaryColor = ColorProperty() + """ + Tertiary color. + + :attr:`tertiaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + tertiaryContainerColor = ColorProperty() + """ + Tertiary container color. + + :attr:`tertiaryContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # On Tertiary. + onTertiaryColor = ColorProperty() + """ + On tertiary color. + + :attr:`onTertiaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + onTertiaryContainerColor = ColorProperty() + """ + On tertiary container color. + + :attr:`onTertiaryContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Surface. + surfaceColor = ColorProperty() + """ + Surface color. + + :attr:`surfaceColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceDimColor = ColorProperty() + """ + Surface dim color. + + :attr:`surfaceDimColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceBrightColor = ColorProperty() + """ + Surface bright color. + + :attr:`surfaceBrightColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceContainerLowestColor = ColorProperty() + """ + Surface container lowest color. + + :attr:`surfaceContainerLowestColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceContainerLowColor = ColorProperty() + """ + Surface container low color. + + :attr:`surfaceContainerLowColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceContainerColor = ColorProperty() + """ + Surface container color. + + :attr:`surfaceContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceContainerHighColor = ColorProperty() + """ + Surface container high color. + + :attr:`surfaceContainerHighColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceContainerHighestColor = ColorProperty() + """ + Surface container highest color. + + :attr:`surfaceContainerHighestColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceVariantColor = ColorProperty() + """ + Surface variant color. + + :attr:`surfaceVariantColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + surfaceTintColor = ColorProperty() + """ + Surface tint color. + + :attr:`surfaceTintColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # On Surface. + onSurfaceColor = ColorProperty() + """ + On surface color. + + :attr:`onSurfaceColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + onSurfaceLightColor = ColorProperty() + """ + On surface light color. + + :attr:`onSurfaceLightColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + onSurfaceVariantColor = ColorProperty() + """ + On surface variant color. + + :attr:`onSurfaceVariantColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Inverse. + inverseSurfaceColor = ColorProperty() + """ + Inverse surface color. + + :attr:`inverseSurfaceColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + inverseOnSurfaceColor = ColorProperty() + """ + Inverse on surface color. + + :attr:`inverseOnSurfaceColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + inversePrimaryColor = ColorProperty() + """ + Inverse primary color. + + :attr:`inversePrimaryColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Background. + backgroundColor = ColorProperty() + """ + Background color. + + :attr:`backgroundColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # On Background. + onBackgroundColor = ColorProperty() + """ + On background color. + + :attr:`onBackgroundColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Error. + errorColor = ColorProperty() + """ + Error color. + + :attr:`errorColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + errorContainerColor = ColorProperty() + """ + Error container color. + + :attr:`errorContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # On Error. + onErrorColor = ColorProperty() + """ + On error color. + + :attr:`onErrorColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + onErrorContainerColor = ColorProperty() + """ + On error container color. + + :attr:`onErrorContainerColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Outline. + outlineColor = ColorProperty() + """ + Outline color. + + :attr:`outlineColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + outlineVariantColor = ColorProperty() + """ + Outline variant color. + + :attr:`outlineVariantColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Shadow/scrim. + shadowColor = ColorProperty() + """ + Shadow color. + + :attr:`shadowColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + scrimColor = ColorProperty() + """ + Scrim color. + + :attr:`scrimColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Disabled. + disabledTextColor = ColorProperty() + """ + Disabled text color. + + :attr:`disabledTextColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + # Transparent. + transparentColor = ColorProperty([0, 0, 0, 0]) + """ + Transparent color. + + :attr:`transparentColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + # Ripple. + rippleColor = ColorProperty("#BDBDBD") + """ + Ripple color. + + :attr:`rippleColor` is an :class:`~kivy.properties.ColorProperty` + and defaults to `'#BDBDBD'`. + """ diff --git a/kivymd/factory_registers.py b/kivymd/factory_registers.py index 4497bfe55..03b55750f 100644 --- a/kivymd/factory_registers.py +++ b/kivymd/factory_registers.py @@ -5,8 +5,11 @@ from kivy.factory import Factory register = Factory.register +# register("MDDataTable", module="kivymd.uix.datatables") register("MDSegmentedButton", module="kivymd.uix.segmentedbutton") register("MDSegmentedButtonItem", module="kivymd.uix.segmentedbutton") +register("MDSegmentButtonIcon", module="kivymd.uix.segmentedbutton") +register("MDSegmentButtonLabel", module="kivymd.uix.segmentedbutton") register("MDScrollView", module="kivymd.uix.scrollview") register("MDRecycleView", module="kivymd.uix.recycleview") register("MDResponsiveLayout", module="kivymd.uix.responsivelayout") @@ -19,6 +22,8 @@ register("MDNavigationRail", module="kivymd.uix.navigationrail") register("MDNavigationRailFabButton", module="kivymd.uix.navigationrail") register("MDNavigationRailMenuButton", module="kivymd.uix.navigationrail") +register("MDNavigationRailItemIcon", module="kivymd.uix.navigationrail") +register("MDNavigationRailItemLabel", module="kivymd.uix.navigationrail") register("MDSwiper", module="kivymd.uix.swiper") register("MDCarousel", module="kivymd.uix.carousel") register("MDWidget", module="kivymd.uix.widget") @@ -37,61 +42,56 @@ register("MDExpansionPanelThreeLine", module="kivymd.uix.expansionpanel") register("FitImage", module="kivymd.uix.fitimage") register("MDBackdrop", module="kivymd.uix.backdrop") -register("MDBanner", module="kivymd.uix.banner") register("MDTooltip", module="kivymd.uix.tooltip") register("MDBottomSheet", module="kivymd.uix.bottomsheet") -register("MDBottomNavigation", module="kivymd.uix.bottomnavigation") -register("MDBottomNavigationItem", module="kivymd.uix.bottomnavigation") +register("MDNavigationBar", module="kivymd.uix.navigationbar") +register("MDNavigationItem", module="kivymd.uix.navigationbar") +register("MDNavigationItemLabel", module="kivymd.uix.navigationbar") +register("MDNavigationItemIcon", module="kivymd.uix.navigationbar") register("MDToggleButton", module="kivymd.uix.behaviors.toggle_behavior") -register("MDFloatingActionButtonSpeedDial", module="kivymd.uix.button") +register("MDButton", module="kivymd.uix.button") +register("MDButtonText", module="kivymd.uix.button") +register("MDButtonIcon", module="kivymd.uix.button") +register("MDFabButton", module="kivymd.uix.button") register("MDIconButton", module="kivymd.uix.button") -register("MDRoundImageButton", module="kivymd.uix.button") -register("MDFlatButton", module="kivymd.uix.button") -register("MDRaisedButton", module="kivymd.uix.button") -register("MDFloatingActionButton", module="kivymd.uix.button") -register("MDRectangleFlatButton", module="kivymd.uix.button") -register("MDTextButton", module="kivymd.uix.button") -register("MDCustomRoundIconButton", module="kivymd.uix.button") -register("MDRoundFlatButton", module="kivymd.uix.button") -register("MDFillRoundFlatButton", module="kivymd.uix.button") -register("MDRectangleFlatIconButton", module="kivymd.uix.button") -register("MDRoundFlatIconButton", module="kivymd.uix.button") -register("MDFillRoundFlatIconButton", module="kivymd.uix.button") +register("MDExtendedFabButton", module="kivymd.uix.button") +register("MDExtendedFabButtonIcon", module="kivymd.uix.button") +register("MDExtendedFabButtonText", module="kivymd.uix.button") register("MDCard", module="kivymd.uix.card") -register("MDSeparator", module="kivymd.uix.card") -register("MDSelectionList", module="kivymd.uix.selection") +register("MDDivider", module="kivymd.uix.divider") register("MDChip", module="kivymd.uix.chip") +register("MDChipLeadingAvatar", module="kivymd.uix.chip") +register("MDChipLeadingIcon", module="kivymd.uix.chip") +register("MDChipTrailingIcon", module="kivymd.uix.chip") +register("MDChipText", module="kivymd.uix.chip") register("MDSmartTile", module="kivymd.uix.imagelist") register("MDLabel", module="kivymd.uix.label") register("MDIcon", module="kivymd.uix.label") +register("MDBadge", module="kivymd.uix.badge") register("MDList", module="kivymd.uix.list") -register("ILeftBody", module="kivymd.uix.list") -register("ILeftBodyTouch", module="kivymd.uix.list") -register("IRightBody", module="kivymd.uix.list") -register("IRightBodyTouch", module="kivymd.uix.list") -register("OneLineListItem", module="kivymd.uix.list") -register("TwoLineListItem", module="kivymd.uix.list") -register("ThreeLineListItem", module="kivymd.uix.list") -register("OneLineAvatarListItem", module="kivymd.uix.list") -register("TwoLineAvatarListItem", module="kivymd.uix.list") -register("ThreeLineAvatarListItem", module="kivymd.uix.list") -register("OneLineIconListItem", module="kivymd.uix.list") -register("TwoLineIconListItem", module="kivymd.uix.list") -register("ThreeLineIconListItem", module="kivymd.uix.list") -register("OneLineRightIconListItem", module="kivymd.uix.list") -register("TwoLineRightIconListItem", module="kivymd.uix.list") -register("ThreeLineRightIconListItem", module="kivymd.uix.list") -register("OneLineAvatarIconListItem", module="kivymd.uix.list") -register("TwoLineAvatarIconListItem", module="kivymd.uix.list") -register("ThreeLineAvatarIconListItem", module="kivymd.uix.list") +register("MDListItem", module="kivymd.uix.list") +register("MDListItemHeadlineText", module="kivymd.uix.list") +register("MDListItemSupportingText", module="kivymd.uix.list") +register("MDListItemTrailingSupportingText", module="kivymd.uix.list") +register("MDListItemLeadingIcon", module="kivymd.uix.list") +register("MDListItemTrailingIcon", module="kivymd.uix.list") +register("MDListItemTrailingCheckbox", module="kivymd.uix.list") +register("MDListItemTertiaryText", module="kivymd.uix.list") register("HoverBehavior", module="kivymd.uix.behaviors.hover_behavior") register("FocusBehavior", module="kivymd.uix.behaviors.focus_behavior") register("MagicBehavior", module="kivymd.uix.behaviors.magic_behavior") register("MDNavigationDrawer", module="kivymd.uix.navigationdrawer") register("MDNavigationLayout", module="kivymd.uix.navigationdrawer") register("MDNavigationDrawerMenu", module="kivymd.uix.navigationdrawer") -register("MDNavigationDrawerHeader", module="kivymd.uix.navigationdrawer") register("MDNavigationDrawerItem", module="kivymd.uix.navigationdrawer") +register( + "MDNavigationDrawerItemLeadingIcon", module="kivymd.uix.navigationdrawer" +) +register( + "MDNavigationDrawerItemTrailingText", module="kivymd.uix.navigationdrawer" +) +register("MDNavigationDrawerHeader", module="kivymd.uix.navigationdrawer") +register("MDNavigationDrawerItemText", module="kivymd.uix.navigationdrawer") register("MDNavigationDrawerLabel", module="kivymd.uix.navigationdrawer") register("MDNavigationDrawerDivider", module="kivymd.uix.navigationdrawer") register("MDProgressBar", module="kivymd.uix.progressbar") @@ -102,9 +102,19 @@ register("MDSpinner", module="kivymd.uix.spinner") register("MDTabs", module="kivymd.uix.tab") register("MDTextField", module="kivymd.uix.textfield") -register("MDTextFieldRect", module="kivymd.uix.textfield") -register("MDTopAppBar", module="kivymd.uix.toolbar") -register("MDBottomAppBar", module="kivymd.uix.toolbar") +register("MDTextFieldHelperText", module="kivymd.uix.textfield") +register("MDTextFieldMaxLengthText", module="kivymd.uix.textfield") +register("MDTextFieldHintText", module="kivymd.uix.textfield") +register("MDTextFieldLeadingIcon", module="kivymd.uix.textfield") +register("MDTextFieldTrailingIcon", module="kivymd.uix.textfield") +register("MDTopAppBarTrailingButtonContainer", module="kivymd.uix.appbar") +register("MDTopAppBarLeadingButtonContainer", module="kivymd.uix.appbar") +register("MDFabBottomAppBarButton", module="kivymd.uix.appbar") +register("MDActionBottomAppBarButton", module="kivymd.uix.appbar") +register("MDTopAppBarTitle", module="kivymd.uix.appbar") +register("MDTopAppBar", module="kivymd.uix.appbar") +register("MDBottomAppBar", module="kivymd.uix.appbar") +register("MDActionTopAppBarButton", module="kivymd.uix.appbar") register("MDDropDownItem", module="kivymd.uix.dropdownitem") register("MDCircularLayout", module="kivymd.uix.circularlayout") register("MDHeroFrom", module="kivymd.uix.hero") diff --git a/kivymd/font_definitions.py b/kivymd/font_definitions.py index e7cc8e2b1..c3637e4df 100755 --- a/kivymd/font_definitions.py +++ b/kivymd/font_definitions.py @@ -1,5 +1,5 @@ """ -Themes/Font Definitions +Themes/Font definitions ======================= .. seealso:: @@ -8,6 +8,7 @@ """ from kivy.core.text import LabelBase +from kivy.metrics import sp from kivymd import fonts_path @@ -48,22 +49,102 @@ for font in fonts: LabelBase.register(**font) -theme_font_styles = [ - "H1", - "H2", - "H3", - "H4", - "H5", - "H6", - "Subtitle1", - "Subtitle2", - "Body1", - "Body2", - "Button", - "Caption", - "Overline", - "Icon", -] +# TODO: Add `weight` properties. +theme_font_styles = { + "Icon": { + "large": { + "line-height": 1, + "font-name": "Icons", + "font-size": sp(24), + }, + }, + "Display": { + "large": { + "line-height": 1.64, + "font-name": "Roboto", + "font-size": sp(57), + }, + "medium": { + "line-height": 1.52, + "font-name": "Roboto", + "font-size": sp(45), + }, + "small": { + "line-height": 1.44, + "font-name": "Roboto", + "font-size": sp(36), + }, + }, + "Headline": { + "large": { + "line-height": 1.40, + "font-name": "Roboto", + "font-size": sp(32), + }, + "medium": { + "line-height": 1.36, + "font-name": "Roboto", + "font-size": sp(28), + }, + "small": { + "line-height": 1.32, + "font-name": "Roboto", + "font-size": sp(24), + }, + }, + "Title": { + "large": { + "line-height": 1.28, + "font-name": "Roboto", + "font-size": sp(22), + }, + "medium": { + "line-height": 1.24, + "font-name": "Roboto", + "font-size": sp(16), + }, + "small": { + "line-height": 1.20, + "font-name": "Roboto", + "font-size": sp(14), + }, + }, + "Body": { + "large": { + "line-height": 1.24, + "font-name": "Roboto", + "font-size": sp(16), + }, + "medium": { + "line-height": 1.20, + "font-name": "Roboto", + "font-size": sp(14), + }, + "small": { + "line-height": 1.16, + "font-name": "Roboto", + "font-size": sp(12), + }, + }, + "Label": { + "large": { + "line-height": 1.20, + "font-name": "Roboto", + "font-size": sp(14), + }, + "medium": { + "line-height": 1.16, + "font-name": "Roboto", + "font-size": sp(12), + }, + "small": { + "line-height": 1.16, + "font-name": "Roboto", + "font-size": sp(11), + }, + }, +} """ -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles-2.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/label-font-style-preview.png + :align: center """ diff --git a/kivymd/fonts/materialdesignicons-webfont.ttf b/kivymd/fonts/materialdesignicons-webfont.ttf index b00c684d3..53061f16f 100644 Binary files a/kivymd/fonts/materialdesignicons-webfont.ttf and b/kivymd/fonts/materialdesignicons-webfont.ttf differ diff --git a/kivymd/icon_definitions.py b/kivymd/icon_definitions.py index 53cb54072..9553457c4 100755 --- a/kivymd/icon_definitions.py +++ b/kivymd/icon_definitions.py @@ -12,7 +12,7 @@ List of icons from materialdesignicons.com. These expanded material design icons are maintained by Austin Andrews (Templarian on Github). -LAST UPDATED: Version 7.2.96 +LAST UPDATED: Version 7.1.96 To preview the icons and their names, you can use the following application: ---------------------------------------------------------------------------- @@ -779,7 +779,6 @@ def on_start(self): "aurora": "\U000F1BB9", "auto-download": "\U000F137E", "auto-fix": "\U000F0068", - "auto-mode": "\U000F1C20", "auto-upload": "\U000F0069", "autorenew": "\U000F006A", "autorenew-off": "\U000F19E7", @@ -879,7 +878,6 @@ def on_start(self): "baseball-bat": "\U000F0853", "baseball-diamond": "\U000F15EC", "baseball-diamond-outline": "\U000F15ED", - "baseball-outline": "\U000F1C5A", "bash": "\U000F1183", "basket": "\U000F0076", "basket-check": "\U000F18E5", @@ -1046,8 +1044,6 @@ def on_start(self): "bell-ring-outline": "\U000F009F", "bell-sleep": "\U000F00A0", "bell-sleep-outline": "\U000F0A93", - "bench": "\U000F1C21", - "bench-back": "\U000F1C22", "beta": "\U000F00A1", "betamax": "\U000F09CB", "biathlon": "\U000F0E14", @@ -1058,9 +1054,6 @@ def on_start(self): "bicycle-penny-farthing": "\U000F15E9", "bike": "\U000F00A3", "bike-fast": "\U000F111F", - "bike-pedal": "\U000F1C23", - "bike-pedal-clipless": "\U000F1C24", - "bike-pedal-mountain": "\U000F1C25", "billboard": "\U000F1010", "billiards": "\U000F0B61", "billiards-rack": "\U000F0B62", @@ -1417,7 +1410,6 @@ def on_start(self): "calendar-search": "\U000F094C", "calendar-search-outline": "\U000F1B6E", "calendar-star": "\U000F09D3", - "calendar-star-four-points": "\U000F1C1F", "calendar-star-outline": "\U000F1B53", "calendar-start": "\U000F166D", "calendar-start-outline": "\U000F1B6F", @@ -1645,7 +1637,6 @@ def on_start(self): "cash-marker": "\U000F0DB8", "cash-minus": "\U000F1260", "cash-multiple": "\U000F0116", - "cash-off": "\U000F1C79", "cash-plus": "\U000F1261", "cash-refund": "\U000F0A9C", "cash-register": "\U000F0CF4", @@ -1764,8 +1755,6 @@ def on_start(self): "check-underline-circle": "\U000F0E20", "check-underline-circle-outline": "\U000F0E21", "checkbook": "\U000F0A9D", - "checkbook-arrow-left": "\U000F1C1D", - "checkbook-arrow-right": "\U000F1C1E", "checkbox-blank": "\U000F012E", "checkbox-blank-badge": "\U000F1176", "checkbox-blank-badge-outline": "\U000F0117", @@ -1778,8 +1767,6 @@ def on_start(self): "checkbox-intermediate-variant": "\U000F1B54", "checkbox-marked": "\U000F0132", "checkbox-marked-circle": "\U000F0133", - "checkbox-marked-circle-auto-outline": "\U000F1C26", - "checkbox-marked-circle-minus-outline": "\U000F1C27", "checkbox-marked-circle-outline": "\U000F0134", "checkbox-marked-circle-plus-outline": "\U000F1927", "checkbox-marked-outline": "\U000F0135", @@ -1875,7 +1862,6 @@ def on_start(self): "circle-small": "\U000F09DF", "circular-saw": "\U000F0E22", "city": "\U000F0146", - "city-switch": "\U000F1C28", "city-variant": "\U000F0A36", "city-variant-outline": "\U000F0A37", "clipboard": "\U000F0147", @@ -1956,8 +1942,6 @@ def on_start(self): "clock-plus-outline": "\U000F1862", "clock-remove": "\U000F1865", "clock-remove-outline": "\U000F1866", - "clock-star-four-points": "\U000F1C29", - "clock-star-four-points-outline": "\U000F1C2A", "clock-start": "\U000F0155", "clock-time-eight": "\U000F1446", "clock-time-eight-outline": "\U000F1452", @@ -2058,7 +2042,6 @@ def on_start(self): "cloud-upload-outline": "\U000F0B7E", "clouds": "\U000F1B95", "clover": "\U000F0816", - "clover-outline": "\U000F1C62", "coach-lamp": "\U000F1020", "coach-lamp-variant": "\U000F1A37", "coat-rack": "\U000F109E", @@ -2265,7 +2248,6 @@ def on_start(self): "cradle-outline": "\U000F1991", "crane": "\U000F0862", "creation": "\U000F0674", - "creation-outline": "\U000F1C2B", "creative-commons": "\U000F0D6B", "credit-card": "\U000F0FEF", "credit-card-check": "\U000F13D0", @@ -2578,7 +2560,6 @@ def on_start(self): "domain-off": "\U000F0D6F", "domain-plus": "\U000F10AD", "domain-remove": "\U000F10AE", - "domain-switch": "\U000F1C2C", "dome-light": "\U000F141E", "domino-mask": "\U000F1023", "donkey": "\U000F07C2", @@ -2694,7 +2675,6 @@ def on_start(self): "email-edit-outline": "\U000F0EE4", "email-fast": "\U000F186F", "email-fast-outline": "\U000F1870", - "email-heart-outline": "\U000F1C5B", "email-lock": "\U000F01F1", "email-lock-outline": "\U000F1B61", "email-mark-as-unread": "\U000F0B92", @@ -2706,7 +2686,6 @@ def on_start(self): "email-off": "\U000F13E3", "email-off-outline": "\U000F13E4", "email-open": "\U000F01EF", - "email-open-heart-outline": "\U000F1C5C", "email-open-multiple": "\U000F0EE9", "email-open-multiple-outline": "\U000F0EEA", "email-open-outline": "\U000F05EF", @@ -2936,8 +2915,6 @@ def on_start(self): "file-document-outline": "\U000F09EE", "file-document-plus": "\U000F1A9D", "file-document-plus-outline": "\U000F1A9E", - "file-document-refresh": "\U000F1C7A", - "file-document-refresh-outline": "\U000F1C7B", "file-document-remove": "\U000F1A9F", "file-document-remove-outline": "\U000F1AA0", "file-download": "\U000F0965", @@ -3023,8 +3000,6 @@ def on_start(self): "file-settings-outline": "\U000F107A", "file-sign": "\U000F19C3", "file-star": "\U000F103A", - "file-star-four-points": "\U000F1C2D", - "file-star-four-points-outline": "\U000F1C2E", "file-star-outline": "\U000F103B", "file-swap": "\U000F0FB4", "file-swap-outline": "\U000F0FB5", @@ -3324,7 +3299,6 @@ def on_start(self): "football-australian": "\U000F025E", "football-helmet": "\U000F025F", "forest": "\U000F1897", - "forest-outline": "\U000F1C63", "forklift": "\U000F07C9", "form-dropdown": "\U000F1400", "form-select": "\U000F1401", @@ -3880,8 +3854,6 @@ def on_start(self): "home-off": "\U000F1A46", "home-off-outline": "\U000F1A47", "home-outline": "\U000F06A1", - "home-percent": "\U000F1C7C", - "home-percent-outline": "\U000F1C7D", "home-plus": "\U000F0975", "home-plus-outline": "\U000F13D6", "home-remove": "\U000F1247", @@ -3891,10 +3863,6 @@ def on_start(self): "home-search-outline": "\U000F13B1", "home-silo": "\U000F1BA0", "home-silo-outline": "\U000F1BA1", - "home-sound-in": "\U000F1C2F", - "home-sound-in-outline": "\U000F1C30", - "home-sound-out": "\U000F1C31", - "home-sound-out-outline": "\U000F1C32", "home-switch": "\U000F1794", "home-switch-outline": "\U000F1795", "home-thermometer": "\U000F0F54", @@ -3991,7 +3959,6 @@ def on_start(self): "image-filter-drama-outline": "\U000F1BFF", "image-filter-frames": "\U000F02F4", "image-filter-hdr": "\U000F02F5", - "image-filter-hdr-outline": "\U000F1C64", "image-filter-none": "\U000F02F6", "image-filter-tilt-shift": "\U000F02F7", "image-filter-vintage": "\U000F02F8", @@ -4042,22 +4009,10 @@ def on_start(self): "induction": "\U000F184C", "infinity": "\U000F06E4", "information": "\U000F02FC", - "information-box": "\U000F1C65", - "information-box-outline": "\U000F1C66", "information-off": "\U000F178C", "information-off-outline": "\U000F178D", "information-outline": "\U000F02FD", - "information-slab-box": "\U000F1C67", - "information-slab-box-outline": "\U000F1C68", - "information-slab-circle": "\U000F1C69", - "information-slab-circle-outline": "\U000F1C6A", - "information-slab-symbol": "\U000F1C6B", - "information-symbol": "\U000F1C6C", "information-variant": "\U000F064E", - "information-variant-box": "\U000F1C6D", - "information-variant-box-outline": "\U000F1C6E", - "information-variant-circle": "\U000F1C6F", - "information-variant-circle-outline": "\U000F1C70", "instagram": "\U000F02FE", "instrument-triangle": "\U000F104E", "integrated-circuit-chip": "\U000F1913", @@ -4178,7 +4133,6 @@ def on_start(self): "land-plots": "\U000F1AB3", "land-plots-circle": "\U000F1AB4", "land-plots-circle-variant": "\U000F1AB5", - "land-plots-marker": "\U000F1C5D", "land-rows-horizontal": "\U000F1AB6", "land-rows-vertical": "\U000F1AB7", "landslide": "\U000F1A48", @@ -4661,7 +4615,6 @@ def on_start(self): "monitor-speaker": "\U000F0F5F", "monitor-speaker-off": "\U000F0F60", "monitor-star": "\U000F0DDC", - "monitor-vertical": "\U000F1C33", "moon-first-quarter": "\U000F0F61", "moon-full": "\U000F0F62", "moon-last-quarter": "\U000F0F63", @@ -4808,9 +4761,7 @@ def on_start(self): "nas": "\U000F08F3", "nativescript": "\U000F0880", "nature": "\U000F038E", - "nature-outline": "\U000F1C71", "nature-people": "\U000F038F", - "nature-people-outline": "\U000F1C72", "navigation": "\U000F0390", "navigation-outline": "\U000F1607", "navigation-variant": "\U000F18F0", @@ -5001,13 +4952,7 @@ def on_start(self): "octagon": "\U000F03C3", "octagon-outline": "\U000F03C4", "octagram": "\U000F06F9", - "octagram-edit": "\U000F1C34", - "octagram-edit-outline": "\U000F1C35", - "octagram-minus": "\U000F1C36", - "octagram-minus-outline": "\U000F1C37", "octagram-outline": "\U000F0775", - "octagram-plus": "\U000F1C38", - "octagram-plus-outline": "\U000F1C39", "octahedron": "\U000F1950", "octahedron-off": "\U000F1951", "odnoklassniki": "\U000F03C5", @@ -5285,8 +5230,6 @@ def on_start(self): "pine-tree": "\U000F0405", "pine-tree-box": "\U000F0406", "pine-tree-fire": "\U000F141A", - "pine-tree-variant": "\U000F1C73", - "pine-tree-variant-outline": "\U000F1C74", "pinterest": "\U000F0407", "pinwheel": "\U000F0AD5", "pinwheel-outline": "\U000F0AD6", @@ -5304,7 +5247,6 @@ def on_start(self): "plane-train": "\U000F1B00", "play": "\U000F040A", "play-box": "\U000F127A", - "play-box-edit-outline": "\U000F1C3A", "play-box-lock": "\U000F1A16", "play-box-lock-open": "\U000F1A17", "play-box-lock-open-outline": "\U000F1A18", @@ -5386,8 +5328,6 @@ def on_start(self): "power-off": "\U000F0902", "power-on": "\U000F0903", "power-plug": "\U000F06A5", - "power-plug-battery": "\U000F1C3B", - "power-plug-battery-outline": "\U000F1C3C", "power-plug-off": "\U000F06A6", "power-plug-off-outline": "\U000F1424", "power-plug-outline": "\U000F1425", @@ -5477,7 +5417,6 @@ def on_start(self): "progress-pencil": "\U000F1787", "progress-question": "\U000F1522", "progress-star": "\U000F1788", - "progress-star-four-points": "\U000F1C3D", "progress-upload": "\U000F0998", "progress-wrench": "\U000F0CBD", "projector": "\U000F042E", @@ -5552,7 +5491,6 @@ def on_start(self): "radioactive-circle-outline": "\U000F185E", "radioactive-off": "\U000F0EC1", "radiobox-blank": "\U000F043D", - "radiobox-indeterminate-variant": "\U000F1C5E", "radiobox-marked": "\U000F043E", "radiology-box": "\U000F14C5", "radiology-box-outline": "\U000F14C6", @@ -5575,22 +5513,10 @@ def on_start(self): "react": "\U000F0708", "read": "\U000F0447", "receipt": "\U000F0824", - "receipt-clock": "\U000F1C3E", - "receipt-clock-outline": "\U000F1C3F", "receipt-outline": "\U000F04F7", - "receipt-send": "\U000F1C40", - "receipt-send-outline": "\U000F1C41", "receipt-text": "\U000F0449", - "receipt-text-arrow-left": "\U000F1C42", - "receipt-text-arrow-left-outline": "\U000F1C43", - "receipt-text-arrow-right": "\U000F1C44", - "receipt-text-arrow-right-outline": "\U000F1C45", "receipt-text-check": "\U000F1A63", "receipt-text-check-outline": "\U000F1A64", - "receipt-text-clock": "\U000F1C46", - "receipt-text-clock-outline": "\U000F1C47", - "receipt-text-edit": "\U000F1C48", - "receipt-text-edit-outline": "\U000F1C49", "receipt-text-minus": "\U000F1A65", "receipt-text-minus-outline": "\U000F1A66", "receipt-text-outline": "\U000F19DC", @@ -5598,8 +5524,6 @@ def on_start(self): "receipt-text-plus-outline": "\U000F1A68", "receipt-text-remove": "\U000F1A69", "receipt-text-remove-outline": "\U000F1A6A", - "receipt-text-send": "\U000F1C4A", - "receipt-text-send-outline": "\U000F1C4B", "record": "\U000F044A", "record-circle": "\U000F0EC2", "record-circle-outline": "\U000F0EC3", @@ -5797,7 +5721,6 @@ def on_start(self): "run-fast": "\U000F046E", "rv-truck": "\U000F11D4", "sack": "\U000F0D2E", - "sack-outline": "\U000F1C4C", "sack-percent": "\U000F0D2F", "safe": "\U000F0A6A", "safe-square": "\U000F127C", @@ -5916,10 +5839,6 @@ def on_start(self): "send-lock": "\U000F07ED", "send-lock-outline": "\U000F1166", "send-outline": "\U000F1165", - "send-variant": "\U000F1C4D", - "send-variant-clock": "\U000F1C7E", - "send-variant-clock-outline": "\U000F1C7F", - "send-variant-outline": "\U000F1C4E", "serial-port": "\U000F065C", "server": "\U000F048B", "server-minus": "\U000F048C", @@ -5949,7 +5868,6 @@ def on_start(self): "shape-outline": "\U000F0832", "shape-oval-plus": "\U000F11FA", "shape-plus": "\U000F0495", - "shape-plus-outline": "\U000F1C4F", "shape-polygon-plus": "\U000F065E", "shape-rectangle-plus": "\U000F065F", "shape-square-plus": "\U000F0660", @@ -6268,7 +6186,6 @@ def on_start(self): "sphere": "\U000F1954", "sphere-off": "\U000F1955", "spider": "\U000F11EA", - "spider-outline": "\U000F1C75", "spider-thread": "\U000F11EB", "spider-web": "\U000F0BCA", "spirit-level": "\U000F14F1", @@ -6285,7 +6202,6 @@ def on_start(self): "sprout-outline": "\U000F0E67", "square": "\U000F0764", "square-circle": "\U000F1500", - "square-circle-outline": "\U000F1C50", "square-edit-outline": "\U000F090C", "square-medium": "\U000F0A13", "square-medium-outline": "\U000F0A14", @@ -6330,12 +6246,7 @@ def on_start(self): "star-david": "\U000F097A", "star-face": "\U000F09A5", "star-four-points": "\U000F0AE2", - "star-four-points-box": "\U000F1C51", - "star-four-points-box-outline": "\U000F1C52", - "star-four-points-circle": "\U000F1C53", - "star-four-points-circle-outline": "\U000F1C54", "star-four-points-outline": "\U000F0AE3", - "star-four-points-small": "\U000F1C55", "star-half": "\U000F0246", "star-half-full": "\U000F04D0", "star-minus": "\U000F1564", @@ -6566,7 +6477,6 @@ def on_start(self): "tag-faces": "\U000F04FA", "tag-heart": "\U000F068B", "tag-heart-outline": "\U000F0BCF", - "tag-hidden": "\U000F1C76", "tag-minus": "\U000F0910", "tag-minus-outline": "\U000F121F", "tag-multiple": "\U000F04FB", @@ -6624,7 +6534,6 @@ def on_start(self): "temple-hindu-outline": "\U000F1B09", "tennis": "\U000F0DA0", "tennis-ball": "\U000F0507", - "tennis-ball-outline": "\U000F1C5F", "tent": "\U000F0508", "terraform": "\U000F1062", "terrain": "\U000F0509", @@ -6679,7 +6588,6 @@ def on_start(self): "thermostat-auto": "\U000F1B17", "thermostat-box": "\U000F0891", "thermostat-box-auto": "\U000F1B18", - "thermostat-cog": "\U000F1C80", "thought-bubble": "\U000F07F6", "thought-bubble-outline": "\U000F07F7", "thumb-down": "\U000F0511", @@ -6809,7 +6717,6 @@ def on_start(self): "torch": "\U000F1606", "tortoise": "\U000F0D3B", "toslink": "\U000F12B8", - "touch-text-outline": "\U000F1C60", "tournament": "\U000F09AE", "tow-truck": "\U000F083C", "tower-beach": "\U000F0681", @@ -6897,7 +6804,6 @@ def on_start(self): "tray-plus": "\U000F1298", "tray-remove": "\U000F1299", "treasure-chest": "\U000F0726", - "treasure-chest-outline": "\U000F1C77", "tree": "\U000F0531", "tree-outline": "\U000F0E69", "trello": "\U000F0532", @@ -6905,8 +6811,6 @@ def on_start(self): "trending-neutral": "\U000F0534", "trending-up": "\U000F0535", "triangle": "\U000F0536", - "triangle-down": "\U000F1C56", - "triangle-down-outline": "\U000F1C57", "triangle-outline": "\U000F0537", "triangle-small-down": "\U000F1A09", "triangle-small-up": "\U000F1A0A", @@ -7111,7 +7015,6 @@ def on_start(self): "view-gallery": "\U000F1888", "view-gallery-outline": "\U000F1889", "view-grid": "\U000F0570", - "view-grid-compact": "\U000F1C61", "view-grid-outline": "\U000F11D9", "view-grid-plus": "\U000F0F8D", "view-grid-plus-outline": "\U000F11DA", @@ -7174,8 +7077,6 @@ def on_start(self): "wall-sconce-round-variant": "\U000F091E", "wall-sconce-round-variant-outline": "\U000F17CD", "wallet": "\U000F0584", - "wallet-bifold": "\U000F1C58", - "wallet-bifold-outline": "\U000F1C59", "wallet-giftcard": "\U000F0585", "wallet-membership": "\U000F0586", "wallet-outline": "\U000F0BDD", @@ -7248,7 +7149,6 @@ def on_start(self): "weather-hail": "\U000F0592", "weather-hazy": "\U000F0F30", "weather-hurricane": "\U000F0898", - "weather-hurricane-outline": "\U000F1C78", "weather-lightning": "\U000F0593", "weather-lightning-rainy": "\U000F067E", "weather-night": "\U000F0594", @@ -7424,23 +7324,27 @@ def on_start(self): if __name__ == "__main__": from kivy.lang import Builder from kivy.properties import StringProperty - from kivy.uix.screenmanager import Screen + from kivymd.uix.screen import MDScreen from kivymd.app import MDApp - from kivymd.uix.list import OneLineIconListItem + from kivymd.uix.list import MDListItem Builder.load_string( """ #:import images_path kivymd.images_path - + - IconLeftWidget: + MDListItemLeadingIcon: icon: root.icon + MDListItemSupportingText: + text: root.text + + md_bg_color: self.theme_cls.backgroundColor MDBoxLayout: orientation: 'vertical' @@ -7452,6 +7356,7 @@ def on_start(self): MDIconButton: icon: 'magnify' + pos_hint: {'center_y': .5} MDTextField: id: search_field @@ -7464,7 +7369,7 @@ def on_start(self): key_size: 'height' RecycleBoxLayout: - padding: dp(10) + padding: dp(10), dp(10), 0, dp(10) default_size: None, dp(48) default_size_hint: 1, None size_hint_y: None @@ -7473,17 +7378,18 @@ def on_start(self): """ ) - class CustomOneLineIconListItem(OneLineIconListItem): + class IconItem(MDListItem): icon = StringProperty() + text = StringProperty() - class PreviousMDIcons(Screen): + class PreviousMDIcons(MDScreen): def set_list_md_icons(self, text="", search=False): """Builds a list of icons for the screen MDIcons.""" def add_icon_item(name_icon): self.ids.rv.data.append( { - "viewclass": "CustomOneLineIconListItem", + "viewclass": "IconItem", "icon": name_icon, "text": name_icon, "callback": lambda x: x, diff --git a/kivymd/material_resources.py b/kivymd/material_resources.py index 2eb66266b..f2aeb3160 100755 --- a/kivymd/material_resources.py +++ b/kivymd/material_resources.py @@ -21,44 +21,3 @@ DEVICE_TYPE = "tablet" else: DEVICE_TYPE = "mobile" - -if DEVICE_TYPE == "mobile": - MAX_NAV_DRAWER_WIDTH = dp(300) - HORIZ_MARGINS = dp(16) - STANDARD_INCREMENT = dp(56) - PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT - LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT - dp(8) -else: - MAX_NAV_DRAWER_WIDTH = dp(400) - HORIZ_MARGINS = dp(24) - STANDARD_INCREMENT = dp(64) - PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT - LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT - -# Elevation. -SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION = 1 -FILE_MANAGER_TOP_APP_BAR_ELEVATION = 1 -FLOATING_ACTION_BUTTON_M2_ELEVATION = 1 -FLOATING_ACTION_BUTTON_M3_ELEVATION = 0.5 -CARD_STYLE_ELEVATED_M3_ELEVATION = 0.5 -CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION = 0 -DATA_TABLE_ELEVATION = 4 -DROP_DOWN_MENU_ELEVATION = 2 -TOP_APP_BAR_ELEVATION = 2 -SNACK_BAR_ELEVATION = 2 - -# Shadow softness. -RAISED_BUTTON_SOFTNESS = 4 -FLOATING_ACTION_BUTTON_M3_SOFTNESS = 0 -DATA_TABLE_SOFTNESS = 12 -DROP_DOWN_MENU_SOFTNESS = 6 - -# Shadow offset. -RAISED_BUTTON_OFFSET = (0, -2) -FLOATING_ACTION_BUTTON_M2_OFFSET = (0, -1) -FLOATING_ACTION_BUTTON_M3_OFFSET = (0, -2) -DATA_TABLE_OFFSET = (0, -2) -DROP_DOWN_MENU_OFFSET = (0, -2) -SNACK_BAR_OFFSET = (0, -2) - -TOUCH_TARGET_HEIGHT = dp(48) diff --git a/kivymd/tests/memory/test_textfield.py b/kivymd/tests/memory/test_textfield.py index 2f09677b4..ae5370fea 100644 --- a/kivymd/tests/memory/test_textfield.py +++ b/kivymd/tests/memory/test_textfield.py @@ -2,12 +2,23 @@ from kivymd.app import MDApp from kivymd.uix.screen import MDScreen -from kivymd.uix.textfield import MDTextField +from kivymd.uix.textfield import ( + MDTextField, + MDTextFieldLeadingIcon, + MDTextFieldHintText, + MDTextFieldHelperText, + MDTextFieldTrailingIcon, + MDTextFieldMaxLengthText, +) len_callbacks = 0 class MyScreen(MDScreen): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.md_bg_color = self.theme_cls.backgroundColor + def remove_widget(self, *args, **kwargs) -> None: global len_callbacks @@ -31,7 +42,26 @@ def add_items(self, *args): self.counter += 1 self.root.clear_widgets() - self.root.add_widget(MDTextField(text=f"Count {self.counter}")) + self.root.add_widget( + MDTextField( + MDTextFieldLeadingIcon( + icon="account", + ), + MDTextFieldHintText( + text="Hint text", + ), + MDTextFieldHelperText( + text="Helper text", + mode="persistent", + ), + MDTextFieldTrailingIcon( + icon="information", + ), + MDTextFieldMaxLengthText( + max_text_length=10, + ), + ) + ) if self.counter > 10: Clock.unschedule(self.add_items) diff --git a/kivymd/theming.py b/kivymd/theming.py index 0c89bcbc9..f68864482 100755 --- a/kivymd/theming.py +++ b/kivymd/theming.py @@ -4,7 +4,7 @@ .. seealso:: - `Material Design spec, Material theming `_ + `Material Design spec, Dynamic color `_ Material App ------------ @@ -21,231 +21,48 @@ The main application class inherited from the :class:`~kivymd.app.MDApp` class has the :attr:`~kivymd.app.MDApp.theme_cls` attribute, with which you control the material properties of your application. - -Changing the theme colors -------------------------- - -The standard theme_cls is designed to provide the standard themes and colors as -defined by Material Design. - -We do not recommend that you change this. - -However, if you do need to change the standard colors, for instance to meet branding -guidelines, you can do this by overloading the `color_definitions.py` object. - -Create a custom color defintion object. This should have the same format as -the `colors `_ -object in `color_definitions.py` and contain definitions for at least the -primary color, the accent color and the Light and Dark backgrounds. - -.. note:: Your custom colors *must* use the names of the - `existing colors as defined in the palette `_ - e.g. You can have `Blue` but you cannot have `NavyBlue`. - -Add the custom theme to the :class:`~kivymd.app.MDApp` as shown in the -following snippet. - -.. tabs:: - - .. tab:: Imperative python style with KV - - .. code-block:: python - - from kivy.lang import Builder - from kivy.properties import ObjectProperty - - from kivymd.app import MDApp - from kivymd.uix.floatlayout import MDFloatLayout - from kivymd.uix.tab import MDTabsBase - from kivymd.icon_definitions import md_icons - - colors = { - "Teal": { - "200": "#212121", - "500": "#212121", - "700": "#212121", - }, - "Red": { - "200": "#C25554", - "500": "#C25554", - "700": "#C25554", - }, - "Light": { - "StatusBar": "E0E0E0", - "AppBar": "#202020", - "Background": "#2E3032", - "CardsDialogs": "#FFFFFF", - "FlatButtonDown": "#CCCCCC", - }, - } - - - KV = ''' - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - title: "Custom theme" - - MDTabs: - id: tabs - - - - - MDIconButton: - id: icon - icon: root.icon - icon_size: "48sp" - theme_icon_color: "Custom" - icon_color: "white" - pos_hint: {"center_x": .5, "center_y": .5} - ''' - - - class Tab(MDFloatLayout, MDTabsBase): - '''Class implementing content for a tab.''' - - icon = ObjectProperty() - - - class Example(MDApp): - icons = list(md_icons.keys())[15:30] - - def build(self): - self.theme_cls.colors = colors - self.theme_cls.primary_palette = "Teal" - self.theme_cls.accent_palette = "Red" - return Builder.load_string(KV) - - def on_start(self): - for name_tab in self.icons: - tab = Tab(title="This is " + name_tab, icon=name_tab) - self.root.ids.tabs.add_widget(tab) - - - Example().run() - - .. tab:: Declarative python style - - .. code-block:: python - - from kivy.properties import ObjectProperty - - from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.button import MDIconButton - from kivymd.uix.floatlayout import MDFloatLayout - from kivymd.uix.tab import MDTabsBase, MDTabs - from kivymd.icon_definitions import md_icons - from kivymd.uix.toolbar import MDTopAppBar - - colors = { - "Teal": { - "200": "#212121", - "500": "#212121", - "700": "#212121", - }, - "Red": { - "200": "#C25554", - "500": "#C25554", - "700": "#C25554", - }, - "Light": { - "StatusBar": "E0E0E0", - "AppBar": "#202020", - "Background": "#2E3032", - "CardsDialogs": "#FFFFFF", - "FlatButtonDown": "#CCCCCC", - }, - } - - - class Tab(MDFloatLayout, MDTabsBase): - '''Class implementing content for a tab.''' - - icon = ObjectProperty() - - - class Example(MDApp): - icons = list(md_icons.keys())[15:30] - - def build(self): - self.theme_cls.colors = colors - self.theme_cls.primary_palette = "Teal" - self.theme_cls.accent_palette = "Red" - - return ( - MDBoxLayout( - MDTopAppBar(title="Custom theme"), - MDTabs(id="tabs"), - orientation="vertical", - ) - ) - - def on_start(self): - for name_tab in self.icons: - self.root.ids.tabs.add_widget( - Tab( - MDIconButton( - icon=name_tab, - icon_size="48sp", - theme_icon_color="Custom", - icon_color="white", - pos_hint={"center_x": .5, "center_y": .5}, - ), - title="This is " + name_tab, - icon=name_tab, - ) - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-color.png - :align: center - -This will change the theme colors to your custom definition. In all other -respects, the theming stays as documented. - -.. warning:: Please note that the key ``'Red'`` is a required key for the - dictionary :attr:`kivymd.color_definition.colors`. """ -from kivy.animation import Animation from kivy.app import App from kivy.clock import Clock from kivy.core.window import Window from kivy.event import EventDispatcher -from kivy.metrics import dp from kivy.properties import ( AliasProperty, BooleanProperty, - ColorProperty, DictProperty, NumericProperty, ObjectProperty, OptionProperty, StringProperty, + ListProperty, ) -from kivy.utils import get_color_from_hex +from kivy.utils import get_color_from_hex, rgba, hex_colormap -from kivymd.color_definitions import colors, hue, palette +from kivymd.dynamic_color import DynamicColor from kivymd.font_definitions import theme_font_styles -from kivymd.material_resources import DEVICE_IOS, DEVICE_TYPE +from kivymd.material_resources import DEVICE_IOS +from kivymd.utils.get_wallpaper import get_wallpaper + +from materialyoucolor.utils.string_utils import argbFromRgb +from materialyoucolor.utils.theme_utils import ( + getDefaultTheme, + getDominantColors, + themeFromSourceColor, +) -class ThemeManager(EventDispatcher): - primary_palette = OptionProperty("Blue", options=palette) +class ThemeManager(EventDispatcher, DynamicColor): + primary_palette = OptionProperty( + None, + options=[name_color.capitalize() for name_color in hex_colormap.keys()], + ) """ The name of the color scheme that the application will use. All major `material` components will have the color of the specified color theme. - Available options are: `'Red'`, `'Pink'`, `'Purple'`, `'DeepPurple'`, - `'Indigo'`, `'Blue'`, `'LightBlue'`, `'Cyan'`, `'Teal'`, `'Green'`, - `'LightGreen'`, `'Lime'`, `'Yellow'`, `'Amber'`, `'Orange'`, `'DeepOrange'`, - `'Brown'`, `'Gray'`, `'BlueGray'`. + See :attr:`kivy.utils.hex_colormap` keys for available values. To change the color scheme of an application: @@ -308,325 +125,38 @@ def build(self): :align: center :attr:`primary_palette` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'Blue'`. - """ - - primary_hue = OptionProperty("500", options=hue) - """ - The color hue of the application. - - Available options are: `'50'`, `'100'`, `'200'`, `'300'`, `'400'`, `'500'`, - `'600'`, `'700'`, `'800'`, `'900'`, `'A100'`, `'A200'`, `'A400'`, `'A700'`. - - To change the hue color scheme of an application: - - .. tabs:: - - .. tab:: Imperative python style with KV - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.screen import MDScreen - from kivymd.uix.button import MDRectangleFlatButton - - - class MainApp(MDApp): - def build(self): - self.theme_cls.primary_palette = "Orange" - self.theme_cls.primary_hue = "200" # "500" - screen = MDScreen() - screen.add_widget( - MDRectangleFlatButton( - text="Hello, World", - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - return screen - - - MainApp().run() - - .. tab:: Declarative python style - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.button import MDRectangleFlatButton - from kivymd.uix.screen import MDScreen - - - class Example(MDApp): - def build(self): - self.theme_cls.primary_palette = "Orange" - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_hue = "200" # "500" - - return ( - MDScreen( - MDRectangleFlatButton( - text="Hello, World", - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - ) - - - Example().run() - - With a value of ``self.theme_cls.primary_hue = "200"`` and ``self.theme_cls.primary_hue = "500"``: - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary_hue.png - :align: center - - :attr:`primary_hue` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'500'`. - """ - - primary_light_hue = OptionProperty("200", options=hue) - """ - Hue value for :attr:`primary_light`. - - :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'200'`. - """ - - primary_dark_hue = OptionProperty("700", options=hue) - """ - Hue value for :attr:`primary_dark`. - - :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'700'`. - """ - - def _get_primary_color(self) -> list: - return get_color_from_hex( - self.colors[self.primary_palette][self.primary_hue] - ) - - primary_color = AliasProperty( - _get_primary_color, bind=("primary_palette", "primary_hue") - ) - """ - The color of the current application theme. - - :attr:`primary_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value of the current application theme, property is readonly. - """ - - def _get_primary_light(self) -> list: - return get_color_from_hex( - self.colors[self.primary_palette][self.primary_light_hue] - ) - - primary_light = AliasProperty( - _get_primary_light, bind=("primary_palette", "primary_light_hue") - ) - """ - Colors of the current application color theme (in lighter color). - - .. tabs:: - - .. tab:: Declarative style with KV - - .. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - - KV = ''' - MDScreen: - - MDRaisedButton: - text: "primary_light" - pos_hint: {"center_x": 0.5, "center_y": 0.7} - md_bg_color: app.theme_cls.primary_light - - MDRaisedButton: - text: "primary_color" - pos_hint: {"center_x": 0.5, "center_y": 0.5} - - MDRaisedButton: - text: "primary_dark" - pos_hint: {"center_x": 0.5, "center_y": 0.3} - md_bg_color: app.theme_cls.primary_dark - ''' - - - class MainApp(MDApp): - def build(self): - self.theme_cls.primary_palette = "Orange" - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - - MainApp().run() - - .. tab:: Declarative python style - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.button import MDRaisedButton - from kivymd.uix.screen import MDScreen - - - class Example(MDApp): - def build(self): - self.theme_cls.primary_palette = "Orange" - self.theme_cls.theme_style = "Dark" - - return ( - MDScreen( - MDRaisedButton( - text="Primary light", - pos_hint={"center_x": 0.5, "center_y": 0.7}, - md_bg_color=self.theme_cls.primary_light, - ), - MDRaisedButton( - text="Primary color", - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ), - MDRaisedButton( - text="Primary dark", - pos_hint={"center_x": 0.5, "center_y": 0.3}, - md_bg_color=self.theme_cls.primary_dark, - ), - ) - ) - - - Example().run() - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-colors-light-dark.png - :align: center - - :attr:`primary_light` is an :class:`~kivy.properties.AliasProperty` that - returns the value of the current application theme (in lighter color), - property is readonly. - """ - - def _get_primary_dark(self) -> list: - return get_color_from_hex( - self.colors[self.primary_palette][self.primary_dark_hue] - ) - - primary_dark = AliasProperty( - _get_primary_dark, bind=("primary_palette", "primary_dark_hue") - ) - """ - Colors of the current application color theme (in darker color). - - :attr:`primary_dark` is an :class:`~kivy.properties.AliasProperty` that - returns the value of the current application theme (in darker color), - property is readonly. - """ - - accent_palette = OptionProperty("Amber", options=palette) - """ - The application color palette used for items such as the tab indicator - in the :class:`~kivymd.uix.tab.MDTabsBar` class and so on. - See :attr:`kivymd.uix.tab.MDTabsBar.indicator_color` attribute. - - :attr:`accent_palette` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'Amber'`. - """ - - accent_hue = OptionProperty("500", options=hue) - """ - Similar to :attr:`primary_hue`, but returns a value for :attr:`accent_palette`. - - :attr:`accent_hue` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'500'`. - """ - - accent_light_hue = OptionProperty("200", options=hue) - """ - Hue value for :attr:`accent_light`. - - :attr:`accent_light_hue` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'200'`. - """ - - accent_dark_hue = OptionProperty("700", options=hue) - """ - Hue value for :attr:`accent_dark`. - - :attr:`accent_dark_hue` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'700'`. - """ - - def _get_accent_color(self) -> list: - return get_color_from_hex( - self.colors[self.accent_palette][self.accent_hue] - ) - - accent_color = AliasProperty( - _get_accent_color, bind=["accent_palette", "accent_hue"] - ) + and defaults to `None`. """ - Similar to :attr:`primary_color`, but returns a value for :attr:`accent_color`. - - :attr:`accent_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`accent_color`, property is - readonly. - """ - - def _get_accent_light(self) -> list: - return get_color_from_hex( - self.colors[self.accent_palette][self.accent_light_hue] - ) - accent_light = AliasProperty( - _get_accent_light, bind=["accent_palette", "accent_light_hue"] - ) - """ - Similar to :attr:`primary_light`, but returns a value for :attr:`accent_light`. + current_color_theme = DictProperty() - :attr:`accent_light` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`accent_light`, property is - readonly. + dynamic_color = BooleanProperty(False) """ + Enables or disables dynamic color. - def _get_accent_dark(self) -> list: - return get_color_from_hex( - self.colors[self.accent_palette][self.accent_dark_hue] - ) + .. versionchanged:: 2.0.0 - accent_dark = AliasProperty( - _get_accent_dark, bind=["accent_palette", "accent_dark_hue"] - ) - """ - Similar to :attr:`primary_dark`, but returns a value for :attr:`accent_dark`. + .. seealso:: + + `Material Design spec, Dynamic color `_ - :attr:`accent_dark` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`accent_dark`, property is - readonly. + :attr:`dynamic_color` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. """ - material_style = OptionProperty("M3", options=["M2", "M3"]) + path_to_wallpaper = StringProperty() """ - Material design style. - Available options are: 'M2', 'M3'. - - .. versionadded:: 1.0.0 + The path to the image to set the color scheme. You can use this option + if you want to use dynamic color on platforms other than the Android + platform. - .. versionchanged:: 1.2.0 - By default now `'M3'`. + .. versionadded:: 2.0.0 - .. seealso:: - - `Material Design 2 `_ and - `Material Design 3 `_ - - - :attr:`material_style` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'M3'`. + :attr:`path_to_wallpaper` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. """ - theme_style_switch_animation = BooleanProperty(False) + theme_style_switch_animation = BooleanProperty(True) """ Animate app colors when switching app color scheme ('Dark/light'). @@ -746,7 +276,7 @@ def switch_theme_style(self, *args): :align: center :attr:`theme_style_switch_animation` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. + and defaults to `True`. """ theme_style_switch_animation_duration = NumericProperty(0.2) @@ -840,816 +370,522 @@ def _get_theme_style(self, opposite: bool) -> str: else: return self.theme_style - def _get_bg_darkest(self, opposite: bool = False) -> list: + def _get_disabled_hint_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": - return get_color_from_hex(self.colors["Light"]["StatusBar"]) + color = get_color_from_hex("000000") + color[3] = 0.38 elif theme_style == "Dark": - return get_color_from_hex(self.colors["Dark"]["StatusBar"]) + color = get_color_from_hex("FFFFFF") + color[3] = 0.50 + return color - bg_darkest = AliasProperty(_get_bg_darkest, bind=["theme_style"]) + disabled_hint_text_color = AliasProperty( + _get_disabled_hint_text_color, bind=["theme_style"] + ) """ - Similar to :attr:`bg_dark`, - but the color values are a tone lower (darker) than :attr:`bg_dark`. - - .. tabs:: + Color of the disabled text used in the :class:`~kivymd.uix.textfield.MDTextField`. - .. tab:: Declarative style with KV + :attr:`disabled_hint_text_color` + is an :class:`~kivy.properties.AliasProperty` that returns the value + in ``rgba`` format for :attr:`disabled_hint_text_color`, + property is readonly. + """ - .. code-block:: python + def _determine_device_orientation(self, _, window_size) -> None: + if window_size[0] > window_size[1]: + self.device_orientation = "landscape" + elif window_size[1] >= window_size[0]: + self.device_orientation = "portrait" - from kivy.lang import Builder + device_orientation = StringProperty() + """ + Device orientation. - from kivymd.app import MDApp + :attr:`device_orientation` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ - KV = ''' - MDBoxLayout: + # Font name, size (sp), always caps, letter spacing (sp). + font_styles = DictProperty(theme_font_styles) + """ + Data of default font styles. - MDWidget: - md_bg_color: app.theme_cls.bg_light + Add custom font + --------------- - MDBoxLayout: - md_bg_color: app.theme_cls.bg_normal + .. tabs:: - MDBoxLayout: - md_bg_color: app.theme_cls.bg_dark + .. tab:: Declarative style with KV + + .. code-block:: python + + from kivy.core.text import LabelBase + from kivy.lang import Builder + from kivy.metrics import sp - MDBoxLayout: - md_bg_color: app.theme_cls.bg_darkest + from kivymd.app import MDApp + + KV = ''' + MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDLabel: + text: "MDLabel" + halign: "center" + font_style: "nasalization" ''' - class MainApp(MDApp): + class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" # "Light" + self.theme_cls.theme_style = "Dark" + + LabelBase.register( + name="nasalization", + fn_regular="nasalization.ttf", + ) + + self.theme_cls.font_styles["nasalization"] = { + "large": { + "line-height": 1.64, + "font-name": "nasalization", + "font-size": sp(57), + }, + "medium": { + "line-height": 1.52, + "font-name": "nasalization", + "font-size": sp(45), + }, + "small": { + "line-height": 1.44, + "font-name": "nasalization", + "font-size": sp(36), + }, + } + return Builder.load_string(KV) - MainApp().run() + Example().run() .. tab:: Declarative python style .. code-block:: python + from kivy.core.text import LabelBase + from kivy.metrics import sp + + from kivymd.uix.label import MDLabel + from kivymd.uix.screen import MDScreen from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.widget import MDWidget class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" # "Light" + self.theme_cls.theme_style = "Dark" + + LabelBase.register( + name="nasalization", + fn_regular="/Users/urijivanov/Projects/Dev/MyGithub/Articles/StarTest/data/font/nasalization-rg.ttf", + ) + + self.theme_cls.font_styles["nasalization"] = { + "large": { + "line-height": 1.64, + "font-name": "nasalization", + "font-size": sp(57), + }, + "medium": { + "line-height": 1.52, + "font-name": "nasalization", + "font-size": sp(45), + }, + "small": { + "line-height": 1.44, + "font-name": "nasalization", + "font-size": sp(36), + }, + } return ( - MDBoxLayout( - MDWidget( - md_bg_color=self.theme_cls.bg_light, - ), - MDWidget( - md_bg_color=self.theme_cls.bg_normal, - ), - MDWidget( - md_bg_color=self.theme_cls.bg_dark, - ), - MDWidget( - md_bg_color=self.theme_cls.bg_darkest, - ), + MDScreen( + MDLabel( + text="JetBrainsMono", + halign="center", + font_style="nasalization", + ) ) ) Example().run() - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bg-normal-dark-darkest.png + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-font-styles.png :align: center - :attr:`bg_darkest` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`bg_darkest`, - property is readonly. + :attr:`font_styles` is an :class:`~kivy.properties.DictProperty`. """ - def _get_op_bg_darkest(self) -> list: - return self._get_bg_darkest(True) + schemes_name_colors = ListProperty() - opposite_bg_darkest = AliasProperty( - _get_op_bg_darkest, bind=["theme_style"] - ) - """ - The opposite value of color in the :attr:`bg_darkest`. + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._determine_device_orientation(None, Window.size) + Window.bind(size=self._determine_device_orientation) + self.bind( + theme_style=lambda *x: Clock.schedule_once( + self.update_theme_colors, 0.1 + ), + primary_palette=lambda *x: Clock.schedule_once( + self.set_colors, 0.1 + ), + ) + # Clock.schedule_once(self.sync_theme_styles) + Clock.schedule_once(self.set_colors) - :attr:`opposite_bg_darkest` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`opposite_bg_darkest`, - property is readonly. - """ + def set_colors(self, *args) -> None: + """Calls methods for setting a new color scheme.""" - def _get_bg_dark(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - return get_color_from_hex(self.colors["Light"]["AppBar"]) - elif theme_style == "Dark": - return get_color_from_hex(self.colors["Dark"]["AppBar"]) + if not self.dynamic_color: + if not self.primary_palette: + self._set_default_color() + else: + self._set_palette_color() + else: + path_to_wallpaper = get_wallpaper( + App.get_running_app().user_data_dir, self.path_to_wallpaper + ) + if path_to_wallpaper: + self._set_dynamic_color(path_to_wallpaper) + else: + self._set_palette_color() - bg_dark = AliasProperty(_get_bg_dark, bind=["theme_style"]) - """ - Similar to :attr:`bg_normal`, - but the color values are one tone lower (darker) than :attr:`bg_normal`. + def on_path_to_wallpaper(self, instance, value) -> None: + """Fired when the `path_to_wallpaper` value changes.""" - :attr:`bg_dark` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`bg_dark`, - property is readonly. - """ + if self.dynamic_color: + self.set_colors() - def _get_op_bg_dark(self) -> list: - return self._get_bg_dark(True) + def update_theme_colors(self, *args) -> None: + """Fired when the `theme_style` value changes.""" - opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=["theme_style"]) - """ - The opposite value of color in the :attr:`bg_dark`. + color_theme = self.current_color_theme + style_theme = self.theme_style - :attr:`opposite_bg_dark` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`opposite_bg_dark`, - property is readonly. - """ + if "schemes" in color_theme: + self.schemes_name_colors = list( + color_theme["schemes"][style_theme.lower()].props.keys() + ) + for color_key in self.schemes_name_colors: + color = color_theme["schemes"][style_theme.lower()].props[ + color_key + ] + exec(f"self.{color_key}Color = {rgba(color)}") + else: + self.schemes_name_colors = list( + color_theme[style_theme.lower()].keys() + ) + for color_key in self.schemes_name_colors: + color = color_theme[style_theme.lower()][color_key] + exec(f"self.{color_key}Color = {rgba(color)}") - def _get_bg_normal(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - return get_color_from_hex(self.colors["Light"]["Background"]) - elif theme_style == "Dark": - return get_color_from_hex(self.colors["Dark"]["Background"]) + def switch_theme(self) -> None: + """Switches the theme from light to dark.""" - bg_normal = AliasProperty(_get_bg_normal, bind=["theme_style"]) - """ - Similar to :attr:`bg_light`, - but the color values are one tone lower (darker) than :attr:`bg_light`. + self.theme_style = "Dark" if self.theme_style == "Light" else "Light" - :attr:`bg_normal` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`bg_normal`, - property is readonly. - """ + def sync_theme_styles(self, *args) -> None: + # Syncs the values from self.font_styles to theme_font_styles + # this will ensure continuity when someone registers a new font_style. + for num, style in enumerate(theme_font_styles): + if style not in self.font_styles: + theme_font_styles.pop(num) + for style in self.font_styles.keys(): + theme_font_styles.append(style) - def _get_op_bg_normal(self) -> list: - return self._get_bg_normal(True) + def _set_dynamic_color(self, path_to_wallpaper: str) -> None: + print("_set_dynamic_color") + dynamic_theme = themeFromSourceColor( + getDominantColors(path_to_wallpaper, quality=10, default_chunk=128)[ + 0 + ] + ) + self.current_color_theme = dynamic_theme + self.schemes_name_colors = list( + dynamic_theme["schemes"][self.theme_style.lower()].props.keys() + ) - opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=["theme_style"]) - """ - The opposite value of color in the :attr:`bg_normal`. + for color_key in self.schemes_name_colors: + color = dynamic_theme["schemes"][self.theme_style.lower()].props[ + color_key + ] + exec(f"self.{color_key}Color = {rgba(color)}") + self.disabledTextColor = self._get_disabled_hint_text_color() + + def _set_default_color(self) -> None: + default_theme = getDefaultTheme() + self.current_color_theme = default_theme + self.schemes_name_colors = list( + default_theme[self.theme_style.lower()].keys() + ) - :attr:`opposite_bg_normal` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`opposite_bg_normal`, - property is readonly. - """ + for color_key in self.schemes_name_colors: + color = default_theme[self.theme_style.lower()][color_key] + exec(f"self.{color_key}Color = {rgba(color)}") + self.disabledTextColor = self._get_disabled_hint_text_color() + + def _set_palette_color(self) -> None: + if not self.primary_palette: + self.primary_palette = "Blue" + color_theme = themeFromSourceColor( + [ + argbFromRgb( + *[ + int(c * 255) + for c in get_color_from_hex( + hex_colormap[self.primary_palette.lower()] + ) + ][:3] + ) + ][0] + ) + self.current_color_theme = color_theme + self.schemes_name_colors = list( + color_theme["schemes"][self.theme_style.lower()].props.keys() + ) - def _get_bg_light(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - return get_color_from_hex(self.colors["Light"]["CardsDialogs"]) - elif theme_style == "Dark": - return get_color_from_hex(self.colors["Dark"]["CardsDialogs"]) + for color_key in self.schemes_name_colors: + color = color_theme["schemes"][self.theme_style.lower()].props[ + color_key + ] + exec(f"self.{color_key}Color = {rgba(color)}") + self.disabledTextColor = self._get_disabled_hint_text_color() - bg_light = AliasProperty(_get_bg_light, bind=["theme_style"]) - """" - Depending on the style of the theme (`'Dark'` or `'Light`') - that the application uses, :attr:`bg_light` contains the color value - in ``rgba`` format for the widgets background. - :attr:`bg_light` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`bg_light`, - property is readonly. +class ThemableBehavior(EventDispatcher): + theme_cls = ObjectProperty() """ + Instance of :class:`~ThemeManager` class. - def _get_op_bg_light(self) -> list: - return self._get_bg_light(True) - - opposite_bg_light = AliasProperty(_get_op_bg_light, bind=["theme_style"]) + :attr:`theme_cls` is an :class:`~kivy.properties.ObjectProperty`. """ - The opposite value of color in the :attr:`bg_light`. - :attr:`opposite_bg_light` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`opposite_bg_light`, - property is readonly. + device_ios = BooleanProperty(DEVICE_IOS) """ + ``True`` if device is ``iOS``. - def _get_divider_color(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - color = get_color_from_hex("000000") - elif theme_style == "Dark": - color = get_color_from_hex("FFFFFF") - color[3] = 0.12 - return color - - divider_color = AliasProperty(_get_divider_color, bind=["theme_style"]) + :attr:`device_ios` is an :class:`~kivy.properties.BooleanProperty`. """ - Color for dividing lines such as :class:`~kivymd.uix.card.MDSeparator`. - :attr:`divider_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`divider_color`, - property is readonly. + theme_line_color = OptionProperty("Primary", options=["Primary", "Custom"]) """ + Line color scheme name. - def _get_op_divider_color(self) -> list: - return self._get_divider_color(True) + .. versionadded:: 2.0.0 - opposite_divider_color = AliasProperty( - _get_op_divider_color, bind=["theme_style"] - ) - """ - The opposite value of color in the :attr:`divider_color`. + Available options are: `'Primary'`, `'Custom'`. - :attr:`opposite_divider_color` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`opposite_divider_color`, - property is readonly. + :attr:`theme_line_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def _get_disabled_primary_color(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - lum = sum(self.primary_color[0:3]) / 3.0 - if theme_style == "Light": - a = 0.38 - elif theme_style == "Dark": - a = 0.50 - return [lum, lum, lum, a] - - disabled_primary_color = AliasProperty( - _get_disabled_primary_color, bind=["theme_style"] - ) + theme_bg_color = OptionProperty("Primary", options=["Primary", "Custom"]) """ - The greyscale disabled version of the current application theme color - in ``rgba`` format. + Background color scheme name. - .. versionadded:: 1.0.0 + .. versionadded:: 2.0.0 - :attr:`disabled_primary_color` - is an :class:`~kivy.properties.AliasProperty` that returns the value - in ``rgba`` format for :attr:`disabled_primary_color`, - property is readonly. - """ - - def _get_op_disabled_primary_color(self) -> list: - return self._get_disabled_primary_color(True) + Available options are: `'Primary'`, `'Custom'`. - opposite_disabled_primary_color = AliasProperty( - _get_op_disabled_primary_color, bind=["theme_style"] - ) + :attr:`theme_bg_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - The opposite value of color in the :attr:`disabled_primary_color`. - - .. versionadded:: 1.0.0 - :attr:`opposite_disabled_primary_color` is an - :class:`~kivy.properties.AliasProperty` that returns the value - in ``rgba`` format for :attr:`opposite_disabled_primary_color`, - property is readonly. + theme_shadow_color = OptionProperty( + "Primary", options=["Primary", "Custom"] + ) """ + Elevation color scheme name. - def _get_text_color(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - color = get_color_from_hex("000000") - color[3] = 0.87 - elif theme_style == "Dark": - color = get_color_from_hex("FFFFFF") - return color + .. versionadded:: 2.0.0 - text_color = AliasProperty(_get_text_color, bind=["theme_style"]) - """ - Color of the text used in the :class:`~kivymd.uix.label.MDLabel`. + Available options are: `'Primary'`, `'Custom'`. - :attr:`text_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`text_color`, - property is readonly. + :attr:`theme_shadow_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def _get_op_text_color(self) -> list: - return self._get_text_color(True) - - opposite_text_color = AliasProperty( - _get_op_text_color, bind=["theme_style"] + theme_shadow_offset = OptionProperty( + "Primary", options=["Primary", "Custom"] ) """ - The opposite value of color in the :attr:`text_color`. + Elevation offset scheme name. - :attr:`opposite_text_color` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`opposite_text_color`, - property is readonly. - """ + .. versionadded:: 2.0.0 - def _get_secondary_text_color(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - color = get_color_from_hex("000000") - color[3] = 0.54 - elif theme_style == "Dark": - color = get_color_from_hex("FFFFFF") - color[3] = 0.70 - return color - - secondary_text_color = AliasProperty( - _get_secondary_text_color, bind=["theme_style"] - ) - """ - The color for the secondary text that is used in classes - from the module :class:`~kivymd/uix/list.TwoLineListItem`. + Available options are: `'Primary'`, `'Custom'`. - :attr:`secondary_text_color` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`secondary_text_color`, - property is readonly. + :attr:`theme_shadow_offset` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def _get_op_secondary_text_color(self) -> list: - return self._get_secondary_text_color(True) - - opposite_secondary_text_color = AliasProperty( - _get_op_secondary_text_color, bind=["theme_style"] + theme_elevation_level = OptionProperty( + "Primary", options=["Primary", "Custom"] ) """ - The opposite value of color in the :attr:`secondary_text_color`. + Elevation level scheme name. - :attr:`opposite_secondary_text_color` - is an :class:`~kivy.properties.AliasProperty` that returns the value - in ``rgba`` format for :attr:`opposite_secondary_text_color`, - property is readonly. - """ + .. versionadded:: 2.0.0 - def _get_icon_color(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - color = get_color_from_hex("000000") - color[3] = 0.54 - elif theme_style == "Dark": - color = get_color_from_hex("FFFFFF") - return color + Available options are: `'Primary'`, `'Custom'`. - icon_color = AliasProperty(_get_icon_color, bind=["theme_style"]) + :attr:`theme_elevation_level` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - Color of the icon used in the :class:`~kivymd.uix.button.MDIconButton`. - :attr:`icon_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`icon_color`, - property is readonly. + theme_font_size = OptionProperty("Primary", options=["Primary", "Custom"]) """ + Font size scheme name. - def _get_op_icon_color(self) -> list: - return self._get_icon_color(True) + .. versionadded:: 2.0.0 - opposite_icon_color = AliasProperty( - _get_op_icon_color, bind=["theme_style"] - ) - """ - The opposite value of color in the :attr:`icon_color`. + Available options are: `'Primary'`, `'Custom'`. - :attr:`opposite_icon_color` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`opposite_icon_color`, - property is readonly. + :attr:`theme_font_size` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def _get_disabled_hint_text_color(self, opposite: bool = False) -> list: - theme_style = self._get_theme_style(opposite) - if theme_style == "Light": - color = get_color_from_hex("000000") - color[3] = 0.38 - elif theme_style == "Dark": - color = get_color_from_hex("FFFFFF") - color[3] = 0.50 - return color - - disabled_hint_text_color = AliasProperty( - _get_disabled_hint_text_color, bind=["theme_style"] - ) + theme_width = OptionProperty("Primary", options=["Primary", "Custom"]) """ - Color of the disabled text used in the :class:`~kivymd.uix.textfield.MDTextField`. + Widget width scheme name. - :attr:`disabled_hint_text_color` - is an :class:`~kivy.properties.AliasProperty` that returns the value - in ``rgba`` format for :attr:`disabled_hint_text_color`, - property is readonly. - """ + .. versionadded:: 2.0.0 - def _get_op_disabled_hint_text_color(self) -> list: - return self._get_disabled_hint_text_color(True) + Available options are: `'Primary'`, `'Custom'`. - opposite_disabled_hint_text_color = AliasProperty( - _get_op_disabled_hint_text_color, bind=["theme_style"] - ) + :attr:`theme_width` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - The opposite value of color in the :attr:`disabled_hint_text_color`. - :attr:`opposite_disabled_hint_text_color` - is an :class:`~kivy.properties.AliasProperty` that returns the value - in ``rgba`` format for :attr:`opposite_disabled_hint_text_color`, - property is readonly. + theme_height = OptionProperty("Primary", options=["Primary", "Custom"]) """ + Widget width scheme name. - # Hardcoded because muh standard - def _get_error_color(self) -> list: - return get_color_from_hex(self.colors["Red"]["A700"]) + .. versionadded:: 2.0.0 - error_color = AliasProperty(_get_error_color, bind=["theme_style"]) - """ - Color of the error text used - in the :class:`~kivymd.uix.textfield.MDTextField`. + Available options are: `'Primary'`, `'Custom'`. - :attr:`error_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`error_color`, - property is readonly. + :attr:`theme_height` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def _get_ripple_color(self) -> list: - return self._ripple_color + theme_line_height = OptionProperty("Primary", options=["Primary", "Custom"]) + """ + Line height scheme name. - def _set_ripple_color(self, value) -> None: - self._ripple_color = value + .. versionadded:: 2.0.0 - _ripple_color = ColorProperty(colors["Gray"]["400"]) - """Private value.""" + Available options are: `'Primary'`, `'Custom'`. - ripple_color = AliasProperty( - _get_ripple_color, _set_ripple_color, bind=["_ripple_color"] - ) + :attr:`theme_line_height` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - Color of ripple effects. - :attr:`ripple_color` is an :class:`~kivy.properties.AliasProperty` that - returns the value in ``rgba`` format for :attr:`ripple_color`, - property is readonly. + theme_font_name = OptionProperty("Primary", options=["Primary", "Custom"]) """ + Font name scheme name. - def _determine_device_orientation(self, _, window_size) -> None: - if window_size[0] > window_size[1]: - self.device_orientation = "landscape" - elif window_size[1] >= window_size[0]: - self.device_orientation = "portrait" + .. versionadded:: 2.0.0 - device_orientation = StringProperty("") - """ - Device orientation. + Available options are: `'Primary'`, `'Custom'`. - :attr:`device_orientation` is an :class:`~kivy.properties.StringProperty`. + :attr:`theme_font_name` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def _get_standard_increment(self) -> float: - if DEVICE_TYPE == "mobile": - if self.device_orientation == "landscape": - return dp(48) - else: - return dp(56) - else: - return dp(64) - - standard_increment = AliasProperty( - _get_standard_increment, bind=["device_orientation"] + theme_shadow_softness = OptionProperty( + "Primary", options=["Primary", "Custom"] ) """ - Value of standard increment. + Elevation softness scheme name. - :attr:`standard_increment` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`standard_increment`, - property is readonly. - """ + .. versionadded:: 2.0.0 - def _get_horizontal_margins(self) -> float: - if DEVICE_TYPE == "mobile": - return dp(16) - else: - return dp(24) + Available options are: `'Primary'`, `'Custom'`. - horizontal_margins = AliasProperty(_get_horizontal_margins) + :attr:`theme_shadow_softness` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - Value of horizontal margins. - :attr:`horizontal_margins` is an :class:`~kivy.properties.AliasProperty` - that returns the value in ``rgba`` format for :attr:`horizontal_margins`, - property is readonly. + theme_focus_color = OptionProperty("Primary", options=["Primary", "Custom"]) """ + Focus color scheme name. - def on_theme_style(self, interval: int, theme_style: str) -> None: - if ( - hasattr(App.get_running_app(), "theme_cls") - and App.get_running_app().theme_cls == self - ): - self.set_clearcolor_by_theme_style(theme_style) - - _set_clearcolor = False - - def set_clearcolor_by_theme_style(self, theme_style): - if self.theme_style_switch_animation and self._set_clearcolor: - Animation( - clearcolor=get_color_from_hex( - self.colors[theme_style]["Background"] - ), - d=self.theme_style_switch_animation_duration, - t="linear", - ).start(Window) - else: - Window.clearcolor = get_color_from_hex( - self.colors[theme_style]["Background"] - ) - self._set_clearcolor = True - - # Font name, size (sp), always caps, letter spacing (sp). - font_styles = DictProperty( - { - "H1": ["RobotoLight", 96, False, -1.5], - "H2": ["RobotoLight", 60, False, -0.5], - "H3": ["Roboto", 48, False, 0], - "H4": ["Roboto", 34, False, 0.25], - "H5": ["Roboto", 24, False, 0], - "H6": ["RobotoMedium", 20, False, 0.15], - "Subtitle1": ["Roboto", 16, False, 0.15], - "Subtitle2": ["RobotoMedium", 14, False, 0.1], - "Body1": ["Roboto", 16, False, 0.5], - "Body2": ["Roboto", 14, False, 0.25], - "Button": ["RobotoMedium", 14, True, 1.25], - "Caption": ["Roboto", 12, False, 0.4], - "Overline": ["Roboto", 10, True, 1.5], - "Icon": ["Icons", 24, False, 0], - } - ) - """ - Data of default font styles. - - Add custom font - --------------- - - .. tabs:: - - .. tab:: Declarative style with KV - - .. code-block:: python - - from kivy.core.text import LabelBase - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.font_definitions import theme_font_styles - - KV = ''' - MDScreen: - - MDLabel: - text: "JetBrainsMono" - halign: "center" - font_style: "JetBrainsMono" - ''' - - - class MainApp(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - - LabelBase.register( - name="JetBrainsMono", - fn_regular="JetBrainsMono-Regular.ttf") - - theme_font_styles.append('JetBrainsMono') - self.theme_cls.font_styles["JetBrainsMono"] = [ - "JetBrainsMono", - 16, - False, - 0.15, - ] - return Builder.load_string(KV) - + .. versionadded:: 2.0.0 - MainApp().run() - - .. tab:: Declarative python style - - .. code-block:: python + Available options are: `'Primary'`, `'Custom'`. - from kivy.core.text import LabelBase - - from kivymd.app import MDApp - from kivymd.uix.screen import MDScreen - from kivymd.uix.label import MDLabel - from kivymd.font_definitions import theme_font_styles - - - class MainApp(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - - LabelBase.register( - name="JetBrainsMono", - fn_regular="JetBrainsMono-Regular.ttf") - - theme_font_styles.append('JetBrainsMono') - self.theme_cls.font_styles["JetBrainsMono"] = [ - "JetBrainsMono", - 16, - False, - 0.15, - ] - return ( - MDScreen( - MDLabel( - text="JetBrainsMono", - halign="center", - font_style="JetBrainsMono", - ) - ) - ) - - - MainApp().run() - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles.png - :align: center - - :attr:`font_styles` is an :class:`~kivy.properties.DictProperty`. + :attr:`theme_focus_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - def set_colors( - self, - primary_palette: str, - primary_hue: str, - primary_light_hue: str, - primary_dark_hue: str, - accent_palette: str, - accent_hue: str, - accent_light_hue: str, - accent_dark_hue: str, - ) -> None: - """ - Courtesy method to allow all of the theme color attributes to be set in one call. - - :attr:`set_colors` allows all of the following to be set in one method call: - - * primary palette color, - * primary hue, - * primary light hue, - * primary dark hue, - * accent palette color, - * accent hue, - * accent ligth hue, and - * accent dark hue. - - Note that all values *must* be provided. If you only want to set one or two values - use the appropriate method call for that. - - .. tabs:: - - .. tab:: Imperative python style - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.screen import MDScreen - from kivymd.uix.button import MDRectangleFlatButton - - class MainApp(MDApp): - def build(self): - self.theme_cls.set_colors( - "Blue", "600", "50", "800", "Teal", "600", "100", "800" - ) - screen = MDScreen() - screen.add_widget( - MDRectangleFlatButton( - text="Hello, World", - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - return screen - - - MainApp().run() - - .. tab:: Declarative python style - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.screen import MDScreen - from kivymd.uix.button import MDRectangleFlatButton - - class MainApp(MDApp): - def build(self): - self.theme_cls.set_colors( - "Blue", "600", "50", "800", "Teal", "600", "100", "800" - ) - return ( - MDScreen( - MDRectangleFlatButton( - text="Hello, World", - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - ) - - - MainApp().run() - """ - - self.primary_palette = primary_palette - self.primary_hue = primary_hue - self.primary_light_hue = primary_light_hue - self.primary_dark_hue = primary_dark_hue - self.accent_palette = accent_palette - self.accent_hue = accent_hue - self.accent_light_hue = accent_light_hue - self.accent_dark_hue = accent_dark_hue - - def __init__(self, **kwargs): - super().__init__(**kwargs) - Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style)) - self._determine_device_orientation(None, Window.size) - Window.bind(size=self._determine_device_orientation) - self.bind(font_styles=self.sync_theme_styles) - self.colors = colors - Clock.schedule_once(self.sync_theme_styles) - - def sync_theme_styles(self, *args) -> None: - # Syncs the values from self.font_styles to theme_font_styles - # this will ensure continuity when someone registers a new font_style. - for num, style in enumerate(theme_font_styles): - if style not in self.font_styles: - theme_font_styles.pop(num) - for style in self.font_styles.keys(): - theme_font_styles.append(style) - - -class ThemableBehavior(EventDispatcher): - theme_cls = ObjectProperty() + theme_divider_color = OptionProperty( + "Primary", options=["Primary", "Custom"] + ) """ - Instance of :class:`~ThemeManager` class. + Divider color scheme name. - :attr:`theme_cls` is an :class:`~kivy.properties.ObjectProperty`. - """ + .. versionadded:: 2.0.0 - device_ios = BooleanProperty(DEVICE_IOS) - """ - ``True`` if device is ``iOS``. + Available options are: `'Primary'`, `'Custom'`. - :attr:`device_ios` is an :class:`~kivy.properties.BooleanProperty`. + :attr:`theme_divider_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - widget_style = OptionProperty( - "android", options=["android", "ios", "desktop"] + theme_text_color = OptionProperty( + "Primary", + options=[ + "Primary", + "Secondary", + "Hint", + "Error", + "Custom", + ], ) """ - Allows to set one of the three style properties for the widget: - `'android'`, `'ios'`, `'desktop'`. - - For example, for the class :class:`~kivymd.uix.selectioncontrol.MDSwitch` - has two styles - `'android'` and `'ios'`: - - .. code-block:: kv - - MDSwitch: - widget_style: "ios" - - .. code-block:: kv - - MDSwitch: - widget_style: "android" + Label color scheme name. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switch-android-ios.png - :align: center + Available options are: `'Primary'`, `'Secondary'`, `'Hint'`, `'Error'`, + `'Custom'`. - :attr:`widget_style` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'android'`. + :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ - opposite_colors = BooleanProperty(False) + theme_icon_color = OptionProperty( + "Primary", + options=[ + "Primary", + "Secondary", + "Hint", + "Error", + "Custom", + ], + ) """ - For some widgets, for example, for a widget - :class:`~kivymd.uix.toolbar.MDTopAppBar` changes the color of the label to - the color opposite to the main theme. - - .. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - opposite_colors: True - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-true.png - :align: center + Label color scheme name. - .. code-block:: kv + Available options are: `'Primary'`, `'Secondary'`, `'Hint'`, `'Error'`, + `'Custom'`. - MDTopAppBar: - title: "MDTopAppBar" - opposite_colors: True - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-false.png - :align: center + :attr:`theme_icon_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. """ def __init__(self, **kwargs): - self.unbind_properties = [ - "theme_style", - "material_style", - "device_orientation", - "primary_color", - "primary_palette", - "accent_palette", - "text_color", - ] - - if self.theme_cls is not None: - pass - else: + if self.theme_cls is None: try: if not isinstance( App.get_running_app().property("theme_cls", True), @@ -1690,15 +926,7 @@ def remove_widget(self, widget) -> None: if hasattr(callback, "proxy") and hasattr( callback.proxy, "theme_cls" ): - if issubclass(widget.__class__, self.md_textfield): - widget.theme_cls.unbind( - **{ - "theme_style": getattr( - callback.proxy, callback.method_name - ) - } - ) - for property_name in self.unbind_properties: + for property_name in ["theme_style", "primary_palette"]: if widget == callback.proxy: widget.theme_cls.unbind( **{ @@ -1707,10 +935,6 @@ def remove_widget(self, widget) -> None: ) } ) - # KivyMD widgets may contain other MD widgets. - for children in widget.children: - if hasattr(children, "theme_cls"): - self.remove_widget(children) except ReferenceError: pass diff --git a/kivymd/theming_dynamic_text.py b/kivymd/theming_dynamic_text.py deleted file mode 100755 index 64f992195..000000000 --- a/kivymd/theming_dynamic_text.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Theming Dynamic Text -==================== - -Two implementations. The first is based on color brightness obtained from- -https://www.w3.org/TR/AERT#color-contrast -The second is based on relative luminance calculation for sRGB obtained from- -https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef -and contrast ratio calculation obtained from- -https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef - -Preliminary testing suggests color brightness more closely matches the -`Material Design spec` suggested text colors, but the alternative implementation -is both newer and the current 'correct' recommendation, so is included here -as an option. -""" - - -def _color_brightness(color): - # Implementation of color brightness method - brightness = color[0] * 299 + color[1] * 587 + color[2] * 114 - brightness = brightness - return brightness - - -def _black_or_white_by_color_brightness(color): - if _color_brightness(color) >= 500: - return "black" - else: - return "white" - - -def _normalized_channel(color): - # Implementation of contrast ratio and relative luminance method - if color <= 0.03928: - return color / 12.92 - else: - return ((color + 0.055) / 1.055) ** 2.4 - - -def _luminance(color): - rg = _normalized_channel(color[0]) - gg = _normalized_channel(color[1]) - bg = _normalized_channel(color[2]) - return 0.2126 * rg + 0.7152 * gg + 0.0722 * bg - - -def _black_or_white_by_contrast_ratio(color): - l_color = _luminance(color) - l_black = 0.0 - l_white = 1.0 - b_contrast = (l_color + 0.05) / (l_black + 0.05) - w_contrast = (l_white + 0.05) / (l_color + 0.05) - return "white" if w_contrast >= b_contrast else "black" - - -def get_contrast_text_color(color, use_color_brightness=True): - if use_color_brightness: - contrast_color = _black_or_white_by_color_brightness(color) - else: - contrast_color = _black_or_white_by_contrast_ratio(color) - if contrast_color == "white": - return 1, 1, 1, 1 - else: - return 0, 0, 0, 1 - - -if __name__ == "__main__": - from kivy.utils import get_color_from_hex - - from kivymd.color_definitions import colors, text_colors - - for c in colors.items(): - if c[0] in ["Light", "Dark"]: - continue - color = c[0] - print(f"For the {color} color palette:") - for name, hex_color in c[1].items(): - if hex_color: - col = get_color_from_hex(hex_color) - col_bri = get_contrast_text_color(col) - con_rat = get_contrast_text_color( - col, use_color_brightness=False - ) - text_color = text_colors[c[0]][name] - print( - f" The {name} hue gives {col_bri} using color " - f"brightness, {con_rat} using contrast ratio, and " - f"{text_color} from the MD spec" - ) diff --git a/kivymd/toast/androidtoast/androidtoast.py b/kivymd/toast/androidtoast/androidtoast.py index 18af63e18..c52b74084 100644 --- a/kivymd/toast/androidtoast/androidtoast.py +++ b/kivymd/toast/androidtoast/androidtoast.py @@ -63,7 +63,7 @@ def toast(text, length_long=False, gravity=0, y=0, x=0): :param length_long: the amount of time (in seconds) that the toast is visible on the screen; :param text: text to be displayed in the toast; - :param short_duration: duration of the toast, if `True` the toast + :param length_long: duration of the toast, if `True` the toast will last 2.3s but if it is `False` the toast will last 3.9s; :param gravity: refers to the toast position, if it is 80 the toast will be shown below, if it is 40 the toast will be displayed above; diff --git a/kivymd/uix/__init__.py b/kivymd/uix/__init__.py index 9aec2dc15..bb7066318 100755 --- a/kivymd/uix/__init__.py +++ b/kivymd/uix/__init__.py @@ -5,10 +5,8 @@ from kivy.uix.label import Label from kivy.uix.screenmanager import Screen -from kivymd.uix.behaviors import SpecificBackgroundColorBehavior - -class MDAdaptiveWidget(SpecificBackgroundColorBehavior): +class MDAdaptiveWidget: adaptive_height = BooleanProperty(False) """ If `True`, the following properties will be applied to the widget: diff --git a/kivymd/uix/anchorlayout.py b/kivymd/uix/anchorlayout.py index fe435423f..381dc3a32 100644 --- a/kivymd/uix/anchorlayout.py +++ b/kivymd/uix/anchorlayout.py @@ -15,7 +15,7 @@ AnchorLayout: canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -26,7 +26,7 @@ .. code-block:: kv MDAnchorLayout: - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor """ __all__ = ("MDAnchorLayout",) @@ -35,13 +35,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDAnchorLayout( - DeclarativeBehavior, ThemableBehavior, AnchorLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + AnchorLayout, + MDAdaptiveWidget, ): """ - Anchor layout class. For more information, see in the - :class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation. + Anchor layout class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.anchorlayout.AnchorLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/appbar/__init__.py b/kivymd/uix/appbar/__init__.py new file mode 100644 index 000000000..1a5c0fa18 --- /dev/null +++ b/kivymd/uix/appbar/__init__.py @@ -0,0 +1,12 @@ +# NOQA F401 +from .appbar import ( + MDActionTopAppBarButton, + MDActionBottomAppBarButton, + MDBottomAppBar, + MDFabBottomAppBarButton, + MDTopAppBar, + MDTopAppBarLeadingButtonContainer, + MDActionTopAppBarButton, + MDTopAppBarTitle, + MDTopAppBarTrailingButtonContainer, +) diff --git a/kivymd/uix/appbar/appbar.kv b/kivymd/uix/appbar/appbar.kv new file mode 100644 index 000000000..3600c949c --- /dev/null +++ b/kivymd/uix/appbar/appbar.kv @@ -0,0 +1,102 @@ + + size_hint_x: None + width: self.minimum_width + padding: "8dp", 0, "16dp", 0 + + + + size_hint_x: None + width: self.minimum_width + padding: "16dp", 0, "16dp", 0 + spacing: "4dp" + + + + pos_hint: {"center_y": .5} + color: + self.theme_cls.onSurfaceVariantColor \ + if self.theme_icon_color == "Primary" else \ + self.icon_color + + + + font_style: + { \ + "small": "Title", \ + "medium": "Headline", \ + "large": "Headline", \ + }[self._appbar.type if self._appbar else "small"] + role: + { \ + "small": "large", \ + "medium": "small", \ + "large": "medium", \ + }[self._appbar.type if self._appbar else "large"] + adaptive_width: + ( \ + True \ + if self._appbar.type == "small" else \ + False \ + ) \ + if self._appbar else True + size_hint_x: + ( \ + None \ + if self._appbar.type == "small" else \ + 1 \ + ) \ + if self._appbar else None + + + + canvas: + Color: + rgba: + self.theme_cls.surfaceColor \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color + Rectangle: + pos: self.pos + size: self.size + + orientation: + "vertical" \ + if self.type in ("medium", "large") else \ + "horizontal" + size_hint_y: None + height: + { \ + "small": "64dp", \ + "medium": "112dp", \ + "large": "152dp", \ + }[self.type] + + BoxLayout: + id: root_box + + BoxLayout: + id: title_box + padding: "16dp", 0, "16dp", 0 + + + + elevation_level: 0 + theme_shadow_color: "Custom" + shadow_color: self.theme_cls.transparentColor + + + + size_hint_y: None + height: "80dp" + elevation_level: + 2 \ + if self.theme_elevation_level == "Primary" else \ + self.elevation_level + shadow_softness: + 2 \ + if self.theme_shadow_softness == "Primary" else \ + self.shadow_softness + md_bg_color: + self.theme_cls.surfaceContainerColor \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color diff --git a/kivymd/uix/appbar/appbar.py b/kivymd/uix/appbar/appbar.py new file mode 100755 index 000000000..a4665e47e --- /dev/null +++ b/kivymd/uix/appbar/appbar.py @@ -0,0 +1,1244 @@ +""" +Components/Appbar +================= + +.. seealso:: + + `Material Design spec, App bars: top `_ + + `Material Design spec, App bars: bottom `_ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-top.png + :align: center + +`KivyMD` provides the following bar positions for use: + +- TopAppBar_ +- BottomAppBar_ + +.. TopAppBar_: +TopAppBar +--------- + +- Contains a title and actions related to the current screen +- Four types: center-aligned, small, medium, and large +- On scroll, apply a container fill color to separate app bar from body content +- Top app bars have the same width as the device window + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/top-appbar-available-types.png + :align: center + +1. Center-aligned +2. Small +3. Medium +4. Large + +.. note:: KivyMD does not provide a `Center-aligned` type panel. But you can + easily create this pit panel yourself (read the documentation below). + +Usage +----- + +.. code-block:: kv + + MDTopAppBar: + type: "small" + + MDTopAppBarLeadingButtonContainer: + + MDActionTopAppBarButton: + icon: "menu" + + MDTopAppBarTitle: + text: "AppBar Center-aligned" + pos_hint: {"center_x": .5} + + MDTopAppBarTrailingButtonContainer: + + MDActionTopAppBarButton: + icon: "account-circle-outline" + +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/top-appbar-anatomy.png + :align: center + +Configurations +============== + +1. Center-aligned +----------------- + +.. code-block:: kv + + MDScreen: + md_bg_color: self.theme_cls.secondaryContainerColor + + MDTopAppBar: + type: "small" + size_hint_x: .8 + pos_hint: {"center_x": .5, "center_y": .5} + + MDTopAppBarLeadingButtonContainer: + + MDActionTopAppBarButton: + icon: "menu" + + MDTopAppBarTitle: + text: "AppBar small" + pos_hint: {"center_x": .5} + + MDTopAppBarTrailingButtonContainer: + + MDActionTopAppBarButton: + icon: "account-circle-outline" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/top-appbar-center-aligned.png + :align: center + +2. Small +-------- + +.. code-block:: kv + + MDScreen: + md_bg_color: self.theme_cls.secondaryContainerColor + + MDTopAppBar: + type: "small" + size_hint_x: .8 + pos_hint: {"center_x": .5, "center_y": .5} + + MDTopAppBarLeadingButtonContainer: + + MDActionTopAppBarButton: + icon: "arrow-left" + + MDTopAppBarTitle: + text: "AppBar small" + + MDTopAppBarTrailingButtonContainer: + + MDActionTopAppBarButton: + icon: "attachment" + + MDActionTopAppBarButton: + icon: "calendar" + + MDActionTopAppBarButton: + icon: "dots-vertical" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/top-appbar-small.png + :align: center + +3. Medium +--------- + +.. code-block:: kv + + MDTopAppBar: + type: "medium" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/top-appbar-medium.png + :align: center + +4. Large +-------- + +.. code-block:: kv + + MDTopAppBar: + type: "large" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/top-appbar-large.png + :align: center + +.. BottomAppBar: +BottomAppBar +------------ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom-m3.png + :align: center + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDBottomAppBar: + + MDFabBottomAppBarButton: + icon: "plus" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-1.png + :align: center + +Add action items +---------------- + +.. code-block:: kv + + #:import MDActionBottomAppBarButton kivymd.uix.appbar.MDActionBottomAppBarButton + + + MDScreen: + + MDBottomAppBar: + action_items: + [ + MDActionBottomAppBarButton(icon="gmail"), + MDActionBottomAppBarButton(icon="label-outline"), + MDActionBottomAppBarButton(icon="bookmark"), + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-2.png + :align: center + +Change action items +------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.appbar import MDActionBottomAppBarButton + + KV = ''' + #:import MDActionBottomAppBarButton kivymd.uix.appbar.MDActionBottomAppBarButton + + + MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDBottomAppBar: + id: bottom_appbar + action_items: + [ + MDActionBottomAppBarButton(icon="gmail"), + MDActionBottomAppBarButton(icon="bookmark"), + ] + + MDFabBottomAppBarButton: + icon: "plus" + on_release: app.change_actions_items() + ''' + + + class Example(MDApp): + def change_actions_items(self): + self.root.ids.bottom_appbar.action_items = [ + MDActionBottomAppBarButton(icon="magnify"), + MDActionBottomAppBarButton(icon="trash-can-outline"), + MDActionBottomAppBarButton(icon="download-box-outline"), + ] + + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-3.gif + :align: center + +A practical example +------------------- + +.. code-block:: python + + from kivy.clock import Clock + from kivy.lang import Builder + from kivy.properties import StringProperty, BooleanProperty, ObjectProperty + from kivy.uix.behaviors import FocusBehavior + from kivy.uix.recycleboxlayout import RecycleBoxLayout + from kivy.uix.recycleview.layout import LayoutSelectionBehavior + from kivy.uix.recycleview.views import RecycleDataViewBehavior + + from kivymd.uix.appbar import MDActionBottomAppBarButton + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.app import MDApp + from kivymd.utils import asynckivy + + from faker import Faker # pip install Faker + + KV = ''' + #:import MDFabBottomAppBarButton kivymd.uix.appbar.MDFabBottomAppBarButton + + + + orientation: "vertical" + adaptive_height: True + md_bg_color: "#373A22" if self.selected else "#1F1E15" + radius: 16 + padding: 0, 0, 0, "16dp" + + MDListItem: + theme_bg_color: "Custom" + md_bg_color: root.md_bg_color + radius: root.radius + ripple_effect: False + + MDListItemLeadingAvatar: + source: root.avatar + # radius: self.height / 2 + + MDListItemHeadlineText: + text: root.name + theme_text_color: "Custom" + text_color: "#8A8D79" + + MDListItemSupportingText: + text: root.time + theme_text_color: "Custom" + text_color: "#8A8D79" + + MDLabel: + text: root.text + adaptive_height: True + theme_text_color: "Custom" + text_color: "#8A8D79" + padding_x: "16dp" + shorten: True + shorten_from: "right" + + Widget: + + + MDFloatLayout: + md_bg_color: "#151511" + + RecycleView: + id: card_list + viewclass: "UserCard" + + SelectableRecycleGridLayout: + orientation: 'vertical' + spacing: "16dp" + padding: "16dp" + default_size: None, dp(120) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + multiselect: True + touch_multiselect: True + + MDBottomAppBar: + id: bottom_appbar + scroll_cls: card_list + allow_hidden: True + theme_bg_color: "Custom" + md_bg_color: "#232217" + + MDFabBottomAppBarButton: + id: fab_button + icon: "plus" + theme_bg_color: "Custom" + md_bg_color: "#373A22" + theme_icon_color: "Custom" + icon_color: "#ffffff" + ''' + + + class UserCard(RecycleDataViewBehavior, MDBoxLayout): + name = StringProperty() + time = StringProperty() + text = StringProperty() + avatar = StringProperty() + callback = ObjectProperty(lambda x: x) + + index = None + selected = BooleanProperty(False) + selectable = BooleanProperty(True) + + def refresh_view_attrs(self, rv, index, data): + self.index = index + return super().refresh_view_attrs(rv, index, data) + + def on_touch_down(self, touch): + if super().on_touch_down(touch): + return True + if self.collide_point(*touch.pos) and self.selectable: + Clock.schedule_once(self.callback) + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, rv, index, is_selected): + self.selected = is_selected + rv.data[index]["selected"] = is_selected + + + class SelectableRecycleGridLayout( + FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout + ): + pass + + + class BottomAppBarButton(MDActionBottomAppBarButton): + theme_icon_color = "Custom" + icon_color = "#8A8D79" + + + class Test(MDApp): + selected_cards = False + + def build(self): + return Builder.load_string(KV) + + def on_tap_card(self, *args): + datas = [data["selected"] for data in self.root.ids.card_list.data] + if True in datas and not self.selected_cards: + self.root.ids.bottom_appbar.action_items = [ + BottomAppBarButton(icon="gmail"), + BottomAppBarButton(icon="label-outline"), + BottomAppBarButton(icon="bookmark"), + ] + self.root.ids.fab_button.icon = "pencil" + self.selected_cards = True + else: + if len(list(set(datas))) == 1 and not list(set(datas))[0]: + self.selected_cards = False + if not self.selected_cards: + self.root.ids.bottom_appbar.action_items = [ + BottomAppBarButton(icon="magnify"), + BottomAppBarButton(icon="trash-can-outline"), + BottomAppBarButton(icon="download-box-outline"), + ] + self.root.ids.fab_button.icon = "plus" + + def on_start(self): + async def generate_card(): + for i in range(10): + await asynckivy.sleep(0) + self.root.ids.card_list.data.append( + { + "name": fake.name(), + "time": fake.date(), + "avatar": fake.image_url(), + "text": fake.text(), + "selected": False, + "callback": self.on_tap_card, + } + ) + + self.on_tap_card() + fake = Faker() + Clock.schedule_once(lambda x: asynckivy.start(generate_card())) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-4.gif + :align: center + +API break +========= + +1.2.0 version +------------- + +.. code-block:: kv + + MDTopAppBar: + type_height: "large" + headline_text: "Headline" + left_action_items: [["arrow-left", lambda x: x]] + right_action_items: + [ \ + ["attachment", lambda x: x], \ + ["calendar", lambda x: x], \ + ["dots-vertical", lambda x: x], \ + ] + anchor_title: "left" + +2.0.0 version +------------- + +.. code-block:: kv + + MDTopAppBar: + type: "large" + + MDTopAppBarLeadingButtonContainer: + + MDActionTopAppBarButton: + icon: "arrow-left" + + MDTopAppBarTitle: + text: "AppBar small" + + MDTopAppBarTrailingButtonContainer: + + MDActionTopAppBarButton: + icon: "attachment" + + MDActionTopAppBarButton: + icon: "calendar" + + MDActionTopAppBarButton: + icon: "dots-vertical" +""" + +from __future__ import annotations + +__all__ = ( + "MDTopAppBar", + "MDTopAppBarTitle", + "MDBottomAppBar", + "MDActionTopAppBarButton", + "MDActionBottomAppBarButton", + "MDFabBottomAppBarButton", + "MDTopAppBarLeadingButtonContainer", + "MDTopAppBarTrailingButtonContainer", +) + +import os + +from kivy import Logger +from kivy.animation import Animation +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 ( + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + ColorProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.scrollview import ScrollView +from kivy.uix.widget import Widget + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CommonElevationBehavior, + DeclarativeBehavior, + RotateBehavior, + ScaleBehavior, + BackgroundColorBehavior, +) +from kivymd.uix.button import MDFabButton, MDIconButton +from kivymd.uix.controllers import WindowController +from kivymd.uix.label import MDLabel + +from kivymd.utils import asynckivy +from kivymd.utils.set_bars_colors import set_bars_colors + +with open( + os.path.join(uix_path, "appbar", "appbar.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class BaseTopAppBarButtonContainer(DeclarativeBehavior, BoxLayout): + # kivymd.uix.appbar.appbar.MDTopAppBar object. + _appbar = ObjectProperty() + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDActionTopAppBarButton): + Clock.schedule_once(lambda x: self._check_icon_color(widget)) + + return super().add_widget(widget) + + def _check_icon_color(self, widget): + if widget.theme_icon_color == "Primary" and widget.icon_color == None: + widget.theme_icon_color = "Custom" + widget.icon_color = widget.theme_cls.onSurfaceColor + + +class MDFabBottomAppBarButton(MDFabButton, RotateBehavior, ScaleBehavior): + """ + Implements a floating action button (FAB) for a bar with type 'bottom'. + + For more information, see in the + :class:`~kivymd.uix.button.button.MDFabButton` and + :class:`~kivymd.uix.behaviors.rotate_behavior.RotateBehavior` and + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and + classes documentation. + """ + + +class MDActionTopAppBarButton(MDIconButton): + """ + Implements action buttons on the bar. + + For more information, see in the + :class:`~kivymd.uix.button.button.MDIconButton` class documentation. + """ + + md_bg_color_disabled = ColorProperty(None) + """ + The background color in (r, g, b, a) or string format of the button when + the button is disabled. + + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + +class MDActionBottomAppBarButton(MDActionTopAppBarButton): + """ + Implements action buttons for a + :class:'~kivymd.uix.appbar.appbar.MDBottomAppBar' class. + + .. versionadded:: 1.2.0 + + For more information, see in the + :class:`~kivymd.uix.appbar.appbar.MDActionTopAppBarButton` + class documentation. + """ + + +class MDTopAppBarTitle(MDLabel): + """ + Implements the panel title. + + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ + + _appbar = ObjectProperty() + _title_width = NumericProperty(0) + + def on_text(self, instance, value) -> None: + """Fired when the :attr:`text` value changes.""" + + def set_title_width(*args) -> None: + self._title_width = self.texture_size[0] + + Clock.schedule_once(set_title_width) + + def on_pos_hint(self, instance, value) -> None: + """Fired when the :attr:`pos_hint` value changes.""" + + if self._appbar: + Clock.schedule_once( + lambda x: self._appbar._set_padding_title(value) + ) + + +class MDTopAppBarLeadingButtonContainer(BaseTopAppBarButtonContainer): + """ + Implements a container for the leading action buttons. + + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. + """ + + +class MDTopAppBarTrailingButtonContainer(BaseTopAppBarButtonContainer): + """ + Implements a container for the trailing action buttons. + + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. + """ + + +class MDTopAppBar( + DeclarativeBehavior, + ThemableBehavior, + CommonElevationBehavior, + BackgroundColorBehavior, + BoxLayout, + WindowController, +): + """ + Top app bar class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` and + :class:`~kivymd.uix.controllers.windowcontroller.WindowController` + classes documentation. + + :Events: + `on_action_button` + Method for the button used for the :class:`~MDBottomAppBar` class. + """ + + set_bars_color = BooleanProperty(False) + """ + If `True` the background color of the bar status will be set automatically + according to the current color of the bar. + + .. versionadded:: 1.0.0 + + See `set_bars_colors `_ + for more information. + + :attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + type = OptionProperty("small", options=["medium", "large", "small"]) + """ + Bar height type. + + .. versionadded:: 1.0.0 + + Available options are: 'medium', 'large', 'small'. + + :attr:`type_height` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'small'`. + """ + + _trailing_button_container = ObjectProperty() + _leading_button_container = ObjectProperty() + _appbar_title = ObjectProperty() + + def on_type(self, instance, value) -> None: + def on_type(*args): + if value in ("medium", "large"): + self.ids.root_box.add_widget(Widget(), index=1) + + Clock.schedule_once(on_type, 0.5) + + def on_size(self, instance, size) -> None: + """Fired when the application screen size changes.""" + + if self._appbar_title: + if not self._appbar_title._title_width: + self._appbar_title._title_width = ( + self._appbar_title.texture_size[0] + ) + Clock.schedule_once( + lambda x: self._appbar_title.on_pos_hint( + self._appbar_title, self._appbar_title.pos_hint + ) + ) + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDTopAppBarTitle): + widget._appbar = self + self._appbar_title = widget + Clock.schedule_once(lambda x: self._add_title(widget)) + elif isinstance(widget, MDTopAppBarTrailingButtonContainer): + self._trailing_button_container = widget + widget._appbar = self + Clock.schedule_once(lambda x: self.ids.root_box.add_widget(widget)) + elif isinstance(widget, MDTopAppBarLeadingButtonContainer): + widget._appbar = self + self._leading_button_container = widget + Clock.schedule_once(lambda x: self.ids.root_box.add_widget(widget)) + else: + return super().add_widget(widget) + + def _add_title(self, widget): + if self.type == "small": + self.ids.root_box.add_widget(widget) + else: + self.ids.title_box.add_widget(widget) + + def _set_padding_title(self, value): + if value.get("center_x", 0) == 0.5 and self.type == "small": + if ( + not self._trailing_button_container + and self._leading_button_container + ): + left_padding = (self.width // 2) - ( + self._leading_button_container.width + + (self._appbar_title._title_width // 2) + ) + self._appbar_title.padding = [left_padding, 0, 0, 0] + elif ( + self._trailing_button_container + and not self._leading_button_container + ): + left_padding = (self.width // 2) - ( + self._appbar_title._title_width // 2 + ) + right_padding = (self.width // 2) - ( + self._trailing_button_container.width + + (self._appbar_title._title_width // 2) + ) + self._appbar_title.padding = [left_padding, 0, right_padding, 0] + elif ( + not self._trailing_button_container + and not self._leading_button_container + ): + left_padding = (self.width // 2) - ( + self._appbar_title._title_width // 2 + ) + right_padding = (self.width // 2) - ( + self._appbar_title._title_width // 2 + ) + self._appbar_title.padding = [left_padding, 0, right_padding, 0] + elif ( + self._trailing_button_container + and self._leading_button_container + ): + left_padding = (self.width // 2) - ( + self._leading_button_container.width + + (self._appbar_title._title_width // 2) + ) + right_padding = (self.width // 2) - ( + self._trailing_button_container.width + + (self._appbar_title._title_width // 2) + ) + self._appbar_title.padding = [left_padding, 0, right_padding, 0] + elif ( + not value + and self._trailing_button_container + and self._leading_button_container + ): + if self.type == "small": + right_padding = self.width - ( + self._trailing_button_container.width + + self._leading_button_container.width + + self._appbar_title._title_width + ) + self._appbar_title.padding = [0, 0, right_padding, 0] + elif ( + not value + and self._trailing_button_container + and not self._leading_button_container + ): + if self.type == "small": + right_padding = self.width - ( + self._trailing_button_container.width + + self._appbar_title._title_width + ) + self._appbar_title.padding = [ + dp(16), + 0, + right_padding - dp(16), + 0, + ] + elif ( + not value + and not self._trailing_button_container + and not self._leading_button_container + ): + self._appbar_title.padding_x = dp(16) + + +class MDBottomAppBar( + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + CommonElevationBehavior, + FloatLayout, +): + """ + Bottom app bar class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` + classes documentation. + + :Events: + `on_show_bar` + The method is fired when the :class:`~MDBottomAppBar` panel + is shown. + `on_hide_bar` + The method is fired when the :class:`~MDBottomAppBar` panel + is hidden. + """ + + action_items = ListProperty() + """ + The icons on the left bar. + + .. versionadded:: 1.2.0 + + :attr:`action_items` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + animation = BooleanProperty(True) + """ + # TODO: add description. + # FIXME: changing the value does not affect anything. + + .. versionadded:: 1.2.0 + + :attr:`animation` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + show_transition = StringProperty("linear") + """ + Type of button display transition. + + .. versionadded:: 1.2.0 + + :attr:`show_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'linear'`. + """ + + hide_transition = StringProperty("in_back") + """ + Type of button hidden transition. + + .. versionadded:: 1.2.0 + + :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'in_back'`. + """ + + hide_duration = NumericProperty(0.4) + """ + Duration of button hidden transition. + + .. versionadded:: 1.2.0 + + :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + show_duration = NumericProperty(0.2) + """ + Duration of button display transition. + + .. versionadded:: 1.2.0 + + :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + scroll_cls = ObjectProperty() + """ + Widget inherited from the :class:`~kivy.uix.scrollview.ScrollView` class. + The value must be set if the :attr:`allow_hidden` parameter is `True`. + + .. versionadded:: 1.2.0 + + :attr:`scroll_cls` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + allow_hidden = BooleanProperty(False) + """ + Allows or disables hiding the panel when scrolling content. + If the value is `True`, the :attr:`scroll_cls` parameter must be specified. + + .. versionadded:: 1.2.0 + + :attr:`allow_hidden` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + bar_is_hidden = BooleanProperty(False) + """ + Is the panel currently hidden. + + .. versionadded:: 1.2.0 + + :attr:`bar_is_hidden` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + _padding = dp(16) + _x = -dp(48) + _scroll_cls_y = 0 + _cache = [] + _current_data = [] + _wait_removed = False + _animated_hidden = True + _animated_show = True + _fab_bottom_app_bar_button = None + _action_overflow_button = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_event_type("on_show_bar") + self.register_event_type("on_hide_bar") + + def button_centering_animation( + self, + button: MDActionBottomAppBarButton | MDFabBottomAppBarButton, + ) -> None: + """ + Animation of centering buttons for + :class:`~MDActionOverFlowButton`, + :class:`~MDActionBottomAppBarButton` and + :class:`~MDFabBottomAppBarButton` classes. + """ + + if self.animation: + Animation( + y=self.height / 2 - dp(48) / 2, + opacity=1, + d=self.show_duration, + t=self.show_transition, + ).start(button) + + def check_scroll_direction(self, scroll_cls, y: float) -> None: + """ + Checks the scrolling direction. + Depending on the scrolling direction, hides or shows the + :class:`~MDBottomAppBar` panel. + """ + + if round(y, 1) < self._scroll_cls_y and not self.bar_is_hidden: + self.hide_bar() + if round(y, 1) > self._scroll_cls_y and self.bar_is_hidden: + self.show_bar() + + self._scroll_cls_y = round(y, 1) + + def show_bar(self) -> None: + """Show :class:`~MDBottomAppBar` panel.""" + + def on_complete(*args): + self.dispatch("on_show_bar") + + def on_progress(animation, instance, progress): + if progress > 0.5 and self._animated_show: + self._animated_show = False + for i, widget in enumerate(self.children): + if isinstance(widget, MDActionBottomAppBarButton): + anim_icon = Animation( + y=self.height / 2 - dp(48) / 2, + d=self.show_duration, + t=self.show_transition, + ) + Clock.schedule_once( + lambda x, y=widget: anim_icon.start(y), + i / 10, + ) + if self._fab_bottom_app_bar_button: + Animation( + y=self._fab_bottom_app_bar_button.y + dp(4), + d=self.show_duration, + t=self.show_transition, + ).start(self._fab_bottom_app_bar_button) + + self.bar_is_hidden = False + self._animated_show = True + anim = Animation( + y=0, + d=self.show_duration, + t=self.show_transition, + ) + anim.bind(on_progress=on_progress, on_complete=on_complete) + anim.start(self) + + def hide_bar(self) -> None: + """Hide :class:`~MDBottomAppBar` panel.""" + + def on_complete(*args): + self.dispatch("on_hide_bar") + + def on_progress(animation, instance, progress): + if ( + progress > 0.5 + and self._animated_hidden + and widget_icon == instance.icon + ): + self._animated_hidden = False + anim_bar = Animation( + y=-self.height, + d=self.hide_duration, + # t=self.hide_transition, + ) + anim_bar.bind(on_complete=on_complete) + anim_bar.start(self) + + if self._fab_bottom_app_bar_button: + Animation( + y=self._fab_bottom_app_bar_button.y - dp(4), + d=self.hide_duration, + t=self.hide_transition, + ).start(self._fab_bottom_app_bar_button) + + self.bar_is_hidden = True + self._animated_hidden = True + len_children = len(self.children) + widget_icon = "" + + for i, widget in enumerate(self.children): + if isinstance(widget, MDActionBottomAppBarButton): + anim = Animation( + y=-widget.height, + d=self.hide_duration, + t=self.hide_transition, + ) + if i + 2 == len_children: + widget_icon = widget.icon + anim.bind(on_progress=on_progress) + Clock.schedule_once( + lambda x, y=widget: anim.start(y), + i / 10, + ) + + def on_show_bar(self, *args) -> None: + """ + The method is fired when the :class:`~MDBottomAppBar` panel + is shown. + """ + + def on_hide_bar(self, *args) -> None: + """ + The method is fired when the :class:`~MDBottomAppBar` panel + is hidden. + """ + + def on_scroll_cls(self, instance, scroll_cls) -> None: + """ + Fired when the value of the :attr:`scroll_cls` attribute changes. + """ + + def on_scroll_cls(*args): + if not self.allow_hidden: + Logger.warning( + "KivyMD: " + "In order for the bottom bar to be automatically hidden " + "in addition to the `scroll_cls` parameter, set the value " + "of the `allow_hidden` parameter to `True`" + ) + + if issubclass(scroll_cls.__class__, ScrollView): + if self.allow_hidden: + scroll_cls.bind(scroll_y=self.check_scroll_direction) + else: + raise TypeError( + f"The `scroll_cls` parameter must be an object inherited " + f"from the {ScrollView} class" + ) + + Clock.schedule_once(on_scroll_cls) + + def on_size(self, *args) -> None: + """Fired when the root screen is resized.""" + + if self._fab_bottom_app_bar_button: + self._fab_bottom_app_bar_button.x = Window.width - (dp(56) + dp(16)) + + def on_action_items(self, instance, value: list) -> None: + """ + Fired when the value of the :attr:`action_items` attribute changes. + """ + + def wait_removed(*args): + if len(self.children) == 1 or not self.children: + Clock.unschedule(wait_removed) + self._wait_removed = False + self._x = -dp(48) + asynckivy.start(add_widget()) + + async def add_widget(): + for button in value: + await asynckivy.sleep(0) + self.add_widget(button) + + if self._cache: + self._cache.append(value) + + for data in self._cache: + if value[0] in data: + for i, widget in enumerate(self.children): + if not self._wait_removed: + Clock.schedule_interval(wait_removed, 0) + self._wait_removed = True + if isinstance(widget, MDActionBottomAppBarButton): + anim = Animation( + y=-widget.height, + d=self.hide_duration, + t=self.hide_transition, + ) + anim.bind( + on_complete=lambda x, y=widget: self.remove_widget( + y + ) + ) + Clock.schedule_once( + lambda x, y=widget: anim.start(y), + i / 10, + ) + else: + self._cache.append(value) + self._current_data = value + asynckivy.start(add_widget()) + + def set_fab_opacity(self, *ars) -> None: + """ + Sets the transparency value of the:class:`~MDFabBottomAppBarButton` + button. + """ + + # self._fab_bottom_app_bar_button.opacity = 1 + + def set_fab_icon(self, instance, value) -> None: + """ + Animates the size of the :class:`~MDFabBottomAppBarButton` button. + """ + + # self._fab_bottom_app_bar_button.opacity = 0 + anim = Animation( + scale_value_x=0, + scale_value_y=0, + opacity=0, + d=self.hide_duration, + t=self.hide_transition, + ) + Animation( + scale_value_x=1, + scale_value_y=1, + opacity=1, + d=self.show_duration, + t=self.show_transition, + ) + anim.bind(on_complete=self.set_fab_opacity) + anim.start(instance) + + def add_widget(self, widget, index=0, canvas=None): + if isinstance(widget, MDActionBottomAppBarButton): + self._x += widget.width + widget.pos = ( + self._x + self._padding, + -dp(48) if self.animation else self.height / 2 - dp(48) / 2, + ) + widget.opacity = int(not self.animation) + super().add_widget(widget) + self.button_centering_animation(widget) + elif isinstance(widget, MDFabBottomAppBarButton): + widget.bind(icon=self.set_fab_icon) + self._fab_bottom_app_bar_button = widget + Clock.schedule_once(self.set_fab_opacity) + widget.scale_value_x = int(not self.animation) + widget.scale_value_y = int(not self.animation) + widget.pos = ( + Window.width - (dp(56) + self._padding), + self.height / 2 - dp(56) / 2, + ) + super().add_widget(widget) diff --git a/kivymd/uix/backdrop/__init__.py b/kivymd/uix/backdrop/__init__.py deleted file mode 100644 index 7a5e6cf27..000000000 --- a/kivymd/uix/backdrop/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .backdrop import MDBackdrop # NOQA F401 diff --git a/kivymd/uix/backdrop/backdrop.kv b/kivymd/uix/backdrop/backdrop.kv deleted file mode 100644 index 2f56d5be4..000000000 --- a/kivymd/uix/backdrop/backdrop.kv +++ /dev/null @@ -1,50 +0,0 @@ - - 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" diff --git a/kivymd/uix/backdrop/backdrop.py b/kivymd/uix/backdrop/backdrop.py deleted file mode 100644 index 276eb1c78..000000000 --- a/kivymd/uix/backdrop/backdrop.py +++ /dev/null @@ -1,548 +0,0 @@ -""" -Components/Backdrop -=================== - -.. seealso:: - - `Material Design spec, 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 - - - - 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 - - - - icon: "android" - - IconLeftWidget: - icon: root.icon - - - - 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 - - - - 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( - ''' - - - 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 `_ -""" - -__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 diff --git a/kivymd/uix/badge/__init__.py b/kivymd/uix/badge/__init__.py new file mode 100644 index 000000000..043d8e1a4 --- /dev/null +++ b/kivymd/uix/badge/__init__.py @@ -0,0 +1 @@ +from .badge import MDBadge # NOQA F401 diff --git a/kivymd/uix/badge/badge.kv b/kivymd/uix/badge/badge.kv new file mode 100644 index 000000000..ac085574d --- /dev/null +++ b/kivymd/uix/badge/badge.kv @@ -0,0 +1,17 @@ + + font_style: "Label" + role: "small" + radius: [self.texture_size[1] / 2, ] + pos_hint: {"center_x": 0.5, "center_y": 0.5} + padding: "4dp", "2dp" + halign: "center" + valign: "center" + adaptive_size: True + md_bg_color: self.theme_cls.errorColor + text_color: self.theme_cls.onErrorColor + pos: + ( \ + self.parent.x + (self.parent.width / 2), \ + self.parent.y + (self.parent.height / 2) \ + ) \ + if self.parent else (0, 0) diff --git a/kivymd/uix/badge/badge.py b/kivymd/uix/badge/badge.py new file mode 100644 index 000000000..8134046c7 --- /dev/null +++ b/kivymd/uix/badge/badge.py @@ -0,0 +1,74 @@ +""" +Components/Badge +================ + +.. versionadded:: 2.0.0 + + +.. seealso:: + + `Material Design 3 spec, Badge `_ + +.. rubric:: Badges show notifications, counts, or status information on + navigation items and icons. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/badges.png + :align: center + +Example +------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDIcon: + icon: "gmail" + pos_hint: {'center_x': .5, 'center_y': .5} + + MDBadge: + text: "12" + ''' + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/badges-example.png + :align: center +""" + +__all__ = ("MDBadge",) + +import os + +from kivy.lang import Builder + +from kivymd.uix.label import MDLabel +from kivymd import uix_path + +with open( + os.path.join(uix_path, "badge", "badge.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDBadge(MDLabel): + """ + Badge class. + + .. versionadded:: 2.0.0 + + For more information see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ diff --git a/kivymd/uix/banner/__init__.py b/kivymd/uix/banner/__init__.py deleted file mode 100644 index 3ed33d09d..000000000 --- a/kivymd/uix/banner/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .banner import MDBanner # NOQA F401 diff --git a/kivymd/uix/banner/banner.kv b/kivymd/uix/banner/banner.kv deleted file mode 100644 index 3d874ce6d..000000000 --- a/kivymd/uix/banner/banner.kv +++ /dev/null @@ -1,85 +0,0 @@ -#:import Window kivy.core.window.Window - - - - 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 - - - - text: root.text_message[0] - secondary_text: root.text_message[1] - divider: None - _no_ripple_effect: True - - ImageLeftWidget: - source: root.icon - - - - text: root.text_message[0] - divider: None - _no_ripple_effect: True - - ImageLeftWidget: - source: root.icon - - - - text: root.text_message[0] - secondary_text: root.text_message[1] - tertiary_text: root.text_message[2] - divider: None - _no_ripple_effect: True - - - - text: root.text_message[0] - secondary_text: root.text_message[1] - divider: None - _no_ripple_effect: True - - - - text: root.text_message[0] - divider: None - _no_ripple_effect: True - - - - 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 diff --git a/kivymd/uix/banner/banner.py b/kivymd/uix/banner/banner.py deleted file mode 100644 index 534783c90..000000000 --- a/kivymd/uix/banner/banner.py +++ /dev/null @@ -1,439 +0,0 @@ -""" -Components/Banner -================= - -.. seealso:: - - `Material Design spec, Banner `_ - -.. 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(''' - - - 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 `_ -""" - -__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', ]; - """ - - 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 diff --git a/kivymd/uix/behaviors/__init__.py b/kivymd/uix/behaviors/__init__.py index d80d8c180..5d26fefc7 100755 --- a/kivymd/uix/behaviors/__init__.py +++ b/kivymd/uix/behaviors/__init__.py @@ -5,21 +5,11 @@ 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, diff --git a/kivymd/uix/behaviors/backgroundcolor_behavior.py b/kivymd/uix/behaviors/backgroundcolor_behavior.py index a24166d68..9e4167e83 100755 --- a/kivymd/uix/behaviors/backgroundcolor_behavior.py +++ b/kivymd/uix/behaviors/backgroundcolor_behavior.py @@ -7,7 +7,7 @@ from __future__ import annotations -__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior") +__all__ = ("BackgroundColorBehavior",) from kivy.animation import Animation from kivy.lang import Builder @@ -15,15 +15,10 @@ 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( """ @@ -49,11 +44,17 @@ 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 +74,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 +94,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 +119,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 +158,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, - ) - - 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 + self.bind(pos=self.update_background_origin) - 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 +179,10 @@ def on_md_bg_color(self, instance_md_widget, color: list | str): 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 diff --git a/kivymd/uix/behaviors/declarative_behavior.py b/kivymd/uix/behaviors/declarative_behavior.py index 0fb5eb782..087d95ac5 100644 --- a/kivymd/uix/behaviors/declarative_behavior.py +++ b/kivymd/uix/behaviors/declarative_behavior.py @@ -293,6 +293,15 @@ def on_start(self): 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 +316,8 @@ class DeclarativeBehavior: and defaults to `''`. """ + __ids = _Dict() + def __init__(self, *args, **kwargs): super().__init__(**kwargs) @@ -314,4 +325,12 @@ def __init__(self, *args, **kwargs): 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 diff --git a/kivymd/uix/behaviors/elevation.py b/kivymd/uix/behaviors/elevation.py index f081d5c13..12dafc4e2 100755 --- a/kivymd/uix/behaviors/elevation.py +++ b/kivymd/uix/behaviors/elevation.py @@ -31,7 +31,7 @@ ) KV = ''' - + size_hint: None, None size: "250dp", "50dp" @@ -39,19 +39,19 @@ 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 @@ def build(self): from kivymd.uix.screen import MDScreen - class RectangularElevationButton( + class ElevationWidget( RectangularRippleBehavior, CommonElevationBehavior, ButtonBehavior, @@ -101,13 +101,13 @@ class Example(MDApp): 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 @@ def build(self): size: 100, 100 md_bg_color: 0, 0, 1, 1 elevation: 2 - radius: 18 + radius: dp(18) ''' @@ -306,6 +306,7 @@ def build(self): 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 @@ def build(self): size=(100, 100), md_bg_color="blue", elevation=2, - radius=18, + radius=dp(18), ) ) ) @@ -355,23 +356,17 @@ def build(self): 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,16 @@ def build(self): 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] * 2.5 + # blur_radius: root.elevation * 10 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 +414,29 @@ 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(1.5), 2: dp(3), 3: dp(6), 4: dp(8), 5: dp(12)} + ) + """ + 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(1), 2: dp(3), 3: dp(6), 4: dp(8), 5: dp(12)}`. + """ + elevation = BoundedNumericProperty(0, min=0, errorvalue=0) """ Elevation of the widget. @@ -449,7 +465,7 @@ class documentation. 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 +502,26 @@ def build(self): from kivymd.uix.behaviors import BackgroundColorBehavior, CommonElevationBehavior KV = ''' - + 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 +538,7 @@ def build(self): :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 +555,23 @@ def build(self): from kivymd.uix.behaviors import BackgroundColorBehavior, CommonElevationBehavior KV = ''' - + 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 +589,7 @@ def build(self): .. 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 +597,7 @@ def build(self): .. 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 +605,7 @@ def build(self): .. 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 +623,7 @@ def build(self): .. 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 +695,10 @@ def build(self): 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. - """ + # _elevation = 0 + _elevation_level = 0 + _shadow_softness = 0 + _shadow_color = (0, 0, 0, 0) - 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 diff --git a/kivymd/uix/behaviors/focus_behavior.py b/kivymd/uix/behaviors/focus_behavior.py index 33e00bdd4..d9ad37ed2 100644 --- a/kivymd/uix/behaviors/focus_behavior.py +++ b/kivymd/uix/behaviors/focus_behavior.py @@ -15,7 +15,7 @@ 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 @@ ''' - class FocusWidget(MDBoxLayout, RectangularElevationBehavior, FocusBehavior): + class FocusWidget(MDBoxLayout, CommonElevationBehavior, FocusBehavior): pass @@ -65,25 +65,26 @@ def build(self): __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 diff --git a/kivymd/uix/behaviors/hover_behavior.py b/kivymd/uix/behaviors/hover_behavior.py index dbe1ce2b1..113662924 100644 --- a/kivymd/uix/behaviors/hover_behavior.py +++ b/kivymd/uix/behaviors/hover_behavior.py @@ -85,23 +85,25 @@ def build(self): 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 +111,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 +120,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 +134,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,33 +159,21 @@ def on_mouse_update(self, *args): 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 - # + # 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 @@ -190,37 +182,35 @@ def on_mouse_update(self, *args): 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 +218,7 @@ def on_mouse_update(self, *args): 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.""" diff --git a/kivymd/uix/behaviors/motion_behavior.py b/kivymd/uix/behaviors/motion_behavior.py index 044970f43..d510cb4b2 100644 --- a/kivymd/uix/behaviors/motion_behavior.py +++ b/kivymd/uix/behaviors/motion_behavior.py @@ -23,7 +23,9 @@ from kivy.animation import Animation from kivy.clock import Clock from kivy.core.window import Window -from kivy.properties import StringProperty, NumericProperty +from kivy.metrics import dp +from kivy.properties import StringProperty, NumericProperty, OptionProperty +from kivymd.uix.behaviors.scale_behavior import ScaleBehavior from kivymd.uix.behaviors.stencil_behavior import StencilBehavior @@ -166,80 +168,217 @@ def on__scale_y(self, instance, value): 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 collapse(self, *args) -> None: + """Collapses the button.""" + + 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:`~MotionBase` class 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. - def set_default_values(self): - """Sets default scaled and transparency values.""" + :attr:`show_button_container_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_circ'`. + """ - self.scale_value_x = self.scale_x - self.scale_value_y = self.scale_y - self.opacity = 0 + 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) -class MotionShackBehavior(StencilBehavior, MotionBase): + if self._scrim: + Animation(alpha=0.4, d=self.show_duration).start(self._scrim) + + Clock.schedule_once(open) + + +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 +396,7 @@ def remove_snackbar(*args): 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 +413,9 @@ def open(*args): ) ) 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 diff --git a/kivymd/uix/behaviors/ripple_behavior.py b/kivymd/uix/behaviors/ripple_behavior.py index fddcef709..319d8e142 100644 --- a/kivymd/uix/behaviors/ripple_behavior.py +++ b/kivymd/uix/behaviors/ripple_behavior.py @@ -285,11 +285,12 @@ class CommonRipple: and defaults to `'ripple_func_out'`. """ + ripple_effect = BooleanProperty(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 +334,8 @@ def fade_out(self, *args) -> None: 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 +381,7 @@ def call_ripple_animation_methods(self, touch) -> None: 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 +432,11 @@ class documentation. """ 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 +500,7 @@ class documentation. """ 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: diff --git a/kivymd/uix/behaviors/state_layer_behavior.py b/kivymd/uix/behaviors/state_layer_behavior.py new file mode 100644 index 000000000..607b05104 --- /dev/null +++ b/kivymd/uix/behaviors/state_layer_behavior.py @@ -0,0 +1,526 @@ +# TODO: Add docs. + +""" +Behaviors/State Layer +===================== + +.. seealso:: + + `Material Design spec, State layers `_ +""" + +from kivy.lang import Builder +from kivy.properties import ColorProperty, NumericProperty, ObjectProperty + +from kivymd.uix.behaviors.focus_behavior import FocusBehavior + + +Builder.load_string( + """ + + 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_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). + """ + + 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 + + target_color = self._get_target_color() + if ( + isinstance( + self, + ( + MDCard, + MDTextField, + MDIconButton, + MDButton, + MDFabButton, + MDExtendedFabButton, + MDChip, + MDSwitch, + MDCheckbox, + BaseListItem, + ), + ) + 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] diff --git a/kivymd/uix/behaviors/toggle_behavior.py b/kivymd/uix/behaviors/toggle_behavior.py index c396a29c7..df77fed06 100644 --- a/kivymd/uix/behaviors/toggle_behavior.py +++ b/kivymd/uix/behaviors/toggle_behavior.py @@ -125,25 +125,16 @@ def build(self): - :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 @@ def __init__(self, **kwargs): # 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 issubclass(self.__class__, ButtonContentsIconText): - self.icon_color = self.text_color + if isinstance(self, MDIconButton): + self.md_bg_color = self.theme_cls.surfaceContainerHighestColor + self.icon_color = self.theme_cls.primaryColor + + # 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." + ) diff --git a/kivymd/uix/behaviors/touch_behavior.py b/kivymd/uix/behaviors/touch_behavior.py index 8aca7bf22..4b528aaa4 100644 --- a/kivymd/uix/behaviors/touch_behavior.py +++ b/kivymd/uix/behaviors/touch_behavior.py @@ -91,10 +91,10 @@ def delete_clock(self, widget, touch, *args): 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.""" diff --git a/kivymd/uix/bottomnavigation/__init__.py b/kivymd/uix/bottomnavigation/__init__.py deleted file mode 100644 index d62fda660..000000000 --- a/kivymd/uix/bottomnavigation/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# NOQA F401 -from .bottomnavigation import MDBottomNavigation, MDBottomNavigationItem diff --git a/kivymd/uix/bottomnavigation/bottomnavigation.kv b/kivymd/uix/bottomnavigation/bottomnavigation.kv deleted file mode 100644 index 79f726b28..000000000 --- a/kivymd/uix/bottomnavigation/bottomnavigation.kv +++ /dev/null @@ -1,118 +0,0 @@ -#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT - - - - 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" - - - - 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" - - - - md_bg_color: root.theme_cls.bg_normal diff --git a/kivymd/uix/bottomnavigation/bottomnavigation.py b/kivymd/uix/bottomnavigation/bottomnavigation.py deleted file mode 100644 index 9b764d4ec..000000000 --- a/kivymd/uix/bottomnavigation/bottomnavigation.py +++ /dev/null @@ -1,880 +0,0 @@ -""" -Components/BottomNavigation -=========================== - -.. seealso:: - - `Material Design 2 spec, Bottom navigation `_ and - `Material Design 3 spec, Bottom navigation `_ - -.. 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 - - - - 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 - - - - 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"" - - -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 diff --git a/kivymd/uix/bottomsheet/bottomsheet.kv b/kivymd/uix/bottomsheet/bottomsheet.kv index 6d63e1c20..bf2e31e47 100644 --- a/kivymd/uix/bottomsheet/bottomsheet.kv +++ b/kivymd/uix/bottomsheet/bottomsheet.kv @@ -27,7 +27,7 @@ orientation: "vertical" - md_bg_color: root.bg_color if root.bg_color else app.theme_cls.bg_darkest + md_bg_color: "red" # root.bg_color if root.bg_color else app.theme_cls.bg_darkest radius: 16, 16, 0, 0 padding: 0, "8dp", 0, 0 diff --git a/kivymd/uix/bottomsheet/bottomsheet.py b/kivymd/uix/bottomsheet/bottomsheet.py index 4961ff029..1406ebcf3 100644 --- a/kivymd/uix/bottomsheet/bottomsheet.py +++ b/kivymd/uix/bottomsheet/bottomsheet.py @@ -579,6 +579,7 @@ def build(self): from kivymd import uix_path from kivymd.uix.behaviors import CommonElevationBehavior, TouchBehavior +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDIconButton from kivymd.uix.label import MDLabel @@ -589,7 +590,7 @@ def build(self): os.path.join(uix_path, "bottomsheet", "bottomsheet.kv"), encoding="utf-8", ) as kv_file: - Builder.load_string(kv_file.read()) + Builder.load_string(kv_file.read(), filename="MDBottomSheet.kv") class BottomSheetDragHandle(MDWidget): @@ -685,13 +686,16 @@ def add_widget(self, widget, *args, **kwargs): return super().add_widget(widget) -class MDBottomSheet(MDBoxLayout, CommonElevationBehavior, TouchBehavior): +class MDBottomSheet( + MDBoxLayout, CommonElevationBehavior, StateLayerBehavior, TouchBehavior +): """ Bottom sheet class. For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` and :class:`~kivymd.uix.behaviors.touch_behavior.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.StateLayerBehavior` and :class:`~kivymd.uix.behaviors.touch_behavior.TouchBehavior` classes documentation. diff --git a/kivymd/uix/boxlayout.py b/kivymd/uix/boxlayout.py index c5f3e75d7..bf465b64e 100644 --- a/kivymd/uix/boxlayout.py +++ b/kivymd/uix/boxlayout.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -28,7 +28,7 @@ MDBoxLayout: adaptive_height: True - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor Available options are: ---------------------- @@ -89,15 +89,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDBoxLayout( - DeclarativeBehavior, ThemableBehavior, BoxLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + BoxLayout, + MDAdaptiveWidget, ): """ Box layout class. - For more information, see in the - :class:`~kivy.uix.boxlayout.BoxLayout` class documentation. + For more information see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/button/__init__.py b/kivymd/uix/button/__init__.py index b833a4cd3..b908f1fe9 100644 --- a/kivymd/uix/button/__init__.py +++ b/kivymd/uix/button/__init__.py @@ -1,17 +1,13 @@ # NOQA F401 from .button import ( - BaseButton, - ButtonContentsIconText, - MDFillRoundFlatButton, - MDFillRoundFlatIconButton, - MDFlatButton, - MDFloatingActionButton, - MDFloatingActionButtonSpeedDial, + MDButton, + MDButtonIcon, + MDButtonText, MDIconButton, - MDRaisedButton, - MDRectangleFlatButton, - MDRectangleFlatIconButton, - MDRoundFlatButton, - MDRoundFlatIconButton, - MDTextButton, + MDFabButton, + BaseButton, + BaseFabButton, + MDExtendedFabButton, + MDExtendedFabButtonIcon, + MDExtendedFabButtonText, ) diff --git a/kivymd/uix/button/button.kv b/kivymd/uix/button/button.kv index 77f7d09d3..92113cacd 100644 --- a/kivymd/uix/button/button.kv +++ b/kivymd/uix/button/button.kv @@ -1,225 +1,396 @@ - - canvas: - Clear + + size_hint: None, None + text_size: self.size + halign: "center" + valign: "center" + size: + { \ + "standard": ("56dp", "56dp"), \ + "small": ("40dp", "40dp"), \ + "large": ("96dp", "96dp"), \ + }[self.style] + radius: + { \ + "standard": [dp(16), ], \ + "small": [dp(12), ], \ + "large": [dp(28), ], \ + }[self.style] + shadow_radius: + { \ + "standard": [dp(12), ], \ + "small": [dp(8), ], \ + "large": [dp(24), ], \ + }[self.style] + shadow_offset: 0, -1 + shadow_softness: + { \ + "standard": 2, \ + "small": 2, \ + "large": 2, \ + }[self.style] + elevation_level: + { \ + "standard": 2, \ + "small": 2, \ + "large": 2, \ + }[self.style] + shadow_color: + ( \ + self.theme_cls.shadowColor \ + if self.theme_shadow_color == "Primary" else \ + self.shadow_color \ + ) \ + if not self.disabled else self.theme_cls.transparentColor + icon_color: + { \ + "surface": self.theme_cls.onPrimaryContainerColor, \ + "secondary": self.theme_cls.onSecondaryContainerColor, \ + "tertiary": self.theme_cls.onTertiaryContainerColor \ + }[self.color_map] \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color \ + if self.icon_color else \ + self.theme_cls.transparentColor \ + ) + disabled_color: + self.theme_cls.onSurfaceColor[:-1] + \ + [self.fab_button_opacity_value_disabled_icon] \ + if not self.icon_color_disabled else self.icon_color_disabled + theme_font_size: "Custom" + font_size: + { \ + "standard": "24sp", \ + "small": "24sp", \ + "large": "36sp", \ + }[root.style] + + canvas.before: Color: - group: "bg-color" rgba: - self._md_bg_color \ - if not self.disabled else \ - self._md_bg_color_disabled + { \ + "surface": self.theme_cls.surfaceColor, \ + "secondary": self.theme_cls.secondaryColor, \ + "tertiary": self.theme_cls.tertiaryColor \ + }[self.color_map] \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color 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 + radius: self.radius - - 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] \ - ) + + x: "16dp" + icon_color: + ( \ + { \ + "surface": self.theme_cls.onPrimaryContainerColor, \ + "secondary": self.theme_cls.onSecondaryContainerColor, \ + "tertiary": self.theme_cls.onTertiaryContainerColor \ + }[self.parent.color_map] \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color \ + if self.icon_color else self.theme_cls.transparentColor \ + ) \ + ) \ + if self.parent else self.theme_cls.transparentColor + disabled_color: + self.theme_cls.onSurfaceColor[:-1] + \ + [self.fab_button_opacity_value_disabled_icon] \ + if not self.icon_color_disabled else self.icon_color_disabled + pos_hint: {"center_y": .5} - 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 - - - - 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 - - - - 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 - - - - adaptive_size: True - color: root.theme_cls.primary_color if not root.color else root.color - opacity: 1 + + adaptive_width: True + text_color: + ( \ + { \ + "surface": self.theme_cls.onPrimaryContainerColor, \ + "secondary": self.theme_cls.onSecondaryContainerColor, \ + "tertiary": self.theme_cls.onTertiaryContainerColor \ + }[self.parent.color_map] \ + if self.theme_text_color == "Primary" else self.text_color \ + ) \ + if self.parent else self.text_color + disabled_color: + ( \ + self.theme_cls.onSurfaceColor[:-1] + \ + [self.fab_button_opacity_value_disabled_icon] \ + if not self.parent.icon_color_disabled else \ + self.parent.icon_color_disabled \ + ) \ + if self.parent else self.theme_cls.transparentColor + pos_hint: {"center_y": .5} - - theme_text_color: "Custom" - md_bg_color: self.theme_cls.primary_color + + size_hint: None, None + size: "56dp", "56dp" + radius: [dp(16), ] + shadow_radius: [dp(12), ] + shadow_offset: 0, -1 + shadow_softness: 2 + elevation_level: 2 + shadow_color: + ( \ + self.theme_cls.shadowColor \ + if self.theme_shadow_color == "Primary" else \ + self.shadow_color \ + ) \ + if not self.disabled else self.theme_cls.transparentColor + theme_font_size: "Custom" + font_size: "24sp" canvas.before: Color: rgba: - self.theme_cls.primary_color \ - if not self._bg_color else \ - self._bg_color + { \ + "standard": self.theme_cls.surfaceContainerColor \ + if self.color_map == "surface" else \ + { \ + "secondary": self.theme_cls.secondaryContainerColor, \ + "tertiary": self.theme_cls.tertiaryContainerColor \ + }[self.color_map], \ + "small": self.theme_cls.surfaceContainerHighColor \ + if self.color_map == "surface" else \ + { \ + "secondary": self.theme_cls.secondaryContainerColor, \ + "tertiary": self.theme_cls.tertiaryColor \ + }[self.color_map], \ + "large": self.theme_cls.surfaceContainerColor \ + if self.color_map == "surface" else \ + { \ + "secondary": self.theme_cls.secondaryContainerColor, \ + "tertiary": self.theme_cls.tertiaryColor \ + }[self.color_map], \ + }[self.style] \ + if self.theme_bg_color == "Primary" else \ + self.md_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] - - - - theme_text_color: "Custom" - md_bg_color: self.theme_cls.primary_color - + size: self.size + pos: 0, 0 + radius: self.radius - - padding_x: "8dp" - padding_y: "8dp" - adaptive_size: True - theme_text_color: "Custom" + canvas.before: Color: - rgba: self.bg_color + rgba: + ( \ + { \ + "standard": self.theme_cls.transparentColor, \ + "outlined": self.theme_cls.transparentColor, \ + "tonal": self.theme_cls.secondaryContainerColor, \ + "filled": self.theme_cls.primaryColor, \ + }[self.style] \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color \ + ) \ + if not self.disabled else \ + ( \ + ( \ + { \ + "standard": self.theme_cls.transparentColor, \ + "outlined": self.theme_cls.transparentColor, \ + "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], \ + }[self.style] \ + ) \ + if not self.md_bg_color_disabled else self.md_bg_color_disabled \ + ) RoundedRectangle: size: self.size pos: self.pos radius: self.radius + + radius: [20,] + halign: "center" + valign: "center" + size_hint: None, None + size: dp(40), dp(40) + text_size: self.size + line_color: + ( \ + ( \ + self.theme_cls.outlineColor \ + if self.theme_line_color == "Primary" else \ + ( \ + self._line_color \ + if self._line_color else \ + self.line_color \ + ) \ + ) \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + \ + [self.icon_button_outlined_opacity_value_disabled_line] \ + ) \ + if self.style == "outlined" else self.theme_cls.transparentColor + icon_color: + ( \ + { \ + "standard": self.theme_cls.primaryColor, \ + "tonal": self.theme_cls.onSecondaryContainerColor, \ + "filled": self.theme_cls.onPrimaryColor, \ + "outlined": self.theme_cls.onSurfaceVariantColor, \ + }[self.style] \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color \ + if self.icon_color else self.theme_cls.transparentColor \ + ) \ + ) + disabled_color: + { \ + "standard": self.theme_cls.onSurfaceColor[:-1] + \ + [self.icon_button_standard_opacity_value_disabled_icon], \ + "tonal": self.theme_cls.onSurfaceColor[:-1] + \ + [self.icon_button_tonal_opacity_value_disabled_icon], \ + "filled": self.theme_cls.onSurfaceColor[:-1] + \ + [self.icon_button_filled_opacity_value_disabled_icon], \ + "outlined": self.theme_cls.onSurfaceColor[:-1] + \ + [self.icon_button_outlined_opacity_value_disabled_icon], \ + }[self.style] \ + if not self.icon_color_disabled else self.icon_color_disabled + + + + md_bg_color: + { \ + "elevated": self.theme_cls.surfaceContainerLowColor, \ + "filled": self.theme_cls.primaryColor, \ + "tonal": self.theme_cls.secondaryContainerColor, \ + "outlined": self.theme_cls.transparentColor, \ + "text": self.theme_cls.transparentColor, \ + }[self.style] \ + if self.theme_bg_color == "Primary" else self.md_bg_color + line_color: + ( \ + ( \ + self.theme_cls.outlineColor \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_outlined_opacity_value_disabled_line] \ + ) \ + if self.style == "outlined" else \ + self.theme_cls.transparentColor \ + ) \ + if self.theme_line_color == "Primary" else self.line_color + size_hint_x: None if self.theme_width == "Primary" else self.size_hint_x + size_hint_y: None if self.theme_height == "Primary" else self.size_hint_y + height: "40dp" + elevation: self.elevation_levels[self.elevation_level] + shadow_color: + ( \ + ( \ + self.theme_cls.shadowColor \ + if self.theme_shadow_color == "Primary" else \ + self.shadow_color \ + ) \ + if self.style not in ["outlined", "text"] else \ + self.theme_cls.transparentColor \ + ) \ + if not self.disabled else self.theme_cls.transparentColor + shadow_radius: self.radius + elevation_level: + { \ + "elevated": 1, \ + "filled": 0, \ + "tonal": 0, \ + "outlined": 0, \ + "text": 0, \ + }[self.style] + shadow_offset: [0, -1] if self.style == "elevated" else [0, 0] + + + + adaptive_size: True + pos_hint: {"center_y": .5} + font_style: "Label" + role: "large" + markup: True + disabled: self._button.disabled if self._button else False + text_color: + ( \ + ( \ + ( \ + { \ + "elevated": self.theme_cls.primaryColor, \ + "filled": self.theme_cls.onPrimaryColor, \ + "tonal": self.theme_cls.onSecondaryContainerColor, \ + "outlined": self.theme_cls.primaryColor, \ + "text": self.theme_cls.primaryColor, \ + }[self._button.style] \ + ) \ + if self._button else self.theme_cls.transparentColor \ + ) \ + if self.theme_text_color == "Primary" else self.text_color \ + ) + disabled_color: + ( \ + { \ + "elevated": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_elevated_opacity_value_disabled_text], \ + "filled": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_filled_opacity_value_disabled_text], \ + "tonal": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_tonal_opacity_value_disabled_text], \ + "outlined": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_outlined_opacity_value_disabled_text], \ + "text": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_text_opacity_value_disabled_text], \ + }[self._button.style] \ + ) \ + if self._button else self.theme_cls.transparentColor + + + + size_hint: None, None + size: "18dp", "18dp" + theme_font_size: "Custom" + font_size: "20sp" + x: "16dp" + pos_hint: {"center_y": .5} + icon_color: + ( \ + ( \ + ( \ + { \ + "elevated": self.theme_cls.primaryColor, \ + "filled": self.theme_cls.onPrimaryColor, \ + "tonal": self.theme_cls.onSecondaryContainerColor, \ + "outlined": self.theme_cls.primaryColor, \ + "text": self.theme_cls.primaryColor, \ + }[self._button.style] \ + ) \ + if self._button else self.theme_cls.transparentColor \ + ) \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color \ + if self.icon_color else \ + self.theme_cls.transparentColor \ + ) \ + ) + disabled_color: + ( \ + { \ + "elevated": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_elevated_opacity_value_disabled_icon], \ + "filled": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_filled_opacity_value_disabled_icon], \ + "tonal": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_tonal_opacity_value_disabled_icon], \ + "outlined": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_outlined_opacity_value_disabled_icon], \ + "text": self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_text_opacity_value_disabled_icon], \ + }[self._button.style] \ + if not self.icon_color_disabled else self.icon_color_disabled \ + ) \ + if self._button else self.theme_cls.transparentColor diff --git a/kivymd/uix/button/button.py b/kivymd/uix/button/button.py index 5750a334d..f8e915881 100755 --- a/kivymd/uix/button/button.py +++ b/kivymd/uix/button/button.py @@ -4,34 +4,55 @@ .. seealso:: - `Material Design spec, Buttons `_ - - `Material Design spec, Buttons: floating action button `_ + `Material Design spec, Buttons `_ .. rubric:: Buttons allow users to take actions, and make choices, - with a single tap. + with a single tap. When choosing the right button for an action, consider + the level of emphasis each button type provides. + +KivyMD provides the following button classes for use: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/buttons.png :align: center -`KivyMD` provides the following button classes for use: - -- MDIconButton_ -- MDFloatingActionButton_ -- MDFlatButton_ -- MDRaisedButton_ -- MDRectangleFlatButton_ -- MDRectangleFlatIconButton_ -- MDRoundFlatButton_ -- MDRoundFlatIconButton_ -- MDFillRoundFlatButton_ -- MDFillRoundFlatIconButton_ -- MDTextButton_ -- MDFloatingActionButtonSpeedDial_ - -.. MDIconButton: -MDIconButton ------------- +1. Elevated button +2. Filled button +3. Filled tonal button +4. Outlined button +5. Text button +6. Icon button +7. Segmented button +8. Floating action button (FAB) +9. Extended FAB + +Common buttons +============== + +.. rubric:: Buttons help people take action, such as sending an email, sharing + a document, or liking a comment. + +.. seealso:: + + `Material Design spec, Buttons `_ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/common-buttons.png + :align: center + +1. Elevated button +2. Filled button +3. Filled tonal button +4. Outlined button +5. Text button + +- Elevated_ +- Filled_ +- Tonal_ +- Outlined_ +- Text_ + +.. Elevated: +Elevated +-------- .. tabs:: @@ -45,17 +66,23 @@ KV = ''' MDScreen: + md_bg_color: app.theme_cls.surfaceColor - MDIconButton: - icon: "language-python" + MDButton: + style: "elevated" pos_hint: {"center_x": .5, "center_y": .5} + + MDButtonIcon: + icon: "plus" + + MDButtonText: + text: "Elevated" ''' class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" + self.theme_cls.primary_palette = "Green" return Builder.load_string(KV) @@ -66,637 +93,602 @@ def build(self): .. code-block:: python from kivymd.app import MDApp - from kivymd.uix.button import MDIconButton + from kivymd.uix.button import MDButton, MDButtonIcon, MDButtonText from kivymd.uix.screen import MDScreen class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" + self.theme_cls.primary_palette = "Green" return ( MDScreen( - MDIconButton( - icon="language-python", + MDButton( + MDButtonIcon( + icon="plus", + ), + MDButtonText( + text="Elevated", + ), + style="elevated", pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) + ), + md_bg_color=self.theme_cls.surfaceColor, ) ) Example().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevated-button.gif :align: center -The :class:`~MDIconButton.icon` parameter must have the name of the icon -from ``kivymd/icon_definitions.py`` file. - -You can also use custom icons: +Common buttons can contain an icon or be without an icon: .. code-block:: kv - MDIconButton: - icon: "kivymd/images/logo/kivymd-icon-256.png" + MDButton: + style: "elevated" + text: "Elevated" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-custom-button.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevated-without-icon-button.png :align: center -By default, :class:`~MDIconButton` button has a size ``(dp(48), dp (48))``. -Use :class:`~BaseButton.icon_size` attribute to resize the button: +.. Filled: +Filled +------ .. code-block:: kv - MDIconButton: - icon: "android" - icon_size: "64sp" + MDButton: + style: "filled" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button-user-font-size.png + MDButtonText: + text: "Filled" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/filled-button.gif :align: center -By default, the color of :class:`~MDIconButton` -(depending on the style of the application) is black or white. -You can change the color of :class:`~MDIconButton` as the text color -of :class:`~kivymd.uix.label.MDLabel`, substituting ``theme_icon_color`` for -``theme_text_color`` and ``icon_color`` for ``text_color``. +.. Tonal: +Tonal +----- .. code-block:: kv - MDIconButton: - icon: "android" - theme_icon_color: "Custom" - icon_color: app.theme_cls.primary_color + MDButton: + style: "tonal" + + MDButtonText: + text: "Tonal" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button-theme-text-color.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tonal-button.gif :align: center -.. MDFloatingActionButton: -MDFloatingActionButton ----------------------- +.. Outlined: +Outlined +-------- -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button.png - :align: center +.. code-block:: kv -The above parameters for :class:`~MDIconButton` apply -to :class:`~MDFloatingActionButton`. + MDButton: + style: "outlined" -To change :class:`~MDFloatingActionButton` background, use the -``md_bg_color`` parameter: + MDButtonText: + text: "Outlined" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/outlined-button.gif + :align: center + +.. Text: +Text +-------- .. code-block:: kv - MDFloatingActionButton: - icon: "android" - md_bg_color: app.theme_cls.primary_color + MDButton: + style: "text" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-md-bg-color.png + MDButtonText: + text: "Text" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-button.gif :align: center -Material design style 3 ------------------------ +Customization of buttons +======================== -.. code-block:: python +Text positioning and button size +-------------------------------- - from kivy.lang import Builder +.. code-block:: kv - from kivymd.app import MDApp - from kivymd.uix.button import MDFloatingActionButton + MDButton: + style: "tonal" + theme_width: "Custom" + height: "56dp" + size_hint_x: .5 - KV = ''' - MDScreen: - md_bg_color: "#f7f2fa" + MDButtonIcon: + x: text.x - (self.width + dp(10)) + icon: "plus" - MDBoxLayout: - id: box - spacing: "56dp" - adaptive_size: True + MDButtonText: + id: text + text: "Tonal" pos_hint: {"center_x": .5, "center_y": .5} - ''' +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/positioning-size-button.png + :align: center - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - self.theme_cls.material_style = "M3" - return Builder.load_string(KV) - - def on_start(self): - data = { - "standard": {"md_bg_color": "#fefbff", "text_color": "#6851a5"}, - "small": {"md_bg_color": "#e9dff7", "text_color": "#211c29"}, - "large": {"md_bg_color": "#f8d7e3", "text_color": "#311021"}, - } - for type_button in data.keys(): - self.root.ids.box.add_widget( - MDFloatingActionButton( - icon="pencil", - type=type_button, - theme_icon_color="Custom", - md_bg_color=data[type_button]["md_bg_color"], - icon_color=data[type_button]["text_color"], - ) - ) +Font of the button text +----------------------- +.. code-block:: kv - Example().run() + MDButton: + style: "filled" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-m3.png - :align: center + MDButtonIcon: + icon: "plus" -.. MDFlatButton: -MDFlatButton ------------- + MDButtonText: + text: "Filled" + font_style: "Title" -To change the text color of: class:`~MDFlatButton` use the ``text_color`` parameter: +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-style-button-text.png + :align: center .. code-block:: kv - MDFlatButton: - text: "MDFlatButton" - theme_text_color: "Custom" - text_color: "orange" + MDButton: + style: "elevated" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-flat-button-text-color.png + MDButtonText: + text: "Elevated" + theme_font_name: "Custom" + font_name: "path/to/font.ttf" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-name-button-text.png :align: center -Or use markup: +Custom button color +------------------- .. code-block:: kv - MDFlatButton: - text: "[color=#00ffcc]MDFlatButton[/color]" + MDButton: + style: "elevated" + theme_shadow_color: "Custom" + shadow_color: "red" -To specify the font size and font name, use the parameters as in the usual -`Kivy` buttons: + MDButtonIcon: + icon: "plus" + theme_icon_color: "Custom" + icon_color: "green" -.. code-block:: kv + MDButtonText: + text: "Elevated" + theme_text_color: "Custom" + text_color: "red" - MDFlatButton: - text: "MDFlatButton" - font_size: "18sp" - font_name: "path/to/font" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-color-button.png + :align: center -.. MDRaisedButton: -MDRaisedButton --------------- +Icon buttons +============ -This button is similar to the :class:`~MDFlatButton` button except that you -can set the background color for :class:`~MDRaisedButton`: +.. rubric:: Use icon buttons when a compact button is required, such as in a + toolbar or image list. There are two types of icon buttons: standard and + contained. -.. code-block:: kv +.. seealso:: - MDRaisedButton: - text: "MDRaisedButton" - md_bg_color: "red" + `Material Design spec, Icon buttons `_ -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-raised-button.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-buttons.png :align: center -.. MDRectangleFlatButton: -MDRectangleFlatButton ---------------------- +1. Standard icon button +2. Contained icon button (including filled, filled tonal, and outlined styles) + +- StandardIcon_ +- FilledIcon_ +- TonalIcon_ +- OutlinedIcon_ + +.. StandardIcon: +StandardIcon +------------ .. code-block:: kv - MDRectangleFlatButton: - text: "MDRectangleFlatButton" - theme_text_color: "Custom" - text_color: "white" - line_color: "red" + MDIconButton: + icon: "heart-outline" + style: "standard" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-button-md-bg-color.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-button-standard.gif :align: center -.. MDRectangleFlatIconButton: -MDRectangleFlatIconButton -------------------------- - -Button parameters :class:`~MDRectangleFlatIconButton` are the same as -button :class:`~MDRectangleFlatButton`, with the addition of the -``theme_icon_color`` and ``icon_color`` parameters as for :class:`~MDIconButton`. +.. FilledIcon: +FilledIcon +---------- .. code-block:: kv - MDRectangleFlatIconButton: - icon: "android" - text: "MDRectangleFlatIconButton" - theme_text_color: "Custom" - text_color: "white" - line_color: "red" - theme_icon_color: "Custom" - icon_color: "orange" + MDIconButton: + icon: "heart-outline" + style: "filled" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-icon-button-custom.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-button-filled.gif :align: center -Without border --------------- +.. TonalIcon: +TonalIcon +--------- -.. code-block:: python +.. code-block:: kv - from kivymd.app import MDApp - from kivymd.uix.screen import MDScreen - from kivymd.uix.button import MDRectangleFlatIconButton + MDIconButton: + icon: "heart-outline" + style: "tonal" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-button-tonal.gif + :align: center - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return ( - MDScreen( - MDRectangleFlatIconButton( - text="MDRectangleFlatIconButton", - icon="language-python", - line_color=(0, 0, 0, 0), - pos_hint={"center_x": .5, "center_y": .5}, - ) - ) - ) +.. OutlinedIcon: +OutlinedIcon +------------ +.. code-block:: kv - Example().run() + MDIconButton: + icon: "heart-outline" + style: "outlined" -.. code-block:: kv +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-button-outlined.gif + :align: center + +Custom icon size +---------------- - MDRectangleFlatIconButton: - text: "MDRectangleFlatIconButton" - icon: "language-python" - line_color: 0, 0, 0, 0 - pos_hint: {"center_x": .5, "center_y": .5} +.. code-block:: kv -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-icon-button-without-border.png + MDIconButton: + icon: "heart-outline" + style: "tonal" + theme_font_size: "Custom" + font_size: "48sp" + radius: [self.height / 2, ] + size_hint: None, None + size: "84dp", "84dp" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-button-size.png :align: center -.. MDRoundFlatButton: -MDRoundFlatButton ------------------ +Custom button color +------------------- .. code-block:: kv - MDRoundFlatButton: - text: "MDRoundFlatButton" - text_color: "white" + MDIconButton: + icon: "heart-outline" + style: "tonal" + theme_bg_color: "Custom" + md_bg_color: "brown" + theme_icon_color: "Custom" + icon_color: "white" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-round-flat-button-text-color.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-button-color.png :align: center -.. MDRoundFlatIconButton: -MDRoundFlatIconButton ---------------------- +FAB buttons +=========== -Button parameters :class:`~MDRoundFlatIconButton` are the same as -button :class:`~MDRoundFlatButton`, with the addition of the -``theme_icon_color`` and ``icon_color`` parameters as for :class:`~MDIconButton`: +.. rubric:: The FAB represents the most important action on a screen. + It puts key actions within reach. -.. code-block:: kv +.. seealso:: - MDRoundFlatIconButton: - text: "MDRoundFlatIconButton" - icon: "android" - text_color: "white" + `Material Design spec, FAB buttons `_ -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-round-flat-icon-button.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fab-buttons.png :align: center -.. MDFillRoundFlatButton: -MDFillRoundFlatButton ---------------------- +1. Standard FAB +2. Small FAB +3. Large FAB -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-fill-round-flat-button.png - :align: center +There are three sizes of floating action buttons: FAB, small FAB, and large FAB: -Button parameters :class:`~MDFillRoundFlatButton` are the same as -button :class:`~MDRaisedButton`. +- Standard_ +- Small_ +- Large_ -.. MDFillRoundFlatIconButton: -MDFillRoundFlatIconButton -------------------------- +.. Standard: +Standard +-------- -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-fill-round-flat-icon-button.png +.. code-block:: kv + + MDFabButton: + icon: "pencil-outline" + style: "standard" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fab-button-standard.gif :align: center -Button parameters :class:`~MDFillRoundFlatIconButton` are the same as -button :class:`~MDRaisedButton`, with the addition of the -``theme_icon_color`` and ``icon_color`` parameters as for :class:`~MDIconButton`. +.. Small: +Small +----- + +.. code-block:: kv -.. note:: Notice that the width of the :class:`~MDFillRoundFlatIconButton` - button matches the size of the button text. + MDFabButton: + icon: "pencil-outline" + style: "small" -.. MDTextButton: -MDTextButton ------------- +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fab-button-small.png + :align: center + +.. Large: +Large +----- .. code-block:: kv - MDTextButton: - text: "MDTextButton" - custom_color: "white" + MDFabButton: + icon: "pencil-outline" + style: "large" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-text-button.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fab-button-large.gif :align: center -.. MDFloatingActionButtonSpeedDial: -MDFloatingActionButtonSpeedDial -------------------------------- +Additional color mappings +------------------------- + +FABs can use other combinations of container and icon colors. The color +mappings below provide the same legibility and functionality as the default, +so the color mapping you use depends on style alone. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fab-color-mapping.png + :align: center -.. Note:: See the full list of arguments in the class - :class:`~MDFloatingActionButtonSpeedDial`. +1. Surface +2. Secondary +3. Tertiary .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp + from kivymd.uix.button import MDFabButton KV = ''' MDScreen: + md_bg_color: app.theme_cls.surfaceColor - MDFloatingActionButtonSpeedDial: - data: app.data - root_button_anim: True + MDBoxLayout: + id: box + adaptive_size: True + spacing: "32dp" + pos_hint: {"center_x": .5, "center_y": .5} ''' class Example(MDApp): - data = { - 'Python': 'language-python', - 'PHP': 'language-php', - 'C++': 'language-cpp', - } - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" + self.theme_cls.primary_palette = "Green" return Builder.load_string(KV) + def on_start(self): + styles = { + "standard": "surface", + "small": "secondary", + "large": "tertiary", + } + for style in styles.keys(): + self.root.ids.box.add_widget( + MDFabButton( + style=style, icon="pencil-outline", color_map=styles[style] + ) + ) + Example().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fab-button-color-mapping.png :align: center -Or without KV Language: - -.. tabs:: +Extended FAB +============ - .. tab:: Imperative python style +.. rubric:: Extended floating action buttons (extended FABs) help people take + primary actions. They're wider than FABs to accommodate a text label and + larger target area. - .. code-block:: python +.. seealso:: - from kivymd.uix.screen import MDScreen - from kivymd.app import MDApp - from kivymd.uix.button import MDFloatingActionButtonSpeedDial + `Material Design spec, FAB buttons `_ +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/extended-fab-button.png + :align: center - class Example(MDApp): - data = { - 'Python': 'language-python', - 'PHP': 'language-php', - 'C++': 'language-cpp', - } +1. Extended FAB with both icon and label text +2. Extended FAB without icon - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - screen = MDScreen() - speed_dial = MDFloatingActionButtonSpeedDial() - speed_dial.data = self.data - speed_dial.root_button_anim = True - screen.add_widget(speed_dial) - return screen +With icon +--------- +.. code-block:: python - Example().run() + from kivy.lang import Builder - .. tab:: Declarative python style + from kivymd.app import MDApp - .. code-block:: python + KV = ''' + MDScreen: + md_bg_color: app.theme_cls.surfaceColor + on_touch_down: + if not btn.collide_point(*args[1].pos): \\ + btn.fab_state = "expand" \\ + if btn.fab_state == "collapse" else "collapse" + + MDExtendedFabButton: + id: btn + icon: "heart" + text: "Compose" + pos_hint: {"center_x": .5, "center_y": .5} + ''' - from kivymd.uix.screen import MDScreen - from kivymd.app import MDApp - from kivymd.uix.button import MDFloatingActionButtonSpeedDial + class Example(MDApp): + def build(self): + self.theme_cls.primary_palette = "Green" + return Builder.load_string(KV) - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return ( - MDScreen( - MDFloatingActionButtonSpeedDial( - data={ - 'Python': 'language-python', - 'PHP': 'language-php', - 'C++': 'language-cpp', - }, - root_button_anim=True, - ) - ) - ) + Example().run() - Example().run() +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/extended-fab-button-icon.gif + :align: center -You can use various types of animation of labels for buttons on the stack: +Without icon +------------ .. code-block:: kv - MDFloatingActionButtonSpeedDial: - hint_animation: True + MDExtendedFabButton: + text: "Compose" + fab_state: "expand" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-hint.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/extended-fab-button-without-icon.png :align: center -You can set your color values for background, text of buttons etc: +API break +========= + +1.2.0 version +------------- .. code-block:: kv - MDFloatingActionButtonSpeedDial: - hint_animation: True - bg_hint_color: app.theme_cls.primary_dark + MDFloatingActionButton: + icon: "plus" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-hint-color.png - :align: center +.. code-block:: kv -Binds to individual buttons ---------------------------- + MDRoundFlatButton: + text: "Outlined" -.. tabs:: +.. code-block:: kv - .. tab:: Declarative KV style + MDRoundFlatIconButton: + text: "Outlined with icon" + icon: "plus" - .. code-block:: python +.. code-block:: kv - from kivy.lang import Builder - from kivy.properties import DictProperty + MDFillRoundFlatButton + text: "Filled" - from kivymd.app import MDApp +.. code-block:: kv - KV = ''' - MDScreen: + MDFillRoundFlatIconButton + text: "Filled with icon" + icon: "plus" - MDFloatingActionButtonSpeedDial: - id: speed_dial - data: app.data - root_button_anim: True - hint_animation: True - ''' +2.0.0 version +------------- - class Example(MDApp): - data = DictProperty() +.. note:: `MDFloatingActionButtonSpeedDial` type buttons were removed + in version `2.0.0`. - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - self.data = { - 'Python': 'language-python', - 'JS': [ - 'language-javascript', - "on_press", lambda x: print("pressed JS"), - "on_release", lambda x: print( - "stack_buttons", - self.root.ids.speed_dial.stack_buttons - ) - ], - 'PHP': [ - 'language-php', - "on_press", lambda x: print("pressed PHP"), - "on_release", self.callback - ], - 'C++': [ - 'language-cpp', - "on_press", lambda x: print("pressed C++"), - "on_release", lambda x: self.callback() - ], - } - return Builder.load_string(KV) +.. code-block:: kv - def callback(self, *args): - print(args) + MDFabButton: + icon: "plus" +.. code-block:: kv - Example().run() + MDButton: + style: "outlined" - .. tab:: Declarative python style + MDButtonText: + text: "Outlined" - .. code-block:: python +.. code-block:: kv - from kivymd.app import MDApp - from kivymd.uix.button import MDFloatingActionButtonSpeedDial - from kivymd.uix.screen import MDScreen + MDButton: + style: "outlined" + MDButtonIcon: + icon: "plus" - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return ( - MDScreen( - MDFloatingActionButtonSpeedDial( - id="speed_dial", - hint_animation=True, - root_button_anim=True, - ) - ) - ) + MDButtonText: + text: "Outlined with icon" + +.. code-block:: kv - def on_start(self): - data = { - "Python": "language-python", - "JS": [ - "language-javascript", - "on_press", lambda x: print("pressed JS"), - "on_release", lambda x: print( - "stack_buttons", - self.root.ids.speed_dial.stack_buttons - ) - ], - "PHP": [ - "language-php", - "on_press", lambda x: print("pressed PHP"), - "on_release", self.callback - ], - "C++": [ - "language-cpp", - "on_press", lambda x: print("pressed C++"), - "on_release", lambda x: self.callback() - ], - } - self.root.ids.speed_dial.data = data - - def callback(self, *args): - print(args) + MDButton: + style: "filled" + MDButtonText: + text: "Filled" - Example().run() +.. code-block:: kv + + MDButton: + style: "filled" + + MDButtonIcon: + icon: "plus" + + MDButtonText: + text: "Filled" """ from __future__ import annotations __all__ = ( - "BaseButton", "MDIconButton", - "MDFloatingActionButton", - "MDFlatButton", - "MDRaisedButton", - "MDRectangleFlatButton", - "MDRectangleFlatIconButton", - "MDRoundFlatButton", - "MDRoundFlatIconButton", - "MDFillRoundFlatButton", - "MDFillRoundFlatIconButton", - "MDTextButton", - "MDFloatingActionButtonSpeedDial", + "MDButtonText", + "MDButtonIcon", + "MDFabButton", + "MDExtendedFabButton", + "MDExtendedFabButtonIcon", + "MDExtendedFabButtonText", + "MDButton", + "BaseButton", + "BaseFabButton", ) import os -from typing import Union -from kivy.animation import Animation from kivy.clock import Clock -from kivy.core.window import Window from kivy.lang import Builder -from kivy.metrics import dp, sp +from kivy.metrics import dp from kivy.properties import ( - BooleanProperty, - BoundedNumericProperty, ColorProperty, - DictProperty, NumericProperty, - ObjectProperty, OptionProperty, - StringProperty, VariableListProperty, + ObjectProperty, ) -from kivy.uix.anchorlayout import AnchorLayout from kivy.uix.behaviors import ButtonBehavior -from kivy.uix.floatlayout import FloatLayout -from kivy.weakproxy import WeakProxy +from kivy.uix.relativelayout import RelativeLayout +from kivymd.uix.label import MDIcon, MDLabel from kivymd import uix_path -from kivymd.color_definitions import text_colors -from kivymd.font_definitions import theme_font_styles -from kivymd.material_resources import ( - FLOATING_ACTION_BUTTON_M2_ELEVATION, - FLOATING_ACTION_BUTTON_M2_OFFSET, - FLOATING_ACTION_BUTTON_M3_ELEVATION, - FLOATING_ACTION_BUTTON_M3_OFFSET, - FLOATING_ACTION_BUTTON_M3_SOFTNESS, - RAISED_BUTTON_OFFSET, - RAISED_BUTTON_SOFTNESS, -) from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import ( CommonElevationBehavior, DeclarativeBehavior, RectangularRippleBehavior, - RotateBehavior, + BackgroundColorBehavior, ) -from kivymd.uix.label import MDLabel -from kivymd.uix.tooltip import MDTooltip +from kivymd.uix.behaviors.motion_behavior import MotionExtendedFabButtonBehavior +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior with open( os.path.join(uix_path, "button", "button.kv"), encoding="utf-8" @@ -704,157 +696,129 @@ def callback(self, *args): Builder.load_string(kv_file.read()) -theme_text_color_options = ( - "Primary", - "Secondary", - "Hint", - "Error", - "Custom", - "ContrastParentBackground", -) - - -class BaseButton( - DeclarativeBehavior, - RectangularRippleBehavior, - ThemableBehavior, - ButtonBehavior, - AnchorLayout, -): +class BaseFabButton: """ - Base class for all buttons. + Implements the basic properties for the + :class:`~MDExtendedFabButton` and :class:`~MDFabButton` classes. - For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and - :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and - :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivy.uix.anchorlayout.AnchorLayout` - classes documentation. + .. versionadded:: 2.0.0 """ - padding = VariableListProperty([dp(16), dp(8), dp(16), dp(8)]) + color_map = OptionProperty( + "surface", options=("surface", "secondary", "tertiary") + ) """ - Padding between the widget box and its children, in pixels: - [padding_left, padding_top, padding_right, padding_bottom]. - - padding also accepts a two argument form [padding_horizontal, - padding_vertical] and a one argument form [padding]. + Additional color mappings. - .. versionadded:: 1.0.0 - - :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` - and defaults to [16dp, 8dp, 16dp, 8dp]. - """ + Available options are: 'surface', 'secondary', 'tertiary'. - halign = OptionProperty("center", options=("left", "center", "right")) + :attr:`color_map` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'secondary'`. """ - Horizontal anchor. - .. versionadded:: 1.0.0 - - :attr:`anchor_x` is an :class:`~kivy.properties.OptionProperty` - and defaults to 'center'. It accepts values of 'left', 'center' or 'right'. + icon_color_disabled = ColorProperty(None) """ + The icon color in (r, g, b, a) or string format of the list item when + the widget item is disabled. - valign = OptionProperty("center", options=("top", "center", "bottom")) + :attr:`icon_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - Vertical anchor. - .. versionadded:: 1.0.0 - - :attr:`anchor_y` is an :class:`~kivy.properties.OptionProperty` - and defaults to 'center'. It accepts values of 'top', 'center' or 'bottom'. + style = OptionProperty("standard", options=("standard", "small", "large")) """ + Button type. - text = StringProperty("") - """ - Button text. + Available options are: 'standard', 'small', 'large'. - :attr:`text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. + :attr:`style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'standard'`. """ - icon = StringProperty("") + fab_state = OptionProperty("collapse", options=("collapse", "expand")) """ - Button icon. + The state of the button. - :attr:`icon` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ + Available options are: 'collapse' or 'expand'. - font_style = OptionProperty("Body1", options=theme_font_styles) + :attr:`fab_state` is an :class:`~kivy.properties.OptionProperty` + and defaults to "collapse". """ - Button text font style. - Available vanilla font_style are: `'H1'`, `'H2'`, `'H3'`, `'H4'`, `'H5'`, - `'H6'`, `'Subtitle1'`, `'Subtitle2'`, `'Body1'`, `'Body2'`, `'Button'`, - `'Caption'`, `'Overline'`, `'Icon'`. - - :attr:`font_style` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Body1'`. + md_bg_color_disabled = ColorProperty(None) """ + The background color in (r, g, b, a) or string format of the list item when + the list button is disabled. - theme_text_color = OptionProperty(None, options=theme_text_color_options) + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - Button text type. Available options are: (`"Primary"`, `"Secondary"`, - `"Hint"`, `"Error"`, `"Custom"`, `"ContrastParentBackground"`). - :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` - and defaults to `None` (set by button class). + radius = VariableListProperty( + [ + dp(16), + ], + length=4, + ) """ + Canvas radius. - theme_icon_color = OptionProperty(None, options=theme_text_color_options) + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(16), dp(16), dp(16), dp(16)]`. """ - Button icon type. Available options are: (`"Primary"`, `"Secondary"`, - `"Hint"`, `"Error"`, `"Custom"`, `"ContrastParentBackground"`). - .. versionadded:: 1.0.0 - - :attr:`theme_icon_color` is an :class:`~kivy.properties.OptionProperty` - and defaults to `None` (set by button subclass). - """ - text_color = ColorProperty(None) +class BaseButton( + DeclarativeBehavior, + BackgroundColorBehavior, + RectangularRippleBehavior, + ButtonBehavior, + ThemableBehavior, + StateLayerBehavior, +): """ - Button text color in (r, g, b, a) or string format. + Base button class. - :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior` + classes documentation. """ - icon_color = ColorProperty(None) + md_bg_color_disabled = ColorProperty(None) """ - Button icon color in (r, g, b, a) or string format. + The background color in (r, g, b, a) or string format of the button when + the button is disabled. - :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - font_name = StringProperty() + shadow_radius = VariableListProperty([0, 0, 0, 0]) """ - Button text font name. + Button shadow radius. - :attr:`font_name` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. + :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. """ - font_size = NumericProperty("14sp") + md_bg_color = ColorProperty(None) """ - Button text font size. + Button background color in (r, g, b, a) or string format. - :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` - and defaults to `14sp`. + :attr:`md_bg_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - icon_size = NumericProperty() + line_color = ColorProperty(None) """ - Icon font size. - Use this parameter as the font size, that is, in sp units. - - .. versionadded:: 1.0.0 + Outlined color. - :attr:`icon_size` is a :class:`~kivy.properties.NumericProperty` + :attr:`line_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ @@ -866,1502 +830,369 @@ class BaseButton( and defaults to `1`. """ - line_color = ColorProperty(None) - """ - Line color in (r, g, b, a) or string format for button border. + def on_press(self, *args) -> None: + """Fired when the button is pressed.""" - :attr:`line_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ + self._on_press(args) - line_color_disabled = ColorProperty(None) - """ - Disabled line color in (r, g, b, a) or string format for button border. + def on_release(self, *args) -> None: + """ + Fired when the button is released + (i.e. the touch/click that pressed the button goes away). + """ - .. versionadded:: 1.0.0 + self._on_release(args) - :attr:`line_color_disabled` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - md_bg_color = ColorProperty(None) +class MDButton(BaseButton, CommonElevationBehavior, RelativeLayout): """ - Button background color in (r, g, b, a) or string format. + Base class for all buttons. - :attr:`md_bg_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ + .. versionadded:: 2.2.0 - md_bg_color_disabled = ColorProperty(None) - """ - The background color in (r, g, b, a) or string format of the button when - the button is disabled. - - :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + For more information, see in the + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~BaseButton` and + :class:`~kivy.uix.relativelayout.RelativeLayout` + classes documentation. """ - disabled_color = ColorProperty(None) + style = OptionProperty( + "elevated", options=("elevated", "filled", "tonal", "outlined", "text") + ) """ - The color of the text and icon when the button is disabled, - in (r, g, b, a) or string format. + Button type. - .. versionadded:: 1.0.0 + Available options are: 'filled', 'elevated', 'outlined', 'tonal', 'text'. - :attr:`disabled_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + :attr:`style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'elevated'`. """ - rounded_button = BooleanProperty(False) + radius = VariableListProperty( + [ + dp(20), + ] + ) """ - Should the button have fully rounded corners (e.g. like M3 buttons)? + Button radius. - .. versionadded:: 1.0.0 - - :attr:`rounded_button` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(20), dp(20), dp(20), dp(20)]`. """ - # Note - _radius must be > 0 to avoid rendering issues. - _radius = BoundedNumericProperty(dp(4), min=0.0999, errorvalue=0.1) - # Properties used for rendering. - _disabled_color = ColorProperty([0.0, 0.0, 0.0, 0.0]) - _md_bg_color = ColorProperty([0.0, 0.0, 0.0, 0.0]) - _md_bg_color_disabled = ColorProperty([0.0, 0.0, 0.0, 0.0]) - _line_color = ColorProperty([0.0, 0.0, 0.0, 0.0]) - _line_color_disabled = ColorProperty([0.0, 0.0, 0.0, 0.0]) - _theme_text_color = OptionProperty(None, options=theme_text_color_options) - _theme_icon_color = OptionProperty(None, options=theme_text_color_options) - _text_color = ColorProperty(None) - _icon_color = ColorProperty(None) - - # Defaults which can be overridden in subclasses - _min_width = NumericProperty(dp(64)) - _min_height = NumericProperty(dp(36)) - - # Default colors - set to None to use primary theme colors - _default_md_bg_color = [0.0, 0.0, 0.0, 0.0] - _default_md_bg_color_disabled = [0.0, 0.0, 0.0, 0.0] - _default_line_color = [0.0, 0.0, 0.0, 0.0] - _default_line_color_disabled = [0.0, 0.0, 0.0, 0.0] - _default_theme_text_color = StringProperty("Primary") - _default_theme_icon_color = StringProperty("Primary") - _default_text_color = ColorProperty(None) - _default_icon_color = ColorProperty(None) - - _animation_fade_bg = ObjectProperty(None, allownone=True) + # kivymd.uix.button.button.MDButtonIcon object. + _button_icon = ObjectProperty() + # kivymd.uix.button.button.MDButtonText object. + _button_text = ObjectProperty() + + _icon_left_pad = dp(16) + _spacing_between_icon_text = dp(10) + _text_right_pad = dp(24) + _text_left_pad = dp(24) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.theme_cls.bind( - primary_palette=self.set_all_colors, - theme_style=self.set_all_colors, - ) - self.bind( - md_bg_color=self.set_button_colors, - md_bg_color_disabled=self.set_button_colors, - line_color=self.set_button_colors, - line_color_disabled=self.set_button_colors, - theme_text_color=self.set_text_color, - text_color=self.set_text_color, - theme_icon_color=self.set_icon_color, - icon_color=self.set_icon_color, - disabled_color=self.set_disabled_color, - rounded_button=self.set_radius, - height=self.set_radius, - ) - Clock.schedule_once(self.set_all_colors) - Clock.schedule_once(self.set_radius) - - def set_disabled_color(self, *args): - """ - Sets the color for the icon, text and line of the button when button - is disabled. - """ - - if self.disabled: - disabled_color = ( - self.disabled_color - if self.disabled_color - else self.theme_cls.disabled_hint_text_color - ) - self._disabled_color = disabled_color - # Button icon color. - if "lbl_ic" in self.ids: - self.ids.lbl_ic.disabled_color = disabled_color - # Button text color. - if "lbl_txt" in self.ids: - self.ids.lbl_txt.disabled_color = disabled_color - else: - self._disabled_color = self._line_color - - def set_all_colors(self, *args) -> None: - """Set all button colours.""" - - self.set_button_colors() - self.set_text_color() - self.set_icon_color() - - def set_button_colors(self, *args) -> None: - """Set all button colours (except text/icons).""" - - # Set main color - _md_bg_color = ( - self.md_bg_color - or self._default_md_bg_color - or self.theme_cls.primary_color - ) - - # Set disabled color - _md_bg_color_disabled = ( - self.md_bg_color_disabled - or ( - [sum(self.md_bg_color[0:3]) / 3.0] * 3 - + [0.38 if self.theme_cls.theme_style == "Light" else 0.5] - if self.md_bg_color - else None - ) - or self._default_md_bg_color_disabled - or self.theme_cls.disabled_primary_color - ) - - # Set line color - _line_color = ( - self.line_color - or self._default_line_color - or self.theme_cls.primary_color - ) - - # Set disabled line color - _line_color_disabled = ( - self.line_color_disabled - or ( - [sum(self.line_color[0:3]) / 3.0] * 3 - + [0.38 if self.theme_cls.theme_style == "Light" else 0.5] - if self.line_color - else None + Clock.schedule_once(self.adjust_width, 0.2) + Clock.schedule_once(self.adjust_pos, 0.2) + + def adjust_pos(self, *args) -> None: + """Adjusts the pos of the button according to the content.""" + + if self._button_icon and self._button_text: + self._button_text.x = ( + self._button_icon.x + + self._spacing_between_icon_text + + self._icon_left_pad + + dp(2) ) - or self._default_line_color_disabled - or self.theme_cls.disabled_primary_color - ) - - if self.theme_cls.theme_style_switch_animation: - Animation( - _md_bg_color=_md_bg_color, - _md_bg_color_disabled=_md_bg_color_disabled, - _line_color=_line_color, - _line_color_disabled=_line_color_disabled, - d=self.theme_cls.theme_style_switch_animation_duration, - t="linear", - ).start(self) - else: - self._md_bg_color = _md_bg_color - self._md_bg_color_disabled = _md_bg_color_disabled - self._line_color = _line_color - self._line_color_disabled = _line_color_disabled - - def set_text_color(self, *args) -> None: - """ - Set _theme_text_color and _text_color based on defaults and options. - """ - - self._theme_text_color = ( - self.theme_text_color or self._default_theme_text_color - ) - if self._default_text_color == "PrimaryHue": - default_text_color = text_colors[self.theme_cls.primary_palette][ - self.theme_cls.primary_hue - ] - elif self._default_text_color == "Primary": - default_text_color = self.theme_cls.primary_color - else: - default_text_color = self.theme_cls.text_color - self._text_color = self.text_color or default_text_color - - def set_icon_color(self, *args) -> None: - """ - Set _theme_icon_color and _icon_color based on defaults and options. - """ - - self._theme_icon_color = ( - (self.theme_icon_color or self._default_theme_icon_color) - if not self.disabled - else "Custom" - ) - if self._default_icon_color == "PrimaryHue": - default_icon_color = text_colors[self.theme_cls.primary_palette][ - self.theme_cls.primary_hue - ] - elif self._default_icon_color == "Primary": - default_icon_color = self.theme_cls.primary_color - else: - default_icon_color = self.theme_cls.text_color - self._icon_color = self.icon_color or default_icon_color - - def set_radius(self, *args) -> None: - """ - Set the radius, if we are a rounded button, based on the - current height. - """ - - if self.rounded_button: - self._radius = self.height / 2 - - # Touch events that cause transparent buttons to fade to background - def on_touch_down(self, touch): - """ - Animates fade to background on press, for buttons with no - background color. - """ - - if touch.is_mouse_scrolling: - return False - elif not self.collide_point(touch.x, touch.y): - return False - elif self in touch.ud: - return False - elif self.disabled: - return False - else: - if self._md_bg_color[3] == 0.0: - self._animation_fade_bg = Animation( - duration=0.5, _md_bg_color=[0.0, 0.0, 0.0, 0.1] + elif not self._button_icon and self._button_text: + self._button_text.x = self._text_left_pad + + def adjust_width(self, *args) -> None: + """Adjusts the width of the button according to the content.""" + + if self._button_icon and self._button_text: + if self.theme_width == "Primary": + self.width = ( + self._button_icon.texture_size[0] + + self._button_text.texture_size[0] + + self._icon_left_pad + + self._spacing_between_icon_text + + self._text_right_pad + ) + elif not self._button_icon and self._button_text: + if self.theme_width == "Primary": + self.width = ( + self._button_text.texture_size[0] + + self._text_left_pad + + self._text_right_pad + ) + elif self._button_icon and not self._button_text: + if self.theme_width == "Primary": + self.width = ( + dp(48) + + self._button_icon.texture_size[0] + - self._spacing_between_icon_text ) - self._animation_fade_bg.start(self) - return super().on_touch_down(touch) - - def on_touch_up(self, touch): - """Animates return to original background on touch release.""" - - if not self.disabled and self._animation_fade_bg: - self._animation_fade_bg.stop_property(self, "_md_bg_color") - self._animation_fade_bg = None - md_bg_color = ( - self.md_bg_color - or self._default_md_bg_color - or self.theme_cls.primary_color - ) - Animation(duration=0.05, _md_bg_color=md_bg_color).start(self) - return super().on_touch_up(touch) - - def on_disabled(self, instance_button, disabled_value: bool) -> None: - if hasattr(super(), "on_disabled"): - if self.disabled is True: - Animation.cancel_all(self, "elevation") - super().on_disabled(instance_button, disabled_value) - Clock.schedule_once(self.set_disabled_color) - -class ButtonElevationBehaviour(CommonElevationBehavior): - """ - Implements elevation behavior as well as the recommended down/disabled - colors for raised buttons. + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDButtonText): + self._button_text = widget + widget.bind( + text=lambda x, y: Clock.schedule_once(self.adjust_width, 0.2) + ) + widget._button = self + elif isinstance(widget, MDButtonIcon): + self._button_icon = widget + widget._button = self + if isinstance(widget, (MDButtonIcon, MDButtonText)): + return super().add_widget(widget) - The minimum elevation for any raised button is `'1dp'`, - by default, set to `'2dp'`. + def set_properties_widget(self) -> None: + """Fired `on_release/on_press/on_enter/on_leave` events.""" - The `_elevation_raised` is automatically computed and is set to - `self.elevation + 6` each time `self.elevation` is updated. - """ + super().set_properties_widget() - _elevation_raised = NumericProperty() - _anim_raised = ObjectProperty(None, allownone=True) - _default_elevation = 2 - - def __init__(self, **kwargs): - super().__init__(**kwargs) - if self.elevation == 0: - self.elevation = self._default_elevation - if hasattr(self, "radius"): - self.bind(_radius=self.setter("radius")) - Clock.schedule_once(self.create_anim_raised) - self.on_disabled(self, self.disabled) - - def create_anim_raised(self, *args) -> None: - if self.elevation: - self._elevation_raised = self.elevation - self._anim_raised = Animation(elevation=self.elevation + 1, d=0.15) - - def on_touch_down(self, touch): if not self.disabled: - if touch.is_mouse_scrolling: - return False - if not self.collide_point(touch.x, touch.y): - return False - if self in touch.ud: - return False - if self._anim_raised and self.elevation: - self._anim_raised.start(self) - return super().on_touch_down(touch) - - def on_touch_up(self, touch): - if not self.disabled: - if self in touch.ud: - self.stop_elevation_anim() - return super().on_touch_up(touch) - return super().on_touch_up(touch) - - def stop_elevation_anim(self): - Animation.cancel_all(self, "elevation") - if self._anim_raised and self.elevation: - self.elevation = self._elevation_raised - - -class ButtonContentsText: - """Contents for :class:`~BaseButton` class consisting of a single label.""" - - -class ButtonContentsIcon: - """ - Contents for a round BaseButton consisting of an :class:`~MDIcon` class. - """ - - _min_width = NumericProperty(0) - - def on_text_color(self, instance_button, color: list) -> None: - """ - Set icon_color equal to text_color. - For backwards compatibility - can use text_color instead - of icon_color. - """ - - if color: - self.icon_color = color - - -class ButtonContentsIconText: - """ - Contents for :class:`~BaseButton` class consisting of a - :class:`~kivy.uix.boxlayout.BoxLayout` with an icon and a label. - """ - - padding = VariableListProperty([dp(12), dp(8), dp(16), dp(8)]) - """ - Padding between the widget box and its children, in pixels: - [padding_left, padding_top, padding_right, padding_bottom]. - - padding also accepts a two argument form [padding_horizontal, - padding_vertical] and a one argument form [padding]. - - .. versionadded:: 1.0.0 - - :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` - and defaults to [12dp, 8dp, 16dp, 8dp]. - """ - - -# Old MD Button classes - - -class OldButtonIconMixin: - """Backwards-compatibility for icons.""" - - icon = StringProperty("android") - - def on_icon_color(self, instance_button, color: list) -> None: - """ - If we are setting an icon color, set theme_icon_color to Custom. - For backwards compatibility (before theme_icon_color existed). - """ - - if color and (self.theme_text_color == "Custom"): - self.theme_icon_color = "Custom" - - -class MDFlatButton(BaseButton, ButtonContentsText): - """ - A flat rectangular button with (by default) no border or background. - Text is the default text color. - - For more information, see in the - :class:`~BaseButton` and :class:`~ButtonContentsText` - classes documentation. - """ - - padding = VariableListProperty([dp(8), dp(8), dp(8), dp(8)]) - """ - Padding between the widget box and its children, in pixels: - [padding_left, padding_top, padding_right, padding_bottom]. - - padding also accepts a two argument form [padding_horizontal, - padding_vertical] and a one argument form [padding]. - - .. versionadded:: 1.0.0 - - :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` - and defaults to [8dp, 8dp, 8dp, 8dp]. - """ - - -class MDRaisedButton(BaseButton, ButtonElevationBehaviour, ButtonContentsText): - """ - A flat button with (by default) a primary color fill and matching - color text. + if self._state == self.state_hover and self.focus_behavior: + if self.style == "filled": + self.elevation_level = 1 + self.shadow_softness = 0 + elif self.style == "tonal": + self.elevation_level = 1 + else: + self.elevation_level = 2 + self.shadow_softness = 2 + elif self._state == self.state_press: + if self.style == "elevated": + self.elevation_level = 2 + self.shadow_softness = 2 + elif self.style == "tonal": + self.elevation_level = 0 + elif self.style == "filled": + self.elevation_level = 0 + self.shadow_softness = 0 + elif not self._state: + if self.style == "elevated": + self.elevation_level = 1 + self.shadow_softness = 0 + elif self.style == "tonal": + self.elevation_level = 0 + elif self.style == "filled": + self.elevation_level = 0 + self.shadow_softness = 0 + + +class MDButtonText(MDLabel): + """ + The class implements the text for the :class:`~MDButton` class. For more information, see in the - :class:`~BaseButton` and - :class:`~ButtonElevationBehaviour` and - :class:`~ButtonContentsText` - classes documentation. + :class:`~kivymd.uix.label.label.MDLabel` class documentation. """ - # FIXME: Move the underlying attributes to the :class:`~BaseButton` class. - # This applies to all classes of buttons that have similar attributes. - _default_md_bg_color = None - _default_md_bg_color_disabled = None - _default_theme_text_color = "Custom" - _default_text_color = "PrimaryHue" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.shadow_softness = RAISED_BUTTON_SOFTNESS - self.shadow_offset = RAISED_BUTTON_OFFSET - # self.shadow_radius = self._radius * 2 + # kivymd.uix.button.button.MDButton object. + _button = ObjectProperty() -class MDRectangleFlatButton(BaseButton, ButtonContentsText): +class MDButtonIcon(MDIcon): """ - A flat button with (by default) a primary color border and primary - color text. + The class implements an icon for the :class:`~MDButton` class. For more information, see in the - :class:`~BaseButton` and :class:`~ButtonContentsText` - classes documentation. + :class:`~kivymd.uix.label.label.MDIcon` class documentation. """ - _default_line_color = None - _default_line_color_disabled = None - _default_theme_text_color = "Custom" - _default_text_color = "Primary" + # kivymd.uix.button.button.MDButton object. + _button = ObjectProperty() -class MDRectangleFlatIconButton( - BaseButton, OldButtonIconMixin, ButtonContentsIconText -): +class MDIconButton(RectangularRippleBehavior, ButtonBehavior, MDIcon): """ - A flat button with (by default) a primary color border, primary color text - and a primary color icon on the left. + Base class for icon buttons. For more information, see in the - :class:`~BaseButton` and - :class:`~OldButtonIconMixin` and - :class:`~ButtonContentsIconText` + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivy.uix.label.label.MDIcon` classes documentation. """ - _default_line_color = None - _default_line_color_disabled = None - _default_theme_text_color = "Custom" - _default_theme_icon_color = "Custom" - _default_text_color = "Primary" - _default_icon_color = "Primary" - - -class MDRoundFlatButton(BaseButton, ButtonContentsText): - """ - A flat button with (by default) fully rounded corners, a primary - color border and primary color text. - - For more information, see in the - :class:`~BaseButton` and :class:`~ButtonContentsText` - classes documentation. + style = OptionProperty( + "standard", options=("standard", "filled", "tonal", "outlined") + ) """ + Button type. - _default_line_color = None - _default_line_color_disabled = None - _default_theme_text_color = "Custom" - _default_text_color = "Primary" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.rounded_button = True + .. versionadded:: 2.0.0 + Available options are: 'standard', 'filled', 'tonal', 'outlined'. -class MDRoundFlatIconButton( - BaseButton, OldButtonIconMixin, ButtonContentsIconText -): + :attr:`style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'standard'`. """ - A flat button with (by default) rounded corners, a primary color border, - primary color text and a primary color icon on the left. - For more information, see in the - :class:`~BaseButton` and - :class:`~OldButtonIconMixin` and - :class:`~ButtonContentsIconText` - classes documentation. + md_bg_color_disabled = ColorProperty(None) """ + The background color in (r, g, b, a) or string format of the list item when + the list button is disabled. - _default_line_color = None - _default_line_color_disabled = None - _default_theme_text_color = "Custom" - _default_theme_icon_color = "Custom" - _default_text_color = "Primary" - _default_icon_color = "Primary" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.rounded_button = True - - -class MDFillRoundFlatButton(BaseButton, ButtonContentsText): + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - A flat button with (by default) rounded corners, a primary color fill - and primary color text. - For more information, see in the - :class:`~BaseButton` and :class:`~ButtonContentsText` - classes documentation. - """ + _line_color = ColorProperty(None) - _default_md_bg_color = None - _default_md_bg_color_disabled = None - _default_theme_text_color = "Custom" - _default_text_color = "PrimaryHue" + def on_line_color(self, instance, value) -> None: + """Fired when the values of :attr:`line_color` change.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.rounded_button = True + if not self.disabled and self.theme_line_color == "Custom": + self._line_color = value -class MDFillRoundFlatIconButton( - BaseButton, OldButtonIconMixin, ButtonContentsIconText +class MDFabButton( + BaseFabButton, + CommonElevationBehavior, + RectangularRippleBehavior, + ButtonBehavior, + MDIcon, ): """ - A flat button with (by default) rounded corners, a primary color fill, - primary color text and a primary color icon on the left. + Base class for FAB buttons. For more information, see in the - :class:`~BaseButton` and - :class:`~OldButtonIconMixin` and - :class:`~ButtonContentsIconText` + :class:`~BaseFabButton` and + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.label.label.MDIcon` classes documentation. """ - _default_md_bg_color = None - _default_md_bg_color_disabled = None - _default_theme_text_color = "Custom" - _default_theme_icon_color = "Custom" - _default_text_color = "PrimaryHue" - _default_icon_color = "PrimaryHue" + def on_press(self, *args) -> None: + """Fired when the button is pressed.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.rounded_button = True - - -class MDIconButton(BaseButton, OldButtonIconMixin, ButtonContentsIcon): - """ - A simple rounded icon button. - - For more information, see in the - :class:`~BaseButton` and - :class:`~OldButtonIconMixin` and - :class:`~ButtonContentsIcon` classes documentation. - """ - - icon = StringProperty("checkbox-blank-circle") - """ - Button icon. + self._on_press(args) - :attr:`icon` is a :class:`~kivy.properties.StringProperty` - and defaults to `'checkbox-blank-circle'`. - """ - - _min_width = NumericProperty(0) - _default_icon_pad = max(dp(48) - sp(24), 0) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.rounded_button = True - # FIXME: GraphicException: Invalid width value, must be > 0 - self.line_width = 0.001 - Clock.schedule_once(self.set_size) - - def set_size(self, interval: Union[int, float]) -> None: + def on_release(self, *args) -> None: """ - Sets the icon width/height based on the current `icon_size` - attribute, or the default value if it is zero. The icon size - is set to `(48, 48)` for an icon with the default font_size 24sp. + Fired when the button is released + (i.e. the touch/click that pressed the button goes away). """ - diameter = self._default_icon_pad + (self.icon_size or sp(24)) - self.width = diameter - self.height = diameter - - -class MDFloatingActionButton( - BaseButton, OldButtonIconMixin, ButtonElevationBehaviour, ButtonContentsIcon -): - """ - Implementation - `FAB `_ - button. - - For more information, see in the - :class:`~BaseButton` and - :class:`~OldButtonIconMixin` and - :class:`~ButtonElevationBehaviour` and - :class:`~ButtonContentsIcon` classes documentation. - """ - - type = OptionProperty("standard", options=["small", "large", "standard"]) - """ - Type of M3 button. - .. versionadded:: 1.0.0 + self._on_release(args) - Available options are: 'small', 'large', 'standard'. + def set_properties_widget(self) -> None: + """Fired `on_release/on_press/on_enter/on_leave` events.""" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-types.png - :align: center + super().set_properties_widget() - :attr:`type` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'standard'`. - """ + if not self.disabled: + if ( + self._state == self.state_hover + and self.focus_behavior + or not self._state + ): + self.elevation_level = 2 + self.shadow_softness = 2 + if self._state == self.state_press: + self.elevation_level = 3 + self.shadow_softness = 4 - _default_md_bg_color = None - _default_md_bg_color_disabled = None - _default_theme_icon_color = "Custom" - _default_icon_color = "PrimaryHue" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # FIXME: GraphicException: Invalid width value, must be > 0 - self.line_width = 0.001 - self.theme_cls.bind(material_style=self.set_size_and_radius) - Clock.schedule_once(self.set_size) - Clock.schedule_once(self.set__radius) - Clock.schedule_once(self.set_font_size) - - def set_font_size(self, *args) -> None: - if self.theme_cls.material_style == "M3": - if self.type == "large": - self.icon_size = "36sp" - else: - self.icon_size = 0 - - def set__radius(self, *args) -> None: - if self.theme_cls.material_style == "M2": - self.shadow_radius = self.height / 2 - self.elevation = FLOATING_ACTION_BUTTON_M2_ELEVATION - self.shadow_offset = FLOATING_ACTION_BUTTON_M2_OFFSET - self.rounded_button = True - else: - self.shadow_softness = FLOATING_ACTION_BUTTON_M3_SOFTNESS - self.shadow_offset = FLOATING_ACTION_BUTTON_M3_OFFSET - self.elevation = FLOATING_ACTION_BUTTON_M3_ELEVATION - self.rounded_button = False - - if self.type == "small": - self._radius = dp(12) - elif self.type == "standard": - self._radius = dp(16) - elif self.type == "large": - self._radius = dp(28) - - self.shadow_radius = self._radius - - def set_size_and_radius(self, *args) -> None: - self.set_size(args) - self.set__radius(args) - - def set_size(self, *args) -> None: - if self.theme_cls.material_style == "M2": - self.size = dp(56), dp(56) - else: - if self.type == "small": - self.size = dp(40), dp(40) - elif self.type == "standard": - self.size = dp(56), dp(56) - elif self.type == "large": - self.size = dp(96), dp(96) - - def on_type(self, instance_md_floating_action_button, type: str) -> None: - self.set_size() - self.set_font_size() - - -class MDTextButton(ButtonBehavior, MDLabel): +class MDExtendedFabButtonIcon(MDIcon): """ - Text button class. + Implements an icon for the :class:`~MDExtendedFabButton` class. - For more information, see in the - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.label.MDLabel` classes documentation. + .. versionadded:: 2.0.0 """ - color = ColorProperty(None) - """ - Button color in (r, g, b, a) or string format. - :attr:`color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - color_disabled = ColorProperty(None) +class MDExtendedFabButtonText(MDLabel): """ - Button color disabled in (r, g, b, a) or string format. + Implements the text for the class :class:`~MDExtendedFabButton' class. - :attr:`color_disabled` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + .. versionadded:: 2.0.0 """ - _color = ColorProperty(None) # last current button text color - - def animation_label(self) -> None: - def set_default_state_label(*args): - Animation(opacity=1, d=0.1, t="in_out_cubic").start(self) - - anim = Animation(opacity=0.5, d=0.2, t="in_out_cubic") - anim.bind(on_complete=set_default_state_label) - anim.start(self) - - def on_press(self, *args): - self.animation_label() - return super().on_press(*args) - - def on_disabled(self, instance_button, disabled_value) -> None: - if disabled_value: - if not self.color_disabled: - self.color_disabled = self.theme_cls.disabled_hint_text_color - self._color = self.color - self.text_color = self.color_disabled - else: - self.text_color = self._color - - -# SpeedDial classes - - -class BaseFloatingBottomButton(MDFloatingActionButton, MDTooltip): - _canvas_width = NumericProperty(0) - _padding_right = NumericProperty(0) - _bg_color = ColorProperty(None) - def set_size(self, interval: Union[int, float]) -> None: - self.width = "46dp" - self.height = "46dp" - - -class MDFloatingBottomButton(BaseFloatingBottomButton): - _bg_color = ColorProperty(None) - - -class MDFloatingRootButton(RotateBehavior, MDFloatingActionButton): - rotate_value_angle = NumericProperty(0) - - -class MDFloatingLabel(MDLabel): - bg_color = ColorProperty([0, 0, 0, 0]) - - -class MDFloatingActionButtonSpeedDial( - DeclarativeBehavior, ThemableBehavior, FloatLayout +class MDExtendedFabButton( + DeclarativeBehavior, + ThemableBehavior, + MotionExtendedFabButtonBehavior, + CommonElevationBehavior, + StateLayerBehavior, + BaseFabButton, + ButtonBehavior, + RelativeLayout, ): """ - For more information, see in the - :class:`~kivy.uix.floatlayout.FloatLayout` class documentation. + Base class for Extended FAB buttons. + + .. versionadded:: 2.0.0 For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivy.uix.floatlayout.FloatLayout` - lasses documentation. + :class:`~kivymd.uix.behaviors.motion_behavior.MotionExtendedFabButtonBehavior` and + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior` and + :class:`~BaseFabButton` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivy.uix.relativelayout.RelativeLayout` + classes documentation. :Events: - :attr:`on_open` - Called when a stack is opened. - :attr:`on_close` - Called when a stack is closed. - :attr:`on_press_stack_button` - Called at the on_press event for the stack button. - :attr:`on_release_stack_button` - Called at the on_press event for the stack button. - """ - - icon = StringProperty("plus") - """ - Root button icon name. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - icon: "pencil" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-icon.png - :align: center - - :attr:`icon` is a :class:`~kivy.properties.StringProperty` - and defaults to `'plus'`. - """ - - anchor = OptionProperty("right", option=["right"]) - """ - Stack anchor. Available options are: `'right'`. - - :attr:`anchor` is a :class:`~kivy.properties.OptionProperty` - and defaults to `'right'`. - """ - - label_text_color = ColorProperty(None) - """ - Color of floating text labels in (r, g, b, a) or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - label_text_color: "orange" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-label-text-color.png - :align: center - - :attr:`label_text_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - label_bg_color = ColorProperty([0, 0, 0, 0]) - """ - Background color of floating text labels in (r, g, b, a) or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - label_text_color: "black" - label_bg_color: "orange" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-label-bg-color.png - :align: center - - :attr:`label_bg_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. - """ - - label_radius = VariableListProperty([0], length=4) - """ - The radius of the background of floating text labels. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - label_text_color: "black" - label_bg_color: "orange" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-label-radius.png - :align: center - - :attr:`label_radius` is a :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. - """ - - data = DictProperty() - """ - Must be a dictionary. - - .. code-block:: python - - { - 'name-icon': 'Text label', - ..., - ..., - } - """ - - right_pad = BooleanProperty(False) - """ - If `True`, the background for the floating text label will increase by the - number of pixels specified in the :attr:`~right_pad_value` parameter. - - Works only if the :attr:`~hint_animation` parameter is set to `True`. - - .. rubric:: False - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - hint_animation: True - right_pad: False - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-right-pad.gif - :align: center - - .. rubric:: True - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - hint_animation: True - right_pad: True - right_pad_value: "10dp" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-right-pad-true.gif - :align: center - - :attr:`right_pad` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - right_pad_value = NumericProperty(0) - """ - See :attr:`~right_pad` parameter for more information. - - :attr:`right_pad_value` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0`. - """ - - root_button_anim = BooleanProperty(False) - """ - If ``True`` then the root button will rotate 45 degrees when the stack - is opened. - - :attr:`root_button_anim` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - opening_transition = StringProperty("out_cubic") - """ - The name of the stack opening animation type. - - :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` - and defaults to `'out_cubic'`. - """ - - closing_transition = StringProperty("out_cubic") - """ - The name of the stack closing animation type. - - :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty` - and defaults to `'out_cubic'`. - """ - - opening_transition_button_rotation = StringProperty("out_cubic") - """ - The name of the animation type to rotate the root button when opening the - stack. - - :attr:`opening_transition_button_rotation` is a :class:`~kivy.properties.StringProperty` - and defaults to `'out_cubic'`. - """ - - closing_transition_button_rotation = StringProperty("out_cubic") - """ - The name of the animation type to rotate the root button when closing the - stack. - - :attr:`closing_transition_button_rotation` is a :class:`~kivy.properties.StringProperty` - and defaults to `'out_cubic'`. - """ - - opening_time = NumericProperty(0.5) - """ - Time required for the stack to go to: attr:`state` `'open'`. - - :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - closing_time = NumericProperty(0.2) - """ - Time required for the stack to go to: attr:`state` `'close'`. - - :attr:`closing_time` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - opening_time_button_rotation = NumericProperty(0.2) - """ - Time required to rotate the root button 45 degrees during the stack - opening animation. - - :attr:`opening_time_button_rotation` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - closing_time_button_rotation = NumericProperty(0.2) - """ - Time required to rotate the root button 0 degrees during the stack - closing animation. - - :attr:`closing_time_button_rotation` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - state = OptionProperty("close", options=("close", "open")) - """ - Indicates whether the stack is closed or open. - Available options are: `'close'`, `'open'`. - - :attr:`state` is a :class:`~kivy.properties.OptionProperty` - and defaults to `'close'`. - """ - - bg_color_root_button = ColorProperty(None) - """ - Background color of root button in (r, g, b, a) or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - bg_color_root_button: "red" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-bg-color-root-button.png - :align: center - - :attr:`bg_color_root_button` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - bg_color_stack_button = ColorProperty(None) - """ - Background color of the stack buttons in (r, g, b, a) or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - bg_color_root_button: "red" - bg_color_stack_button: "red" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-bg-color-stack-button.png - :align: center - - :attr:`bg_color_stack_button` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - color_icon_stack_button = ColorProperty(None) - """ - The color icon of the stack buttons in (r, g, b, a) or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - bg_color_root_button: "red" - bg_color_stack_button: "red" - color_icon_stack_button: "white" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-color-icon-stack-button.png - :align: center - - :attr:`color_icon_stack_button` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - color_icon_root_button = ColorProperty(None) - """ - The color icon of the root button in (r, g, b, a) or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - bg_color_root_button: "red" - bg_color_stack_button: "red" - color_icon_stack_button: "white" - color_icon_root_button: self.color_icon_stack_button - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-color-icon-root-button.png - :align: center - - :attr:`color_icon_root_button` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - bg_hint_color = ColorProperty(None) - """ - Background color for the floating text of the buttons in (r, g, b, a) - or string format. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - bg_hint_color: "red" - hint_animation: True - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-bg-hint-color.png - :align: center - - :attr:`bg_hint_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - hint_animation = BooleanProperty(False) - """ - Whether to use button extension animation to display floating text. - - :attr:`hint_animation` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. + `on_collapse` + Fired when the button is collapsed. + `on_expand` + Fired when the button is expanded. """ - stack_buttons = DictProperty() - - _label_pos_y_set = False - _anim_buttons_data = {} - _anim_labels_data = {} - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.register_event_type("on_open") - self.register_event_type("on_close") - self.register_event_type("on_press_stack_button") - self.register_event_type("on_release_stack_button") - Window.bind(on_resize=self._update_pos_buttons) - - def on_open(self, *args): - """Called when a stack is opened.""" - - def on_close(self, *args): - """Called when a stack is closed.""" - - def on_leave(self, instance_button: MDFloatingBottomButton) -> None: - """Called when the mouse cursor goes outside the button of stack.""" - - if self.state == "open": - for widget in self.children: - if isinstance(widget, MDFloatingLabel) and self.hint_animation: - Animation.cancel_all(widget) - for item in self.data.items(): - if widget.text in item: - Animation( - _canvas_width=0, - _padding_right=0, - d=self.opening_time, - t=self.opening_transition, - _elevation=0, - ).start(instance_button) - Animation( - opacity=0, d=0.1, t=self.opening_transition - ).start(widget) - - def on_enter(self, instance_button: MDFloatingBottomButton) -> None: - """Called when the mouse cursor is over a button from the stack.""" - - if self.state == "open": - for widget in self.children: - if isinstance(widget, MDFloatingLabel) and self.hint_animation: - Animation.cancel_all(widget) - for item in self.data.items(): - if widget.text in item: - Animation( - _canvas_width=widget.width + dp(24), - _padding_right=self.right_pad_value - if self.right_pad - else 0, - d=self.opening_time, - t=self.opening_transition, - ).start(instance_button) - if ( - instance_button.icon - == self.data[f"{widget.text}"] - or instance_button.icon - == self.data[f"{widget.text}"][0] - ): - Animation( - opacity=1, - d=self.opening_time, - t=self.opening_transition, - ).start(widget) - else: - Animation( - opacity=0, d=0.1, t=self.opening_transition - ).start(widget) - - def on_data(self, instance_speed_dial, data: dict) -> None: - """Creates a stack of buttons.""" - - def on_data(*args): - # Bottom buttons. - for name, parameters in data.items(): - name_icon = ( - parameters if (type(parameters) is str) else parameters[0] - ) - - bottom_button = MDFloatingBottomButton( - icon=name_icon, - on_enter=self.on_enter, - on_leave=self.on_leave, - opacity=0, - ) - bottom_button.bind( - on_press=lambda x: self.dispatch("on_press_stack_button"), - on_release=lambda x: self.dispatch( - "on_release_stack_button" - ), - ) - - if "on_press" in parameters: - callback = parameters[parameters.index("on_press") + 1] - bottom_button.bind(on_press=callback) - - if "on_release" in parameters: - callback = parameters[parameters.index("on_release") + 1] - bottom_button.bind(on_release=callback) - - self.set_pos_bottom_buttons(bottom_button) - self.add_widget(bottom_button) - self.stack_buttons[name] = WeakProxy(bottom_button) - # Labels. - floating_text = name - if floating_text: - label = MDFloatingLabel(text=floating_text, opacity=0) - label.bg_color = self.label_bg_color - label.radius = self.label_radius - label.text_color = ( - self.label_text_color - if self.label_text_color - else self.theme_cls.text_color - ) - self.add_widget(label) - # Top root button. - root_button = MDFloatingRootButton(on_release=self.open_stack) - root_button.icon = self.icon - self.set_pos_root_button(root_button) - self.add_widget(root_button) - - self.clear_widgets() - self.stack_buttons = {} - self._anim_buttons_data = {} - self._anim_labels_data = {} - self._label_pos_y_set = False - Clock.schedule_once(on_data) - - def on_icon(self, instance_speed_dial, name_icon: str) -> None: - self._set_button_property(MDFloatingRootButton, "icon", name_icon) - - def on_label_text_color( - self, instance_speed_dial, color: list | str - ) -> None: - for widget in self.children: - if isinstance(widget, MDFloatingLabel): - widget.text_color = color - - def on_color_icon_stack_button( - self, instance_speed_dial, color: list - ) -> None: - self._set_button_property(MDFloatingBottomButton, "icon_color", color) - - def on_hint_animation(self, instance_speed_dial, value: bool) -> None: - for widget in self.children: - if isinstance(widget, MDFloatingLabel): - widget.md_bg_color = (0, 0, 0, 0) - - def on_bg_hint_color(self, instance_speed_dial, color: list) -> None: - setattr(MDFloatingBottomButton, "_bg_color", color) - - def on_color_icon_root_button( - self, instance_speed_dial, color: list - ) -> None: - self._set_button_property(MDFloatingRootButton, "icon_color", color) - - def on_bg_color_stack_button( - self, instance_speed_dial, color: list - ) -> None: - self._set_button_property(MDFloatingBottomButton, "md_bg_color", color) - - def on_bg_color_root_button(self, instance_speed_dial, color: list) -> None: - self._set_button_property(MDFloatingRootButton, "md_bg_color", color) - - def on_press_stack_button(self, *args) -> None: - """ - Called at the on_press event for the stack button. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - on_press_stack_button: print(*args) - - .. versionadded:: 1.1.0 - """ - - def on_release_stack_button(self, *args) -> None: - """ - Called at the on_release event for the stack button. - - .. code-block:: kv - - MDFloatingActionButtonSpeedDial: - on_release_stack_button: print(*args) - - .. versionadded:: 1.1.0 - """ - - def set_pos_labels(self, instance_floating_label: MDFloatingLabel) -> None: - """ - Sets the position of the floating labels. - Called when the application's root window is resized. - """ - - if self.anchor == "right": - instance_floating_label.x = ( - Window.width - instance_floating_label.width - dp(86) - ) - - def set_pos_root_button( - self, instance_floating_root_button: MDFloatingRootButton - ) -> None: - """ - Sets the position of the root button. - Called when the application's root window is resized. - """ - - def set_pos_root_button(*args): - if self.anchor == "right": - instance_floating_root_button.y = dp(20) - instance_floating_root_button.x = self.parent.width - ( - dp(56) + dp(20) - ) - - Clock.schedule_once(set_pos_root_button) - - def set_pos_bottom_buttons( - self, instance_floating_bottom_button: MDFloatingBottomButton - ) -> None: - """ - Sets the position of the bottom buttons in a stack. - Called when the application's root window is resized. - """ + _icon = ObjectProperty() + _label = ObjectProperty() - if self.anchor == "right": - if self.state != "open": - instance_floating_bottom_button.y = ( - instance_floating_bottom_button.height / 2 - ) - instance_floating_bottom_button.x = Window.width - ( - instance_floating_bottom_button.height - + instance_floating_bottom_button.width / 2 - ) - - def open_stack( - self, instance_floating_root_button: MDFloatingRootButton - ) -> None: - """Opens a button stack.""" - - for widget in self.children: - if isinstance(widget, MDFloatingLabel): - Animation.cancel_all(widget) - - if self.state != "open": - y = 0 - label_position = dp(54) - anim_buttons_data = {} - anim_labels_data = {} - - for widget in self.children: - if isinstance(widget, MDFloatingBottomButton): - # Sets new button positions. - y += dp(56) - widget.y = widget.y * 2 + y - if not self._anim_buttons_data: - anim_buttons_data[widget] = Animation( - opacity=1, - d=self.opening_time, - t=self.opening_transition, - ) - elif isinstance(widget, MDFloatingLabel): - # Sets new labels positions. - label_position += dp(56) - # Sets the position of signatures only once. - if not self._label_pos_y_set: - widget.y = widget.y * 2 + label_position - widget.x = Window.width - widget.width - dp(86) - if not self._anim_labels_data: - anim_labels_data[widget] = Animation( - opacity=1, d=self.opening_time - ) - elif ( - isinstance(widget, MDFloatingRootButton) - and self.root_button_anim - ): - # Rotates the root button 45 degrees. - Animation( - rotate_value_angle=-45, - d=self.opening_time_button_rotation, - t=self.opening_transition_button_rotation, - ).start(widget) - - if anim_buttons_data: - self._anim_buttons_data = anim_buttons_data - if anim_labels_data and not self.hint_animation: - self._anim_labels_data = anim_labels_data - - self.state = "open" - self.dispatch("on_open") - self.do_animation_open_stack(self._anim_buttons_data) - self.do_animation_open_stack(self._anim_labels_data) - if not self._label_pos_y_set: - self._label_pos_y_set = True - else: - self.close_stack() - - def do_animation_open_stack(self, anim_data: dict) -> None: - """ - :param anim_data: - { - : - , - : - , - ..., - } - """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_event_type("on_collapse") + self.register_event_type("on_expand") + Clock.schedule_once(self._set_text_pos, 0.5) + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDExtendedFabButtonIcon): + self._icon = widget + elif isinstance(widget, MDExtendedFabButtonText): + self._label = widget + widget.opacity = 0 + + return super().add_widget(widget) + + def on_collapse(self, *args): + """Fired when the button is collapsed.""" + + def on_expand(self, *args): + """Fired when the button is expanded.""" + + def on_fab_state(self, instance, state: str) -> None: + """Fired when the :attr:`fab_state` value changes.""" + + if state == "expand": + Clock.schedule_once(self.expand) + Clock.schedule_once(lambda x: self.dispatch("on_expand")) + elif state == "collapse": + Clock.schedule_once(self.collapse) + Clock.schedule_once(lambda x: self.dispatch("on_collapse")) + + def on__x(self, instance, value) -> None: + self._label.x = ( + self._icon.x + self._icon.texture_size[0] + dp(24) - value + ) - def on_progress(animation, widget, value): - if value >= 0.1: - animation_open_stack() - - def animation_open_stack(*args): - try: - widget = next(widgets_list) - animation = anim_data[widget] - animation.bind(on_progress=on_progress) - animation.start(widget) - except StopIteration: - pass - - widgets_list = iter(list(anim_data.keys())) - animation_open_stack() - - def close_stack(self): - """Closes the button stack.""" - - for widget in self.children: - if isinstance(widget, MDFloatingBottomButton): - Animation( - y=widget.height / 2, - d=self.closing_time, - t=self.closing_transition, - opacity=0, - ).start(widget) - elif isinstance(widget, MDFloatingLabel): - if widget.opacity > 0: - Animation(opacity=0, d=0.1).start(widget) - elif ( - isinstance(widget, MDFloatingRootButton) - and self.root_button_anim - ): - Animation( - rotate_value_angle=0, - d=self.closing_time_button_rotation, - t=self.closing_transition_button_rotation, - ).start(widget) - self.state = "close" - self.dispatch("on_close") - - def _update_pos_buttons(self, instance, width, height): - # Updates button positions when resizing screen. - for widget in self.children: - if isinstance(widget, MDFloatingBottomButton): - self.set_pos_bottom_buttons(widget) - elif isinstance(widget, MDFloatingRootButton): - self.set_pos_root_button(widget) - elif isinstance(widget, MDFloatingLabel): - self.set_pos_labels(widget) - - def _set_button_property( - self, instance, property_name: str, property_value: str | list - ): - def set_count_widget(*args): - if self.children: - for widget in self.children: - if isinstance(widget, instance): - setattr(instance, property_name, property_value) - Clock.unschedule(set_count_widget) - break - - Clock.schedule_interval(set_count_widget, 0) + def _set_text_pos(self, *args): + if self._icon and self._label: + self._label.x = self._icon.x + self._icon.texture_size[0] + dp(24) + elif not self._icon and self._label: + self._label.opacity = 1 + self.width = self._label.texture_size[0] + dp(32) + self._label.pos_hint = {"center_x": 0.5} diff --git a/kivymd/uix/card/__init__.py b/kivymd/uix/card/__init__.py index 7577fc990..9bcb032bd 100644 --- a/kivymd/uix/card/__init__.py +++ b/kivymd/uix/card/__init__.py @@ -4,5 +4,4 @@ MDCardSwipe, MDCardSwipeFrontBox, MDCardSwipeLayerBox, - MDSeparator, ) diff --git a/kivymd/uix/card/card.kv b/kivymd/uix/card/card.kv index 7ed37bd78..3669d9b9f 100644 --- a/kivymd/uix/card/card.kv +++ b/kivymd/uix/card/card.kv @@ -1,9 +1,78 @@ : - md_bg_color: app.theme_cls.divider_color + md_bg_color: + app.theme_cls.secondaryContainerColor \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color - + + shadow_radius: [dp(14), ] + shadow_color: + ( \ + self.theme_cls.shadowColor \ + if self.theme_shadow_color == "Primary" else \ + self.shadow_color \ + ) \ + if not self.disabled else \ + { \ + "filled": \ + self.theme_cls.shadowColor[:-1] + [.5] \ + if self.theme_shadow_color == "Primary" else \ + self.shadow_color[:-1] + [.5], \ + "outlined": self.theme_cls.transparentColor, \ + "elevated": self.theme_cls.shadowColor[:-1] + [.5] \ + if self.theme_shadow_color == "Primary" else \ + self.shadow_color[:-1] + [.5], + }[self.style] + shadow_offset: + ( \ + { \ + "filled": [0, 0], \ + "outlined": [0, 0], \ + "elevated": [0, -1], \ + }[self.style] \ + if not self.disabled else \ + { \ + "filled": [0, -1], \ + "outlined": [0, 0], \ + "elevated": [0, -1], \ + }[self.style] \ + ) \ + if self.theme_shadow_offset == "Primary" else self.shadow_offset + elevation_level: + ( + { \ + "filled": 0, \ + "outlined": 0, \ + "elevated": self.elevation_level if self.elevation_level else 1, \ + }[self.style] \ + ) \ + if not self.disabled else \ + { \ + "filled": 1, \ + "outlined": 0, \ + "elevated": self.elevation_level if self.elevation_level else 1, \ + }[self.style] + elevation: self.elevation_levels[self.elevation_level] + line_color: + (\ + ( \ + self.theme_cls.outlineColor \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + \ + [self.button_outlined_opacity_value_disabled_line] \ + ) \ + if self.style == "outlined" else \ + self.theme_cls.transparentColor \ + ) \ + if self.theme_line_color == "Primary" else self.line_color md_bg_color: - self.theme_cls.divider_color \ - if not root.color \ - else root.color + ( \ + { \ + "filled": app.theme_cls.surfaceContainerHighestColor, \ + "outlined": app.theme_cls.surfaceColor, \ + "elevated": app.theme_cls.surfaceContainerLowColor, \ + }[self.style] \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color \ + ) diff --git a/kivymd/uix/card/card.py b/kivymd/uix/card/card.py index 7e0bdf9ac..ab4cd5ee3 100755 --- a/kivymd/uix/card/card.py +++ b/kivymd/uix/card/card.py @@ -4,8 +4,7 @@ .. seealso:: - `Material Design spec, Cards `_ and - `Material Design 3 spec, Cards `_ + `Material Design 3 spec, Cards `_ .. rubric:: Cards contain content and actions about a single subject. @@ -18,16 +17,25 @@ - MDCardSwipe_ .. note:: :class:`~MDCard` inherited from - :class:`~kivy.uix.boxlayout.BoxLayout`. You can use all parameters and - attributes of the :class:`~kivy.uix.boxlayout.BoxLayout` class in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout`. You can use all parameters and + attributes of the :class:`~kivymd.uix.boxlayout.MDBoxLayout` class in the :class:`~MDCard` class. .. MDCard: MDCard ------ -An example of the implementation of a card in the style of material design version 3 ------------------------------------------------------------------------------------- +There are three types of cards: elevated, filled, and outlined: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/available-cards.png + :align: center + +1. Elevated card +2. Filled card +3. Outlined card + +Example +------- .. tabs:: @@ -42,10 +50,10 @@ from kivymd.uix.card import MDCard KV = ''' - - padding: 4 + + padding: "4dp" size_hint: None, None - size: "200dp", "100dp" + size: "240dp", "100dp" MDRelativeLayout: @@ -54,7 +62,6 @@ pos_hint: {"top": 1, "right": 1} MDLabel: - id: label text: root.text adaptive_size: True color: "grey" @@ -63,39 +70,31 @@ MDScreen: + theme_bg_color: "Custom" + md_bg_color: self.theme_cls.backgroundColor MDBoxLayout: id: box adaptive_size: True - spacing: "56dp" + spacing: "12dp" pos_hint: {"center_x": .5, "center_y": .5} ''' - class MD3Card(MDCard): - '''Implements a material design v3 card.''' + class MyCard(MDCard): + '''Implements a material card.''' text = StringProperty() class Example(MDApp): def build(self): - self.theme_cls.material_style = "M3" return Builder.load_string(KV) def on_start(self): - styles = { - "elevated": "#f6eeee", "filled": "#f4dedc", "outlined": "#f8f5f4" - } - for style in styles.keys(): + for style in ("elevated", "filled", "outlined"): self.root.ids.box.add_widget( - MD3Card( - line_color=(0.2, 0.2, 0.2, 0.8), - style=style, - text=style.capitalize(), - md_bg_color=styles[style], - shadow_offset=(0, -1), - ) + MyCard(style=style, text=style.capitalize()) ) @@ -114,31 +113,29 @@ def on_start(self): from kivymd.uix.screen import MDScreen - class MD3Card(MDCard): - '''Implements a material design v3 card.''' + class MyCard(MDCard): + '''Implements a material card.''' class Example(MDApp): def build(self): - self.theme_cls.material_style = "M3" return ( MDScreen( MDBoxLayout( id="box", adaptive_size=True, - spacing="56dp", + spacing="12dp", pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) + ), + theme_bg_color="Custom", + md_bg_color=self.theme_cls.backgroundColor, ) ) def on_start(self): - styles = { - "elevated": "#f6eeee", "filled": "#f4dedc", "outlined": "#f8f5f4" - } - for style in styles.keys(): + for style in ("elevated", "filled", "outlined"): self.root.ids.box.add_widget( - MD3Card( + MyCard( MDRelativeLayout( MDIconButton( icon="dots-vertical", @@ -147,22 +144,114 @@ def on_start(self): MDLabel( text=style.capitalize(), adaptive_size=True, - color="grey", pos=("12dp", "12dp"), ), ), - line_color=(0.2, 0.2, 0.2, 0.8), style=style, - text=style.capitalize(), - md_bg_color=styles[style], - shadow_offset=(0, -1), + padding="4dp", + size_hint=(None, None), + size=("240dp", "100dp"), + ripple_behavior=True, ) ) Example().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/cards-m3.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-usage.png + :align: center + +Elevated +-------- + +.. code-block:: kv + + MDCard + style: "elevated" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-elevated.png + :align: center + +Filled +------ + +.. code-block:: kv + + MDCard + style: "filled" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-filled.png + :align: center + +Outlined +-------- + +.. code-block:: kv + + MDCard + style: "outlined" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-outlined.png + :align: center + + +Customization of card +===================== + +.. code-block:: kv + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + theme_bg_color: "Custom" + md_bg_color: self.theme_cls.backgroundColor + + MDCard: + style: "elevated" + pos_hint: {"center_x": .5, "center_y": .5} + padding: "4dp" + size_hint: None, None + size: "240dp", "100dp" + # Sets custom properties. + theme_shadow_color: "Custom" + shadow_color: "green" + theme_bg_color: "Custom" + md_bg_color: "white" + md_bg_color_disabled: "grey" + theme_shadow_offset: "Custom" + shadow_offset: (1, -2) + theme_shadow_softness: "Custom" + shadow_softness: 1 + theme_elevation_level: "Custom" + elevation_level: 2 + + MDRelativeLayout: + + MDIconButton: + icon: "dots-vertical" + pos_hint: {"top": 1, "right": 1} + + MDLabel: + text: "Elevated" + adaptive_size: True + color: "grey" + pos: "12dp", "12dp" + bold: True + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.primary_palette = "Green" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-customization.png :align: center .. MDCardSwipe: @@ -196,8 +285,8 @@ class SwipeToDeleteItem(MDCardSwipe): .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sceleton-mdcard-swiper.png :align: center -End full code -------------- +Example +------- .. tabs:: @@ -212,16 +301,20 @@ class SwipeToDeleteItem(MDCardSwipe): from kivymd.uix.card import MDCardSwipe KV = ''' - + : size_hint_y: None height: content.height MDCardSwipeLayerBox: - # Content under the card. + padding: "8dp" + + MDIconButton: + icon: "trash-can" + pos_hint: {"center_y": .5} + on_release: app.remove_item(root) MDCardSwipeFrontBox: - # Content of card. OneLineListItem: id: content text: root.text @@ -230,39 +323,34 @@ class SwipeToDeleteItem(MDCardSwipe): MDScreen: - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - elevation: 4 - title: "MDCardSwipe" + MDScrollView: - MDScrollView: - scroll_timeout : 100 - - MDList: - id: md_list - padding: 0 + MDList: + id: md_list + padding: 0 ''' class SwipeToDeleteItem(MDCardSwipe): - '''Card with `swipe-to-delete` behavior.''' - text = StringProperty() class Example(MDApp): - def build(self): + def __init__(self, **kwargs): + super().__init__(**kwargs) self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) + self.screen = Builder.load_string(KV) - def on_start(self): - '''Creates a list of cards.''' + def build(self): + return self.screen + def remove_item(self, instance): + self.screen.ids.md_list.remove_widget(instance) + + def on_start(self): for i in range(20): - self.root.ids.md_list.add_widget( + self.screen.ids.md_list.add_widget( SwipeToDeleteItem(text=f"One-line item {i}") ) @@ -274,14 +362,12 @@ def on_start(self): .. code-block:: python from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.card import ( MDCardSwipe, MDCardSwipeLayerBox, MDCardSwipeFrontBox ) from kivymd.uix.list import MDList, OneLineListItem from kivymd.uix.screen import MDScreen from kivymd.uix.scrollview import MDScrollView - from kivymd.uix.toolbar import MDTopAppBar class Example(MDApp): @@ -290,20 +376,13 @@ def build(self): self.theme_cls.primary_palette = "Orange" return ( MDScreen( - MDBoxLayout( - MDTopAppBar( - elevation=4, - title="MDCardSwipe", + MDScrollView( + MDList( + id="md_list", + padding=0, ), - MDScrollView( - MDList( - id="md_list", - ), - id="scroll", - scroll_timeout=100, - ), - id="box", - orientation="vertical", + id="scroll", + scroll_timeout=100, ), ) ) @@ -312,7 +391,7 @@ def on_start(self): '''Creates a list of cards.''' for i in range(20): - self.root.ids.box.ids.scroll.ids.md_list.add_widget( + self.root.ids.scroll.ids.md_list.add_widget( MDCardSwipe( MDCardSwipeLayerBox(), MDCardSwipeFrontBox( @@ -323,7 +402,7 @@ def on_start(self): ) ), size_hint_y=None, - height="52dp", + height="48dp", ) ) @@ -355,19 +434,11 @@ def on_start(self): # By default, the parameter is "hand" - type_swipe: "hand" + type_swipe: "hand" # "auto" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hand-mdcard-swipe.gif :align: center -.. code-block:: kv - - : - type_swipe: "auto" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/auto-mdcard-swipe.gif - :align: center - Removing an item using the ``type_swipe = "auto"`` parameter ------------------------------------------------------------ @@ -410,9 +481,10 @@ def on_swipe_complete(self, instance): def on_swipe_complete(self, instance): self.root.ids.box.ids.scroll.ids.md_list.remove_widget(instance) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/autodelete-mdcard-swipe.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/auto-mdcard-swipe.gif :align: center + Add content to the bottom layer of the card ------------------------------------------- @@ -431,256 +503,20 @@ def on_swipe_complete(self, instance): pos_hint: {"center_y": .5} on_release: app.remove_item(root) -End full code -------------- - -.. tabs:: - - .. tab:: Declarative KV styles - - .. code-block:: python - - from kivy.lang import Builder - from kivy.properties import StringProperty - - from kivymd.app import MDApp - from kivymd.uix.card import MDCardSwipe - - KV = ''' - : - size_hint_y: None - height: content.height - - MDCardSwipeLayerBox: - padding: "8dp" - - MDIconButton: - icon: "trash-can" - pos_hint: {"center_y": .5} - on_release: app.remove_item(root) - - MDCardSwipeFrontBox: - - OneLineListItem: - id: content - text: root.text - _no_ripple_effect: True - - - MDScreen: - - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - elevation: 4 - title: "MDCardSwipe" - - MDScrollView: - - MDList: - id: md_list - padding: 0 - ''' - - - class SwipeToDeleteItem(MDCardSwipe): - text = StringProperty() - - - class Example(MDApp): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - self.screen = Builder.load_string(KV) - - def build(self): - return self.screen - - def remove_item(self, instance): - self.screen.ids.md_list.remove_widget(instance) - - def on_start(self): - for i in range(20): - self.screen.ids.md_list.add_widget( - SwipeToDeleteItem(text=f"One-line item {i}") - ) - - - Example().run() - - .. tab:: Decralative python styles - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.button import MDIconButton - from kivymd.uix.card import ( - MDCardSwipe, MDCardSwipeLayerBox, MDCardSwipeFrontBox - ) - from kivymd.uix.list import MDList, OneLineListItem - from kivymd.uix.screen import MDScreen - from kivymd.uix.scrollview import MDScrollView - from kivymd.uix.toolbar import MDTopAppBar - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return ( - MDScreen( - MDBoxLayout( - MDTopAppBar( - elevation=4, - title="MDCardSwipe", - ), - MDScrollView( - MDList( - id="md_list", - ), - id="scroll", - scroll_timeout=100, - ), - id="box", - orientation="vertical", - ), - ) - ) - - def on_start(self): - '''Creates a list of cards.''' - - for i in range(20): - self.root.ids.box.ids.scroll.ids.md_list.add_widget( - MDCardSwipe( - MDCardSwipeLayerBox( - MDIconButton( - icon="trash-can", - pos_hint={"center_y": 0.5}, - on_release=self.remove_item, - ), - ), - MDCardSwipeFrontBox( - OneLineListItem( - id="content", - text=f"One-line item {i}", - _no_ripple_effect=True, - ) - ), - size_hint_y=None, - height="52dp", - ) - ) - - def remove_item(self, instance): - self.root.ids.box.ids.scroll.ids.md_list.remove_widget( - instance.parent.parent - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/handdelete-mdcard-swipe.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdcard-swipe-content.png :align: center - -Focus behavior --------------- - -.. code-block:: kv - - MDCard: - focus_behavior: True - -.. tabs:: - - .. tab:: Declarative KV styles - - .. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDScreen: - - MDCard: - size_hint: .7, .4 - focus_behavior: True - pos_hint: {"center_x": .5, "center_y": .5} - md_bg_color: "darkgrey" - unfocus_color: "darkgrey" - focus_color: "grey" - elevation: 6 - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - - Example().run() - - .. tab:: Declarative python styles - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.card import MDCard - from kivymd.uix.screen import MDScreen - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return ( - MDScreen( - MDCard( - size_hint=(0.7, 0.4), - focus_behavior=True, - pos_hint={"center_x": 0.5, "center_y": 0.5}, - md_bg_color="darkgrey", - unfocus_color="darkgrey", - focus_color="grey", - elevation=6, - ), - ) - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-focus.gif - :align: center - -Ripple behavior ---------------- - -.. code-block:: kv - - MDCard: - ripple_behavior: True - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-behavior.gif - :align: center - """ +from __future__ import annotations + __all__ = ( "MDCard", "MDCardSwipe", "MDCardSwipeFrontBox", "MDCardSwipeLayerBox", - "MDSeparator", ) import os -from typing import Union from kivy.animation import Animation from kivy.clock import Clock @@ -688,67 +524,32 @@ def build(self): from kivy.metrics import dp from kivy.properties import ( BooleanProperty, - ColorProperty, NumericProperty, OptionProperty, StringProperty, VariableListProperty, + ColorProperty, ) +from kivy.uix.behaviors import ButtonBehavior from kivy.uix.boxlayout import BoxLayout -from kivy.utils import get_color_from_hex from kivymd import uix_path -from kivymd.color_definitions import colors -from kivymd.material_resources import ( - CARD_STYLE_ELEVATED_M3_ELEVATION, - CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION, -) from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import ( - BackgroundColorBehavior, CommonElevationBehavior, DeclarativeBehavior, RectangularRippleBehavior, + BackgroundColorBehavior, ) -from kivymd.uix.behaviors.focus_behavior import FocusBehavior +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.relativelayout import MDRelativeLayout with open( os.path.join(uix_path, "card", "card.kv"), encoding="utf-8" ) as kv_file: - Builder.load_string(kv_file.read()) - - -class MDSeparator(MDBoxLayout): - """ - A separator line. - - For more information, see in the - :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. - """ - - color = ColorProperty(None) - """ - Separator color in (r, g, b, a) or string format. - - :attr:`color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.on_orientation() - - def on_orientation(self, *args) -> None: - self.size_hint = ( - (1, None) if self.orientation == "horizontal" else (None, 1) - ) - if self.orientation == "horizontal": - self.height = dp(1) - else: - self.width = dp(1) + Builder.load_string(kv_file.read(), filename="MDCard.kv") class MDCard( @@ -756,34 +557,28 @@ class MDCard( MDAdaptiveWidget, ThemableBehavior, BackgroundColorBehavior, - RectangularRippleBehavior, CommonElevationBehavior, - FocusBehavior, + RectangularRippleBehavior, + StateLayerBehavior, + ButtonBehavior, BoxLayout, ): """ Card class. For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivymd.uix.MDAdaptiveWidget` and :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivymd.uix.behaviors.BackgroundColorBehavior` and - :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and - :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and - :class:`~kivymd.uix.behaviors.FocusBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and :class:`~kivy.uix.boxlayout.BoxLayout` and classes documentation. """ - focus_behavior = BooleanProperty(False) - """ - Using focus when hovering over a card. - - :attr:`focus_behavior` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - ripple_behavior = BooleanProperty(False) """ Use ripple effect for card. @@ -792,17 +587,17 @@ class MDCard( and defaults to `False`. """ - radius = VariableListProperty([dp(6), dp(6), dp(6), dp(6)]) + radius = VariableListProperty([dp(16), dp(16), dp(16), dp(16)]) """ Card radius by default. .. versionadded:: 1.0.0 :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` - and defaults to `[dp(6), dp(6), dp(6), dp(6)]`. + and defaults to `[dp(16), dp(16), dp(16), dp(16)]`. """ - style = OptionProperty(None, options=("filled", "elevated", "outlined")) + style = OptionProperty("filled", options=("filled", "elevated", "outlined")) """ Card type. @@ -811,65 +606,78 @@ class MDCard( Available options are: 'filled', 'elevated', 'outlined'. :attr:`style` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'elevated'`. + and defaults to `None`. """ - _bg_color_map = ( - get_color_from_hex(colors["Light"]["CardsDialogs"]), - get_color_from_hex(colors["Dark"]["CardsDialogs"]), - [1.0, 1.0, 1.0, 0.0], - ) + md_bg_color_disabled = ColorProperty(None) + """ + The background color in (r, g, b, a) or string format of the card when + the card is disabled. + + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.theme_cls.bind( - material_style=self.set_style, theme_style=self.update_md_bg_color - ) - Clock.schedule_once(self.set_style) Clock.schedule_once( lambda x: self.on_ripple_behavior(0, self.ripple_behavior) ) - self.update_md_bg_color(self, self.theme_cls.theme_style) - def update_md_bg_color(self, instance_card, theme_style: str) -> None: - if self.md_bg_color in self._bg_color_map: - self.md_bg_color = get_color_from_hex( - colors[theme_style]["CardsDialogs"] - ) - - def set_style(self, *args) -> None: - self.set_radius() - self.set_elevation() - self.set_line_color() - - def set_line_color(self) -> None: - if self.theme_cls.material_style == "M3": - if self.style == "elevated" or self.style == "filled": - self.line_color = [0, 0, 0, 0] - - def set_elevation(self) -> None: - if self.theme_cls.material_style == "M3": - if self.style == "outlined" or self.style == "filled": - self.elevation = CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION - elif self.style == "elevated": - self.elevation = CARD_STYLE_ELEVATED_M3_ELEVATION - - def set_radius(self) -> None: - if ( - self.radius == [dp(6), dp(6), dp(6), dp(6)] - and self.theme_cls.material_style == "M3" - ): - self.radius = [dp(16), dp(16), dp(16), dp(16)] - elif ( - self.radius == [dp(16), dp(16), dp(16), dp(16)] - and self.theme_cls.material_style == "M2" - ): - self.radius = [dp(6), dp(6), dp(6), dp(6)] - - def on_ripple_behavior( - self, interval: Union[int, float], value_behavior: bool - ) -> None: - self._no_ripple_effect = False if value_behavior else True + def on_press(self, *args) -> None: + """Fired when the button is pressed.""" + + self._on_press(args) + + def on_release(self, *args) -> None: + """ + Fired when the button is released + (i.e. the touch/click that pressed the button goes away). + """ + + self._on_release(args) + + def on_ripple_behavior(self, interval: int | float, value: bool) -> None: + """Fired when the :attr:`ripple_behavior` value changes.""" + + self.ripple_effect = not self.ripple_effect + + def set_properties_widget(self) -> None: + """Fired `on_release/on_press/on_enter/on_leave` events.""" + + super().set_properties_widget() + + if not self.disabled: + if self._state == self.state_hover and self.focus_behavior: + self._elevation_level = self.elevation_level + self._shadow_softness = self.shadow_softness + self._bg_color = self.md_bg_color + + if self.style in ["filled", "outlined"]: + if self.theme_elevation_level == "Primary": + self.elevation_level = 0 + if self.theme_shadow_softness == "Primary": + self.shadow_softness = 0 + else: + if self.theme_elevation_level == "Primary": + self.elevation_level = 2 + if self.theme_shadow_softness == "Primary": + self.shadow_softness = 1 + if self.theme_shadow_offset == "Primary": + self.shadow_offset = [0, -2] + elif self._state == self.state_press: + if self.theme_elevation_level == "Primary": + self.elevation_level = 1 + if self.theme_shadow_softness == "Primary": + self.shadow_softness = 0 + elif not self._state: + if self.theme_elevation_level == "Primary": + self.elevation_level = 1 + if self.theme_shadow_softness == "Primary": + self.shadow_softness = 0 + if self.theme_shadow_offset == "Primary": + self.shadow_offset = [0, -1] + self.md_bg_color = self._bg_color class MDCardSwipe(MDRelativeLayout): @@ -881,7 +689,7 @@ class MDCardSwipe(MDRelativeLayout): :Events: :attr:`on_swipe_complete` - Called when a swipe of card is completed. + Fired when a swipe of card is completed. """ open_progress = NumericProperty(0.0) @@ -993,16 +801,14 @@ def __init__(self, *args, **kwargs): self.register_event_type("on_swipe_complete") super().__init__(*args, **kwargs) - def add_widget(self, widget, index=0, canvas=None): - if isinstance(widget, (MDCardSwipeFrontBox, MDCardSwipeLayerBox)): - return super().add_widget(widget) - def on_swipe_complete(self, *args): - """Called when a swipe of card is completed.""" + """Fired when a swipe of card is completed.""" def on_anchor( self, instance_swipe_to_delete_item, anchor_value: str ) -> None: + """Fired when the value of :attr:`anchor` changes.""" + if anchor_value == "right": self.open_progress = 1.0 else: @@ -1011,6 +817,8 @@ def on_anchor( def on_open_progress( self, instance_swipe_to_delete_item, progress_value: float ) -> None: + """Fired when the value of :attr:`open_progress` changes.""" + def on_open_progress(*args): if self.anchor == "left": self.children[0].x = self.width * progress_value @@ -1043,7 +851,7 @@ def on_touch_up(self, touch): if self.collide_point(touch.x, touch.y): if not self._to_closed: self._opens_process = False - self.complete_swipe() + self._complete_swipe() return super().on_touch_up(touch) def on_touch_down(self, touch): @@ -1053,18 +861,9 @@ def on_touch_down(self, touch): Clock.schedule_once(self.close_card, self.closing_interval) return super().on_touch_down(touch) - def complete_swipe(self) -> None: - expr = ( - self.open_progress <= self.max_swipe_x - if self.anchor == "left" - else self.open_progress >= self.max_swipe_x - ) - if expr: - Clock.schedule_once(self.close_card, self.closing_interval) - else: - self.open_card() - def open_card(self) -> None: + """Animates the opening of the card.""" + if self.type_swipe == "hand": swipe_x = ( self.max_opened_x @@ -1081,11 +880,28 @@ def open_card(self) -> None: self.state = "opened" def close_card(self, *args) -> None: + """Animates the closing of the card.""" + anim = Animation(x=0, t=self.closing_transition, d=self.opening_time) anim.bind(on_complete=self._reset_open_progress) anim.start(self.children[0]) self.state = "closed" + def add_widget(self, widget, index=0, canvas=None): + if isinstance(widget, (MDCardSwipeFrontBox, MDCardSwipeLayerBox)): + return super().add_widget(widget) + + def _complete_swipe(self) -> None: + expr = ( + self.open_progress <= self.max_swipe_x + if self.anchor == "left" + else self.open_progress >= self.max_swipe_x + ) + if expr: + Clock.schedule_once(self.close_card, self.closing_interval) + else: + self.open_card() + def _on_swipe_complete(self, *args): self.dispatch("on_swipe_complete") @@ -1104,4 +920,9 @@ class MDCardSwipeFrontBox(MDCard): class MDCardSwipeLayerBox(MDBoxLayout): - pass + """ + Card swipe back box. + + For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` + class documentation. + """ diff --git a/kivymd/uix/chip/__init__.py b/kivymd/uix/chip/__init__.py index fc6a3a29c..7246a4142 100644 --- a/kivymd/uix/chip/__init__.py +++ b/kivymd/uix/chip/__init__.py @@ -1 +1,7 @@ -from .chip import MDChip, MDChipText # NOQA F401 +from .chip import ( + MDChip, + MDChipText, + MDChipLeadingIcon, + MDChipTrailingIcon, + MDChipLeadingAvatar, +) # NOQA F401 diff --git a/kivymd/uix/chip/chip.kv b/kivymd/uix/chip/chip.kv index e008dd25e..17d4ce948 100644 --- a/kivymd/uix/chip/chip.kv +++ b/kivymd/uix/chip/chip.kv @@ -1,29 +1,91 @@ + + icon_color: + ( \ + { \ + "filter": app.theme_cls.onSurfaceVariantColor, \ + "suggestion": app.theme_cls.onSurfaceVariantColor, \ + "input": app.theme_cls.onSurfaceVariantColor, \ + "assist": app.theme_cls.primaryColor, \ + }[self._type] \ + if self.theme_icon_color == "Primary" else self.icon_color \ + ) \ + if not root.disabled else self.disabled_color + disabled_color: + { \ + "filter": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_icon], \ + "suggestion": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_icon], \ + "input": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_icon], \ + "assist": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_icon], \ + }[self._type] \ + if not self.icon_color_disabled else self.icon_color_disabled + + + + font_style: "Label" + role: "large" + text_color: + ( \ + { \ + "filter": app.theme_cls.onSurfaceVariantColor, \ + "suggestion": app.theme_cls.onSurfaceVariantColor, \ + "input": app.theme_cls.onSurfaceVariantColor, \ + "assist": app.theme_cls.onSurfaceColor, \ + }[self._type] \ + if root.theme_text_color == "Primary" else root.text_color \ + ) \ + if not root.disabled else self.disabled_color + disabled_color: + { \ + "filter": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_text], \ + "suggestion": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_text], \ + "input": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_text], \ + "assist": app.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_text], \ + }[self._type] \ + if not self.text_color_disabled else self.text_color_disabled + + size_hint_y: None height: "32dp" adaptive_width: True radius: - 16 \ + dp(16) \ if self.radius == [0, 0, 0, 0] else \ - (max(self.radius) if max(self.radius) < self.height / 2 else 16) - md_bg_color: + (max(self.radius) if max(self.radius) < self.height / 2 else dp(16)) + line_color: ( \ ( \ - app.theme_cls.bg_darkest \ - if app.theme_cls.theme_style == "Light" else \ - app.theme_cls.bg_light \ + self.theme_cls.outlineColor \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + \ + [self.chip_opacity_value_disabled_container] \ ) \ - if not self._origin_md_bg_color else \ - self._origin_md_bg_color + if self.type != "filter" else \ + self.theme_cls.transparentColor \ ) \ - 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 \ + if self.theme_line_color == "Primary" else \ + self._line_color if not self.disabled else \ + ( \ + self.line_color_disabled \ + if self.line_color_disabled else \ + self._line_color \ ) + md_bg_color: + { \ + "filter": self.theme_cls.surfaceContainerLowColor, \ + "suggestion": self.theme_cls.surfaceContainerLowColor, \ + "input": self.theme_cls.surfaceContainerLowColor, \ + "assist": self.theme_cls.surfaceContainerLowColor, \ + }[self.type] \ + if self.theme_bg_color == "Primary" else self.md_bg_color LeadingIconContainer: id: leading_icon_container diff --git a/kivymd/uix/chip/chip.py b/kivymd/uix/chip/chip.py index c9f359310..cb72148a8 100755 --- a/kivymd/uix/chip/chip.py +++ b/kivymd/uix/chip/chip.py @@ -4,7 +4,7 @@ .. seealso:: - `Material Design spec, Chips `_ + `Material Design 3 spec, Chips `_ .. rubric:: Chips can show multiple interactive elements together in the same area, such as a list of selectable movie times, or a series of email @@ -17,6 +17,25 @@ Usage ----- +.. code-block:: kv + + MDChip: + + MDChipLeadingAvatar: # MDChipLeadingIcon + + MDChipText: + + MDChipTrailingIcon: + +Anatomy +======= + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-anatomy.png + :align: center + +Example +------- + .. tabs:: .. tab:: Declarative KV style @@ -75,34 +94,6 @@ def build(self): .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip.png :align: center -Anatomy -------- - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/anatomy-chip.png - :align: center - -1. Container -2. Label text -3. Leading icon or image (optional) -4. Trailing remove icon (optional, input & filter chips only) - -Container ---------- - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/radius-chip.png - :align: center - -All chips are slightly rounded with an 8dp corner. - -Shadows and elevation ---------------------- - -Chip containers can be elevated if the placement requires protection, such as -on top of an image. - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadows-elevation-chip.png - :align: center - The following types of chips are available: ------------------------------------------- @@ -641,7 +632,7 @@ def build(self): API break ========= -1.1.1 version +1.2.0 version ------------- .. code-block:: python @@ -670,7 +661,7 @@ def on_release_chip(self, instance_check): Test().run() -1.2.0 version +2.0.0 version ------------- .. code-block:: python @@ -724,7 +715,6 @@ def on_release_chip(self, instance_check): BooleanProperty, ColorProperty, OptionProperty, - StringProperty, VariableListProperty, ) from kivy.uix.behaviors import ButtonBehavior @@ -738,6 +728,7 @@ def on_release_chip(self, instance_check): ScaleBehavior, TouchBehavior, ) +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.label import MDIcon, MDLabel @@ -750,6 +741,29 @@ def on_release_chip(self, instance_check): class BaseChipIcon( CircularRippleBehavior, ScaleBehavior, ButtonBehavior, MDIcon ): + icon_color = ColorProperty(None) + """ + Button icon color in (r, g, b, a) or string format. + + :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_color_disabled = ColorProperty(None) + """ + The icon color in (r, g, b, a) or string format of the chip when + the chip is disabled. + + .. versionadded:: 2.0.0 + + :attr:`icon_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _type = OptionProperty( + "suggestion", options=["assist", "filter", "input", "suggestion"] + ) + def __init__(self, **kwargs): super().__init__(**kwargs) self.ripple_scale = 1.5 @@ -760,7 +774,8 @@ def adjust_icon_size(self, *args) -> None: # icon size according to the standards of material design version 3. if ( self.font_name == "Icons" - and self.theme_cls.font_styles["Icon"][1] == self.font_size + and self.theme_cls.font_styles["Icon"]["large"]["font-size"] + == self.font_size ): self.font_size = ( "18sp" @@ -792,10 +807,10 @@ class MDChipLeadingAvatar(BaseChipIcon): Implements the leading avatar for the chip. For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.label.MDIcon` + :class:`~kivymd.uix.label.label.MDIcon` classes documentation. """ @@ -805,10 +820,10 @@ class MDChipLeadingIcon(BaseChipIcon): Implements the leading icon for the chip. For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.label.MDIcon` + :class:`~kivymd.uix.label.label.MDIcon` classes documentation. """ @@ -818,10 +833,10 @@ class MDChipTrailingIcon(BaseChipIcon): Implements the trailing icon for the chip. For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.label.MDIcon` + :class:`~kivymd.uix.label.label.MDIcon` classes documentation. """ @@ -831,9 +846,24 @@ class MDChipText(MDLabel): Implements the label for the chip. For more information, see in the - :class:`~kivymd.uix.label.MDLabel` classes documentation. + :class:`~kivymd.uix.label.label.MDLabel` classes documentation. """ + text_color_disabled = ColorProperty(None) + """ + The text color in (r, g, b, a) or string format of the chip when + the chip is disabled. + + .. versionadded:: 2.0.0 + + :attr:`text_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _type = OptionProperty( + "suggestion", options=["assist", "filter", "input", "suggestion"] + ) + class MDChip( MDBoxLayout, @@ -841,16 +871,17 @@ class MDChip( ButtonBehavior, CommonElevationBehavior, TouchBehavior, + StateLayerBehavior, ): """ Chip class. For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` and - :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and - :class:`~kivymd.uix.behaviors.TouchBehavior` + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.touch_behavior.TouchBehavior` classes documentation. """ @@ -862,23 +893,13 @@ class MDChip( and defaults to `[dp(8), dp(8), dp(8), dp(8)]`. """ - text = StringProperty(deprecated=True) - """ - Chip text. - - .. deprecated:: 1.2.0 - - :attr:`text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - type = OptionProperty( "suggestion", options=["assist", "filter", "input", "suggestion"] ) """ Type of chip. - .. versionadded:: 1.2.0 + .. versionadded:: 2.0.0 Available options are: `'assist'`, `'filter'`, `'input'`, `'suggestion'`. @@ -886,74 +907,6 @@ class MDChip( and defaults to `'suggestion'`. """ - icon_left = StringProperty(deprecated=True) - """ - Chip left icon. - - .. versionadded:: 1.0.0 - - .. deprecated:: 1.2.0 - - :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - icon_right = StringProperty(deprecated=True) - """ - Chip right icon. - - .. versionadded:: 1.0.0 - - .. deprecated:: 1.2.0 - - :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - text_color = ColorProperty(None, deprecated=True) - """ - Chip's text color in (r, g, b, a) or string format. - - .. deprecated:: 1.2.0 - - :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - icon_right_color = ColorProperty(None, deprecated=True) - """ - Chip's right icon color in (r, g, b, a) or string format. - - .. versionadded:: 1.0.0 - - .. deprecated:: 1.2.0 - - :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - icon_left_color = ColorProperty(None, deprecated=True) - """ - Chip's left icon color in (r, g, b, a) or string format. - - .. versionadded:: 1.0.0 - - .. deprecated:: 1.2.0 - - :attr:`icon_left_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - icon_check_color = ColorProperty(None) - """ - Chip's check icon color in (r, g, b, a) or string format. - - .. versionadded:: 1.0.0 - - :attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - active = BooleanProperty(False) """ Whether the check is marked or not. @@ -969,12 +922,23 @@ class MDChip( The background color of the chip in the marked state in (r, g, b, a) or string format. - .. versionadded:: 1.2.0 + .. versionadded:: 2.0.0 :attr:`selected_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ + line_color_disabled = ColorProperty(None) + """ + The color of the outline in the disabled state + + .. versionadded:: 2.0.0 + + :attr:`line_color_disabled` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _line_color = ColorProperty(None) _current_md_bg_color = ColorProperty(None) # A flag that disallow ripple animation of the chip # at the time of clicking the chip icons. @@ -986,11 +950,19 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def on_long_touch(self, *args) -> None: + """Fired when the widget is pressed for a long time.""" + if self.type == "filter": self.active = not self.active + def on_line_color(self, instance, value) -> None: + """Fired when the values of :attr:`line_color` change.""" + + if not self.disabled: + self._line_color = value + def on_type(self, instance, value: str) -> None: - """Called when the values of :attr:`type` change.""" + """Fired when the values of :attr:`type` change.""" def adjust_padding(*args): """ @@ -1069,7 +1041,14 @@ def complete_anim_ripple(self, *args) -> None: self.set_chip_bg_color( self.selected_color if self.selected_color - else self.theme_cls.primary_color + else { + "filter": self.theme_cls.surfaceContainerLowColor, + "suggestion": self.theme_cls.surfaceContainerLowColor, + "input": self.theme_cls.surfaceContainerLowColor, + "assist": self.theme_cls.surfaceContainerLowColor, + }[self.type] + if self.theme_bg_color == "Primary" + else self.md_bg_color ) else: if ( @@ -1125,11 +1104,26 @@ def set_chip_bg_color(self, color: list | str) -> None: Animation(md_bg_color=color, d=0.2).start(self) self._anim_complete = not self._anim_complete - def on_press(self, *args): + def on_press(self, *args) -> None: + """Fired when the button is pressed.""" + if self.active: self.active = False + self._on_press(args) + + def on_release(self, *args) -> None: + """ + Fired when the button is released + (i.e. the touch/click that pressed the button goes away). + """ + + self._on_release(args) + def add_widget(self, widget, *args, **kwargs): + def set_type(*args): + widget._type = self.type + def add_icon_leading_trailing(container): if len(container.children): type_icon = ( @@ -1213,6 +1207,7 @@ def add_icon_leading_trailing(container): container.add_widget(widget) if isinstance(widget, MDChipText): + Clock.schedule_once(set_type) widget.adaptive_size = True widget.pos_hint = {"center_y": 0.5} if self.type == "suggestion": @@ -1221,12 +1216,14 @@ def add_icon_leading_trailing(container): lambda x: self.ids.label_container.add_widget(widget) ) elif isinstance(widget, (MDChipLeadingIcon, MDChipLeadingAvatar)): + Clock.schedule_once(set_type) Clock.schedule_once( lambda x: add_icon_leading_trailing( self.ids.leading_icon_container ) ) elif isinstance(widget, MDChipTrailingIcon): + Clock.schedule_once(set_type) Clock.schedule_once( lambda x: add_icon_leading_trailing( self.ids.trailing_icon_container @@ -1236,6 +1233,10 @@ def add_icon_leading_trailing(container): widget, (LabelTextContainer, LeadingIconContainer, TrailingIconContainer), ): + if isinstance( + widget, (LeadingIconContainer, TrailingIconContainer) + ): + Clock.schedule_once(set_type) return super().add_widget(widget) def _set_allow_chip_ripple( diff --git a/kivymd/uix/datatables/datatables.kv b/kivymd/uix/datatables/datatables.kv index b5d81a7ad..59fa9b441 100644 --- a/kivymd/uix/datatables/datatables.kv +++ b/kivymd/uix/datatables/datatables.kv @@ -3,23 +3,23 @@ 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 \ - ) +# 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) @@ -57,7 +57,7 @@ if root.theme_cls.theme_style == "Dark" else \ (0, 0, 0, 1) - MDSeparator: + MDDivider: @@ -83,7 +83,7 @@ if root.theme_cls.theme_style == "Dark" else \ (0, 0, 0, 1) - MDSeparator: + MDDivider: id: separator @@ -109,10 +109,10 @@ 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 +# md_bg_color: +# root.theme_cls.bg_light \ +# if not root.background_color_header \ +# else root.background_color_header MDBoxLayout: orientation: "vertical" @@ -132,7 +132,7 @@ CellHeader: id: first_cell - MDSeparator: + MDDivider: diff --git a/kivymd/uix/datatables/datatables.py b/kivymd/uix/datatables/datatables.py index 44748a551..bf3452c3d 100644 --- a/kivymd/uix/datatables/datatables.py +++ b/kivymd/uix/datatables/datatables.py @@ -58,11 +58,12 @@ from kivymd import uix_path from kivymd.effects.stiffscroll import StiffScrollEffect -from kivymd.material_resources import ( - DATA_TABLE_ELEVATION, - DATA_TABLE_OFFSET, - DATA_TABLE_SOFTNESS, -) + +# from kivymd.material_resources import ( +# DATA_TABLE_ELEVATION, +# DATA_TABLE_OFFSET, +# DATA_TABLE_SOFTNESS, +# ) from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import HoverBehavior from kivymd.uix.boxlayout import MDBoxLayout @@ -1307,7 +1308,7 @@ def build(self): and defaults to `False`. """ - elevation = NumericProperty(DATA_TABLE_ELEVATION) + elevation = NumericProperty(0) """ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation` attribute. @@ -1327,7 +1328,7 @@ def build(self): and defaults to `[6]`. """ - shadow_softness = NumericProperty(DATA_TABLE_SOFTNESS) + shadow_softness = NumericProperty(0) """ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness` attribute. @@ -1349,7 +1350,7 @@ def build(self): and defaults to `2`. """ - shadow_offset = ListProperty(DATA_TABLE_OFFSET) + shadow_offset = ListProperty([0, 0]) """ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset` attribute. diff --git a/kivymd/uix/dialog/__init__.py b/kivymd/uix/dialog/__init__.py index 40adbe46f..9fc91ded4 100644 --- a/kivymd/uix/dialog/__init__.py +++ b/kivymd/uix/dialog/__init__.py @@ -1 +1,8 @@ -from .dialog import BaseDialog, MDDialog # NOQA F401 +from .dialog import ( + MDDialog, + MDDialogIcon, + MDDialogHeadlineText, + MDDialogSupportingText, + MDDialogContentContainer, + MDDialogButtonContainer, +) # NOQA F401 diff --git a/kivymd/uix/dialog/dialog.kv b/kivymd/uix/dialog/dialog.kv index 96e01a375..42eb3ae22 100644 --- a/kivymd/uix/dialog/dialog.kv +++ b/kivymd/uix/dialog/dialog.kv @@ -1,90 +1,114 @@ -#:import images_path kivymd.images_path - - - - 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 - - - - shadow_color: 0.0, 0.0, 0.0, 0.0 - elevation: 0 - shadow_softness: 0 - shadow_offset: 0, 0 - - - - DialogContainer: - id: container - orientation: "vertical" + radius: root.radius + size_hint_min_x: "280dp" + size_hint_max_x: "560dp" + pos_hint: {"center_x": .5, "center_y": .5} + size_hint_y: None + height: container.height + theme_shadow_color: "Custom" + shadow_color: self.theme_cls.transparentColor + focus_behavior: False + + RelativeLayout: 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" + height: container.height 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 + id: container + orientation: "vertical" 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 + height: self.minimum_height + button_container.height + dp(24) + spacing: "16dp" + padding: "24dp" + y: button_container.height + dp(24) + + AnchorLayout: + id: icon_container + size_hint_y: None + anchor_x: "center" + height: self.children[0].height if self.children else 0 + + BoxLayout: + id: headline_container + size_hint_y: None + height: self.minimum_height + + BoxLayout: + id: supporting_text_container + size_hint_y: None + height: self.minimum_height + + BoxLayout: + id: content_container + size_hint_y: None + height: self.minimum_height BoxLayout: - id: spacer_bottom_box + id: button_container 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" + y: content_container.y + + + + size_hint: None, None + size: "24dp", "24dp" + theme_font_size: "Custom" + font_size: "24sp" + icon_color: + self.theme_cls.secondaryColor \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color \ + if self.icon_color else self.theme_cls.transparentColor \ + ) + + + + adaptive_height: True + halign: "center" + font_style: "Headline" + role: "small" + markup: True + color: + self.theme_cls.onSurfaceColor \ + if self.theme_text_color == "Primary" else ( \ + self.text_color \ + if self.text_color != self.theme_cls.transparentColor else \ + self.theme_cls.onSurfaceColor \ + ) + + + + adaptive_height: True + halign: "center" + font_style: "Body" + role: "medium" + markup: True + text_color: + self.theme_cls.onSurfaceVariantColor \ + if self.theme_text_color == "Primary" else ( \ + self.text_color \ + if self.text_color != self.theme_cls.transparentColor else \ + self.theme_cls.onSurfaceVariantColor \ + ) + + + + size_hint_y: None + height: self.minimum_height + + + + size_hint_y: None + height: self.minimum_height + padding: "24dp", 0, "24dp", 0 + + + + canvas: + Color: + rgba: self.color[:-1] + [self.alpha] + Rectangle: + pos: self.pos + size: self.size diff --git a/kivymd/uix/dialog/dialog.py b/kivymd/uix/dialog/dialog.py index 9f2db53a6..a16dca1f4 100755 --- a/kivymd/uix/dialog/dialog.py +++ b/kivymd/uix/dialog/dialog.py @@ -4,182 +4,156 @@ .. seealso:: - `Material Design spec, Dialogs `_ + `Material Design spec, Dialogs `_ -.. rubric:: Dialogs inform users about a task and can contain critical - information, require decisions, or involve multiple tasks. +.. rubric:: Dialogs provide important prompts in a user flow. -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialogs.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-preview.png :align: center -Usage ------ +- Use dialogs to make sure users act on information +- Two types: basic and full-screen (full-screen not provided in KivyMD) +- Should be dedicated to completing a single task +- Can also display information relevant to the task +- Commonly used to confirm high-risk actions like deleting progress + +Anatomy +======= + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-anatomy.png + :align: center + +Example +======= .. code-block:: python from kivy.lang import Builder + from kivy.uix.widget import Widget from kivymd.app import MDApp - from kivymd.uix.button import MDFlatButton - from kivymd.uix.dialog import MDDialog + from kivymd.uix.button import MDButton, MDButtonText + from kivymd.uix.dialog import ( + MDDialog, + MDDialogIcon, + MDDialogHeadlineText, + MDDialogSupportingText, + MDDialogButtonContainer, + MDDialogContentContainer, + ) + from kivymd.uix.divider import MDDivider + from kivymd.uix.list import ( + MDListItem, + MDListItemLeadingIcon, + MDListItemSupportingText, + ) KV = ''' - MDFloatLayout: + MDScreen: + md_bg_color: self.theme_cls.backgroundColor - MDFlatButton: - text: "ALERT DIALOG" + MDButton: pos_hint: {'center_x': .5, 'center_y': .5} on_release: app.show_alert_dialog() + + MDButtonText: + text: "Show 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, + MDDialog( + # ----------------------------Icon----------------------------- + MDDialogIcon( + icon="refresh", + ), + # -----------------------Headline text------------------------- + MDDialogHeadlineText( + text="Reset settings?", + ), + # -----------------------Supporting text----------------------- + MDDialogSupportingText( + text="This will reset your app preferences back to their " + "default settings. The following accounts will also " + "be signed out:", + ), + # -----------------------Custom content------------------------ + MDDialogContentContainer( + MDDivider(), + MDListItem( + MDListItemLeadingIcon( + icon="gmail", + ), + MDListItemSupportingText( + text="KivyMD-library@yandex.com", + ), + theme_bg_color="Custom", + md_bg_color=self.theme_cls.transparentColor, + ), + MDListItem( + MDListItemLeadingIcon( + icon="gmail", ), - MDFlatButton( - text="DISCARD", - theme_text_color="Custom", - text_color=self.theme_cls.primary_color, + MDListItemSupportingText( + text="kivydevelopment@gmail.com", ), - ], - ) - self.dialog.open() + theme_bg_color="Custom", + md_bg_color=self.theme_cls.transparentColor, + ), + MDDivider(), + orientation="vertical", + ), + # ---------------------Button container------------------------ + MDDialogButtonContainer( + Widget(), + MDButton( + MDButtonText(text="Cancel"), + style="text", + ), + MDButton( + MDButtonText(text="Accept"), + style="text", + ), + spacing="8dp", + ), + # ------------------------------------------------------------- + ).open() Example().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/alert-dialog.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-example.gif :align: center -""" -__all__ = ("MDDialog", "BaseDialog") +.. warning:: Do not try to use the MDDialog widget in KV files. -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. +API break +========= - .. code-block:: python +1.2.0 version +------------- - [...] - 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) +.. code-block:: python + from kivy.uix.widget import Widget -class MDDialog(BaseDialog): - """ - Dialog class. + from kivymd.app import MDApp + from kivymd.uix.button import MDFlatButton + from kivymd.uix.dialog import MDDialog - 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 + class Example(MDApp): + def build(self): + return Widget() - [...] - self.dialog = MDDialog( - title="Reset settings?", + def on_start(self): + MDDialog( + title="Discard draft?", buttons=[ MDFlatButton( text="CANCEL", @@ -187,30 +161,35 @@ class MDDialog(BaseDialog): text_color=self.theme_cls.primary_color, ), MDFlatButton( - text="ACCEPT", + text="DISCARD", theme_text_color="Custom", text_color=self.theme_cls.primary_color, ), ], - ) - [...] + ).open() - .. 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 `''`. - """ + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-api-break-1-2-0.png + :align: center + +.. code-block:: python + + from kivy.uix.widget import Widget + + from kivymd.app import MDApp + from kivymd.uix.button import MDFlatButton + from kivymd.uix.dialog import MDDialog - text = StringProperty() - """ - Text dialog. - .. code-block:: python + class Example(MDApp): + def build(self): + return Widget() - [...] - self.dialog = MDDialog( - title="Reset settings?", + def on_start(self): + MDDialog( + title="Discard draft?", text="This will reset your device to its default factory settings.", buttons=[ MDFlatButton( @@ -219,198 +198,266 @@ class MDDialog(BaseDialog): text_color=self.theme_cls.primary_color, ), MDFlatButton( - text="ACCEPT", + text="DISCARD", theme_text_color="Custom", text_color=self.theme_cls.primary_color, ), ], - ) - [...] + ).open() - .. 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 `''`. - """ + Example().run() - buttons = ListProperty() - """ - List of button objects for dialog. - Objects must be inherited from :class:`~kivymd.uix.button.BaseButton` class. +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/2-dialog-api-break-1-2-0.png + :align: center - .. code-block:: python +.. code-block:: python - [...] - self.dialog = MDDialog( - text="Discard draft?", - buttons=[ - MDFlatButton(text="CANCEL"), MDRaisedButton(text="DISCARD"), + from kivy.lang import Builder + from kivy.properties import StringProperty + from kivy.uix.widget import Widget + + from kivymd import images_path + from kivymd.app import MDApp + from kivymd.uix.dialog import MDDialog + from kivymd.uix.list import OneLineAvatarListItem + + KV = ''' + + + ImageLeftWidget: + source: root.source + ''' + + + class Item(OneLineAvatarListItem): + divider = None + source = StringProperty() + + + class Example(MDApp): + def build(self): + Builder.load_string(KV) + return Widget() + + def on_start(self): + MDDialog( + title="Set backup account", + type="simple", + items=[ + Item(text="user01@gmail.com", source=f"{images_path}/logo/kivymd-icon-128.png"), + Item(text="user02@gmail.com", source="data/logo/kivy-icon-128.png"), ], - ) - [...] + ).open() - .. 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 `[]`. - """ + Example().run() - items = ListProperty() - """ - List of items objects for dialog. - Objects must be inherited from :class:`~kivymd.uix.list.BaseListItem` class. +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/3-dialog-api-break-1-2-0.png + :align: center - With type 'simple' - ~~~~~~~~~~~~~~~~~~ +2.2.0 version +------------- - .. code-block:: python +.. code-block:: python - from kivy.lang import Builder - from kivy.properties import StringProperty + from kivy.uix.widget import Widget - from kivymd.app import MDApp - from kivymd.uix.dialog import MDDialog - from kivymd.uix.list import OneLineAvatarListItem + from kivymd.uix.widget import MDWidget + from kivymd.app import MDApp + from kivymd.uix.button import MDButton, MDButtonText + from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogButtonContainer - KV = ''' - - ImageLeftWidget: - source: root.source + class Example(MDApp): + def build(self): + return MDWidget(md_bg_color=self.theme_cls.backgroundColor) + def on_start(self): + MDDialog( + MDDialogHeadlineText( + text="Discard draft?", + halign="left", + ), + MDDialogButtonContainer( + Widget(), + MDButton( + MDButtonText(text="Cancel"), + style="text", + ), + MDButton( + MDButtonText(text="Discard"), + style="text", + ), + spacing="8dp", + ), + ).open() - MDFloatLayout: - MDFlatButton: - text: "ALERT DIALOG" - pos_hint: {'center_x': .5, 'center_y': .5} - on_release: app.show_simple_dialog() - ''' + Example().run() +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-api-break-2-2-0.png + :align: center - class Item(OneLineAvatarListItem): - divider = None - source = StringProperty() +.. code-block:: python + from kivy.uix.widget import Widget - class Example(MDApp): - dialog = None + from kivymd.uix.widget import MDWidget + from kivymd.app import MDApp + from kivymd.uix.button import MDButton, MDButtonText + from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogButtonContainer - 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() + class Example(MDApp): + def build(self): + return MDWidget(md_bg_color=self.theme_cls.backgroundColor) - Example().run() + def on_start(self): + MDDialog( + MDDialogHeadlineText( + text="Discard draft?", + halign="left", + ), + MDDialogSupportingText( + text="This will reset your device to its default factory settings.", + halign="left", + ), + MDDialogButtonContainer( + Widget(), + MDButton( + MDButtonText(text="Cancel"), + style="text", + ), + MDButton( + MDButtonText(text="Discard"), + style="text", + ), + spacing="8dp", + ), + ).open() - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-items.png - :align: center - With type 'confirmation' - ~~~~~~~~~~~~~~~~~~~~~~~~ + Example().run() - .. code-block:: python +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/2-dialog-api-break-2-2-0.png + :align: center + +.. code-block:: python - from kivy.lang import Builder + from kivymd import images_path + from kivymd.uix.widget import MDWidget + from kivymd.app import MDApp + from kivymd.uix.dialog import ( + MDDialog, + MDDialogHeadlineText, + MDDialogContentContainer, + ) + from kivymd.uix.list import ( + MDListItem, + MDListItemLeadingAvatar, + MDListItemSupportingText, + ) + + + class Example(MDApp): + def build(self): + return MDWidget(md_bg_color=self.theme_cls.backgroundColor) + + def on_start(self): + MDDialog( + MDDialogHeadlineText( + text="Set backup account", + halign="left", + ), + MDDialogContentContainer( + MDListItem( + MDListItemLeadingAvatar( + source=f"{images_path}/logo/kivymd-icon-128.png", + ), + MDListItemSupportingText( + text="user01@gmail.com", + ), + theme_bg_color="Custom", + md_bg_color=self.theme_cls.transparentColor, + ), + MDListItem( + MDListItemLeadingAvatar( + source="data/logo/kivy-icon-128.png", + ), + MDListItemSupportingText( + text="user01@gmail.com", + ), + theme_bg_color="Custom", + md_bg_color=self.theme_cls.transparentColor, + ), + orientation="vertical", + ), + ).open() - from kivymd.app import MDApp - from kivymd.uix.button import MDFlatButton - from kivymd.uix.dialog import MDDialog - from kivymd.uix.list import OneLineAvatarIconListItem - KV = ''' - - on_release: root.set_icon(check) + Example().run() - CheckboxLeftWidget: - id: check - group: "check" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/3-dialog-api-break-2-2-0.png + :align: center +""" - MDFloatLayout: +__all__ = [ + "MDDialog", + "MDDialogIcon", + "MDDialogHeadlineText", + "MDDialogSupportingText", + "MDDialogContentContainer", + "MDDialogButtonContainer", +] - MDFlatButton: - text: "ALERT DIALOG" - pos_hint: {'center_x': .5, 'center_y': .5} - on_release: app.show_confirmation_dialog() - ''' +import os +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + VariableListProperty, + NumericProperty, + ColorProperty, + ObjectProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.widget import Widget - class ItemConfirm(OneLineAvatarIconListItem): - divider = None +from kivymd.uix.card import MDCard +from kivymd.uix.label import MDIcon, MDLabel +from kivymd import uix_path +from kivymd.material_resources import DEVICE_TYPE +from kivymd.uix.behaviors import MotionDialogBehavior, DeclarativeBehavior - 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 +with open( + os.path.join(uix_path, "dialog", "dialog.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) - class Example(MDApp): - dialog = None +class MDDialog(MDCard, MotionDialogBehavior): + """ + Dialog class. - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) + For more information, see in the + :class:`~kivymd.uix.card.card.MDCard` and + :class:`~kivymd.uix.behaviors.motion_behavior.MotionDialogBehavior` + classes documentation. - 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 `[]`. + :Events: + `on_pre_open`: + Fired before the MDDialog is opened. When this event is fired + MDDialog is not yet added to window. + `on_open`: + Fired when the MDDialog is opened. + `on_pre_dismiss`: + Fired before the MDDialog is closed. + `on_dismiss`: + Fired when the MDDialog is closed. If the callback returns True, + the dismiss will be canceled. """ width_offset = NumericProperty(dp(48)) @@ -421,270 +468,149 @@ def show_confirmation_dialog(self): 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() + radius = VariableListProperty(dp(28), lenght=4) """ - Custom content class. This attribute is only available when :attr:`type` is - set to `'custom'`. - - .. tabs:: - - .. tab:: Declarative KV style + Dialog corners rounding value. - .. 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 = ''' - - 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'`. + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(28), dp(28), dp(28), dp(28)]`. """ - md_bg_color = ColorProperty(None) + scrim_color = ColorProperty([0, 0, 0, 0.5]) """ - Background color in the (r, g, b, a) or string format. + Color for scrim in (r, g, b, a) or string format. - :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + :attr:`scrim_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0.5]`. """ - _scroll_height = NumericProperty("28dp") - _spacer_top = NumericProperty("24dp") + _scrim = ObjectProperty() # kivymd.uix.dialog.dialog.MDDialogScrim object + _is_open = False # is the dialog currently open or closed. - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_event_type("on_open") + self.register_event_type("on_pre_open") + self.register_event_type("on_dismiss") + self.register_event_type("on_pre_dismiss") + self.opacity = 0 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, + self.size_hint_max_x = max( + 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 add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDDialogIcon): + self.ids.icon_container.add_widget(widget) + elif isinstance(widget, MDDialogHeadlineText): + self.ids.headline_container.add_widget(widget) + elif isinstance(widget, MDDialogSupportingText): + self.ids.supporting_text_container.add_widget(widget) + elif isinstance(widget, MDDialogContentContainer): + self.ids.content_container.add_widget(widget) + elif isinstance(widget, MDDialogButtonContainer): + self.ids.button_container.add_widget(widget) + else: + return super().add_widget(widget) + + def open(self) -> None: + """Show the dialog.""" + + if self._is_open: + return + + self.dispatch("on_pre_open") + self._is_open = True - def update_items(self, items: list) -> None: - self.ids.box_items.clear_widgets() - self.items = items - self.create_items() + if not self._scrim: + self._scrim = MDDialogScrim(color=self.scrim_color) - def on_open(self) -> None: - # TODO: Add scrolling text. - self.height = self.ids.container.height + Window.add_widget(self._scrim) + Window.add_widget(self) super().on_open() + self.dispatch("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 on_pre_open(self, *args) -> None: + """Fired when a dialog pre opened.""" - def edit_padding_for_item(self, instance_item) -> None: - instance_item.ids._left_container.x = 0 - instance_item._txt_left_pad = "56dp" + def on_open(self, *args) -> None: + """Fired when a dialog opened.""" - 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 + def on_dismiss(self, *args) -> None: + """Fired when a dialog dismiss.""" - 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) + def on_pre_dismiss(self, *args) -> None: + """Fired when a dialog pre-dismiss.""" + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + self.dismiss() + return True + super().on_touch_down(touch) + return True + + def dismiss(self, *args) -> None: + """Closes the dialog.""" + + self.dispatch("on_pre_dismiss") + super().on_dismiss() + self._is_open = False + self.dispatch("on_dismiss") + + +class MDDialogIcon(MDIcon): + """ + The class implements an icon. + + For more information, see in the + :class:`~kivymd.uix.label.label.MDIcon` class documentation. + """ + + +class MDDialogHeadlineText(MDLabel): + """ + The class implements the headline text. + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ + + +class MDDialogSupportingText(MDLabel): + """ + The class implements the supporting text. + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ + + +class MDDialogContentContainer(DeclarativeBehavior, BoxLayout): + """ + The class implements the container for custom widgets. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation. + """ + + +class MDDialogButtonContainer(DeclarativeBehavior, BoxLayout): + """ + The class implements a container for placing dialog buttons. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation. + """ - 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) +class MDDialogScrim(Widget): + color = ColorProperty(None) + alpha = NumericProperty(0) diff --git a/kivymd/uix/divider/__init__.py b/kivymd/uix/divider/__init__.py new file mode 100644 index 000000000..91a40756d --- /dev/null +++ b/kivymd/uix/divider/__init__.py @@ -0,0 +1 @@ +from .divider import MDDivider # NOQA F401 diff --git a/kivymd/uix/divider/divider.kv b/kivymd/uix/divider/divider.kv new file mode 100644 index 000000000..071ea9070 --- /dev/null +++ b/kivymd/uix/divider/divider.kv @@ -0,0 +1,10 @@ + + canvas: + Color: + rgba: + app.theme_cls.outlineVariantColor \ + if not self.color else \ + self.color + Rectangle: + size: self.size + pos: self.pos diff --git a/kivymd/uix/divider/divider.py b/kivymd/uix/divider/divider.py new file mode 100644 index 000000000..0d29be8b9 --- /dev/null +++ b/kivymd/uix/divider/divider.py @@ -0,0 +1,49 @@ +# TODO: Add doc strings. + +import os + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ColorProperty +from kivy.uix.boxlayout import BoxLayout + +from kivymd import uix_path + +with open( + os.path.join(uix_path, "divider", "divider.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDDivider(BoxLayout): + """ + A separator line. + + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivy.uix.boxlayout.BoxLayout` class documentation. + """ + + color = ColorProperty(None) + """ + Separator color in (r, g, b, a) or string format. + + :attr:`color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.on_orientation() + + def on_orientation(self, *args) -> None: + """Fired when the values of :attr:`orientation` change.""" + + self.size_hint = ( + (1, None) if self.orientation == "horizontal" else (None, 1) + ) + if self.orientation == "horizontal": + self.height = dp(1) + else: + self.width = dp(1) diff --git a/kivymd/uix/floatlayout.py b/kivymd/uix/floatlayout.py index b64ddcfc3..7832d34f2 100644 --- a/kivymd/uix/floatlayout.py +++ b/kivymd/uix/floatlayout.py @@ -13,7 +13,7 @@ FloatLayout: canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor RoundedRectangle: pos: self.pos size: self.size @@ -26,7 +26,7 @@ MDFloatLayout: radius: [25, 0, 0, 0] - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor .. Warning:: For a :class:`~kivy.uix.floatlayout.FloatLayout`, the ``minimum_size`` attributes are always 0, so you cannot use @@ -37,13 +37,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDFloatLayout( - DeclarativeBehavior, ThemableBehavior, FloatLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + FloatLayout, + MDAdaptiveWidget, ): """ - Float layout class. For more information, see in the - :class:`~kivy.uix.floatlayout.FloatLayout` class documentation. + Float layout class. + + For more information see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/gridlayout.py b/kivymd/uix/gridlayout.py index 96bfd79cb..c7d8c973b 100644 --- a/kivymd/uix/gridlayout.py +++ b/kivymd/uix/gridlayout.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -28,7 +28,7 @@ MDGridLayout: adaptive_height: True - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor Available options are: ---------------------- @@ -87,13 +87,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDGridLayout( - DeclarativeBehavior, ThemableBehavior, GridLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + GridLayout, + MDAdaptiveWidget, ): """ - Grid layout class. For more information, see in the - :class:`~kivy.uix.gridlayout.GridLayout` class documentation. + Grid layout class. + + For more information see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.gridlayout.GridLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/label/label.kv b/kivymd/uix/label/label.kv index 7fba34825..7550e52fd 100644 --- a/kivymd/uix/label/label.kv +++ b/kivymd/uix/label/label.kv @@ -2,67 +2,36 @@ - disabled_color: self.theme_cls.disabled_hint_text_color text_size: (self.width if not self.adaptive_width else None) \ if not self.adaptive_size else None, \ None + color: + self.text_color \ + if self.text_color else \ + self.theme_cls.onSurfaceColor + disabled_color: + app.theme_cls.onSurfaceColor[:-1] + \ + [self.label_opacity_value_disabled_text] + font_size: + self.theme_cls.font_styles[self.font_style][self.role]["font-size"] \ + if self.theme_font_size == "Primary" else self.font_size + line_height: + self.theme_cls.font_styles[self.font_style][self.role]["line-height"] \ + if self.theme_line_height == "Primary" else self.line_height + font_name: + self.theme_cls.font_styles[self.font_style][self.role]["font-name"] \ + if self.theme_font_name == "Primary" else self.font_name -: - canvas: - Color: - rgba: (1, 1, 1, 1) if self.source else (0, 0, 0, 0) - Rectangle: - group: "rectangle" - source: self.source if self.source else None - pos: - self.pos \ - if not self.source else \ - (self.x - self._size[0] / 2, self.y) - size: - self._size \ - if self.source else \ - self.size - + font_style: "Icon" text: u"{}".format(md_icons[root.icon]) if root.icon in md_icons else "blank" source: None if root.icon in md_icons else root.icon + adaptive_size: True + color: + self.icon_color \ + if self.icon_color else \ + self.theme_cls.onSurfaceVariantColor - # Badge icon. - MDLabel: - id: badge - font_style: "Icon" - adaptive_size: True - opposite_icon_color: True - color: root.badge_icon_color - text: - u"{}".format(md_icons[root.badge_icon]) \ - if root.badge_icon in md_icons else \ - "" - pos: - root.x + root.width / 2 + self.width / 2 - dp(6), \ - root.y + self.texture_size[1] / 2 + dp(6) - font_size: - ( \ - root.font_size / 1.5 \ - if not root.badge_font_size else \ - root.badge_font_size \ - ) \ - if root.badge_icon and root.badge_icon != "blank" else 0 - canvas.before: - Color: - rgba: - ( \ - root.badge_bg_color \ - if root.badge_bg_color else \ - app.theme_cls.error_color \ - ) \ - if root.badge_icon else \ - (0, 0, 0, 0) - RoundedRectangle: - group: "badge" - radius: [self.width / 2,] - pos: self.pos - size: self.size diff --git a/kivymd/uix/label/label.py b/kivymd/uix/label/label.py index 73bd46b68..bac48d66d 100755 --- a/kivymd/uix/label/label.py +++ b/kivymd/uix/label/label.py @@ -2,7 +2,7 @@ Components/Label ================ -.. rubric:: The :class:`MDLabel` widget is for rendering text. +.. rubric:: The `MDLabel` widget is for rendering text. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/label.png :align: center @@ -14,9 +14,8 @@ MDLabel ------- -Class :class:`MDLabel` inherited from the :class:`~kivy.uix.label.Label` class -but for :class:`MDLabel` the ``text_size`` parameter is ``(self.width, None)`` -and default is positioned on the left: +Example +------- .. tabs:: @@ -30,20 +29,21 @@ KV = ''' MDScreen: + md_bg_color: self.theme_cls.backgroundColor MDLabel: text: "MDLabel" + halign: "center" ''' - class Test(MDApp): + class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) - Test().run() + Example().run() .. tab:: Declarative Python style @@ -59,137 +59,118 @@ def build(self): class Test(MDApp): def build(self): self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" return ( MDScreen( MDLabel( - text="MDLabel" - ) + text="MDLabel", + halign="center", + ), + md_bg_color=self.theme_cls.backgroundColor, ) ) Test().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-to-left.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/label-example.png :align: center -.. Note:: See :attr:`~kivy.uix.label.Label.halign` - and :attr:`~kivy.uix.label.Label.valign` attributes - of the :class:`~kivy.uix.label.Label` class +To use a custom color for :class:`~MDLabel`, use a theme `'Custom'`. +After that, you can specify the desired color in the ``text_color`` parameter: .. code-block:: kv - MDLabel: - text: "MDLabel" - halign: "center" + MDLabel: + text: "Custom color" + halign: "center" + theme_text_color: "Custom" + text_color: "red" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-to-center.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-custom-color.png :align: center -:class:`~MDLabel` color: ------------------------- - -:class:`~MDLabel` provides standard color themes for label color management: - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.label import MDLabel - - KV = ''' - MDBoxLayout: - orientation: "vertical" - ''' - - - class Test(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - screen = Builder.load_string(KV) - - # Names of standard color themes. - for name_theme in [ - "Primary", - "Secondary", - "Hint", - "Error", - "ContrastParentBackground", - ]: - screen.add_widget( - MDLabel( - text=name_theme, - halign="center", - theme_text_color=name_theme, - ) - ) - return screen +:class:`~MDLabel` provides standard font styles for labels. To do this, +specify the name of the desired style in the :attr:`~MDLabel.font_style` +and :attr:`~MDLabel.role` parameters: +.. code-block:: kv - Test().run() + MDLabel: + text: "Display, role - 'large'" + font_style: "Display" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-theme-text-color.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style-display-large.png :align: center -To use a custom color for :class:`~MDLabel`, use a theme `'Custom'`. -After that, you can specify the desired color in the ``rgba`` format -in the ``text_color`` parameter: - .. code-block:: kv MDLabel: - text: "Custom color" - halign: "center" - theme_text_color: "Custom" - text_color: "blue" + text: "Display, role - 'small'" + font_style: "Display" + role: "small" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-custom-color.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style-display-small.png :align: center -:class:`~MDLabel` provides standard font styles for labels. To do this, -specify the name of the desired style in the :attr:`~MDLabel.font_style` -parameter: +.. seealso:: + + `Material Design spec, Typography `_ + + +All styles +---------- .. code-block:: python from kivy.lang import Builder - from kivymd.app import MDApp - from kivymd.uix.label import MDLabel from kivymd.font_definitions import theme_font_styles + from kivymd.app import MDApp KV = ''' - MDScrollView: - - MDList: - id: box - spacing: "8dp" + MDScreen: + md_bg_color: self.theme_cls.backgroundColor + + MDRecycleView: + id: rv + key_viewclass: 'viewclass' + key_size: 'height' + + RecycleBoxLayout: + padding: dp(10) + spacing: dp(10) + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: "vertical" ''' - class Test(MDApp): + class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" - screen = Builder.load_string(KV) - - # Names of standard font styles. - for name_style in theme_font_styles[:-1]: - screen.ids.box.add_widget( - MDLabel( - text=f"{name_style} style", - halign="center", - font_style=name_style, - adaptive_height=True, - ) - ) - return screen + return Builder.load_string(KV) + def on_start(self): + for style in theme_font_styles: + if style != "Icon": + for role in theme_font_styles[style]: + font_size = int(theme_font_styles[style][role]["font-size"]) + self.root.ids.rv.data.append( + { + "viewclass": "MDLabel", + "text": f"{style} {role} {font_size} sp", + "adaptive_height": "True", + "font_style": style, + "role": role, + } + ) - Test().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style.png + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/label-font-style-preview.png :align: center Highlighting and copying labels @@ -210,11 +191,12 @@ def build(self): KV = ''' MDScreen: + md_bg_color: self.theme_cls.backgroundColor MDLabel: adaptive_size: True pos_hint: {"center_x": .5, "center_y": .5} - text: "MDLabel" + text: "Do a double click on me" allow_selection: True padding: "4dp", "4dp" ''' @@ -223,7 +205,6 @@ def build(self): class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) @@ -233,7 +214,7 @@ def build(self): .. code-block:: python - from kivy.lang.builder import Builder + from kivy.clock import Clock from kivymd.app import MDApp from kivymd.uix.label import MDLabel @@ -241,18 +222,23 @@ def build(self): class Example(MDApp): + def on_start(self): + def on_start(dt): + self.root.md_bg_color = self.theme_cls.backgroundColor + + Clock.schedule_once(on_start) + def build(self): self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" return ( MDScreen( MDLabel( adaptive_size=True, - pos_hint={"center_x": .5, "center_y": .5}, - text="MDLabel", + pos_hint={"center_x": 0.5, "center_y": 0.5}, + text="Do a double click on me", allow_selection=True, padding=("4dp", "4dp"), - ) + ), ) ) @@ -481,7 +467,6 @@ def open_context_menu(self, instance_label: CopyLabel) -> None: MDIcon: icon: "gmail" - pos_hint: {"center_x": .5, "center_y": .5} .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon.png :align: center @@ -493,8 +478,9 @@ def open_context_menu(self, instance_label: CopyLabel) -> None: MDIcon: icon: "gmail" - badge_icon: "numeric-10" - pos_hint: {"center_x": .5, "center_y": .5} + + MDBadge: + text: "10+" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-badge.png :align: center @@ -505,45 +491,34 @@ def open_context_menu(self, instance_label: CopyLabel) -> None: __all__ = ("MDLabel", "MDIcon") import os -from typing import Union from kivy.animation import Animation from kivy.clock import Clock from kivy.core.clipboard import Clipboard from kivy.core.window import Window -from kivy.graphics import Color, Rectangle +from kivy.graphics import Color, RoundedRectangle from kivy.lang import Builder -from kivy.metrics import sp + from kivy.properties import ( - AliasProperty, BooleanProperty, ColorProperty, - ListProperty, - NumericProperty, ObjectProperty, - OptionProperty, StringProperty, + VariableListProperty, + OptionProperty, ) from kivy.uix.label import Label from kivymd import uix_path from kivymd.theming import ThemableBehavior -from kivymd.theming_dynamic_text import get_contrast_text_color from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior, TouchBehavior -from kivymd.uix.floatlayout import MDFloatLayout - -__MDLabel_colors__ = { - "Primary": "text_color", - "Secondary": "secondary_text_color", - "Hint": "disabled_hint_text_color", - "Error": "error_color", - "OP": { - "Primary": "opposite_text_color", - "Secondary": "opposite_secondary_text_color", - "Hint": "opposite_disabled_hint_text_color", - }, -} +from kivymd.uix.behaviors import ( + DeclarativeBehavior, + TouchBehavior, + BackgroundColorBehavior, +) +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior + with open( os.path.join(uix_path, "label", "label.kv"), encoding="utf-8" @@ -554,80 +529,68 @@ def open_context_menu(self, instance_label: CopyLabel) -> None: class MDLabel( DeclarativeBehavior, ThemableBehavior, + BackgroundColorBehavior, Label, MDAdaptiveWidget, TouchBehavior, + StateLayerBehavior, ): """ Label class. For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and :class:`~kivy.uix.label.Label` and :class:`~kivymd.uix.MDAdaptiveWidget` and - :class:`~kivymd.uix.behaviors.TouchBehavior` + :class:`~kivymd.uix.behaviors.touch_behavior.TouchBehavior` and + :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior` classes documentation. :Events: `on_ref_press` - Called when the user clicks on a word referenced with a + Fired when the user clicks on a word referenced with a ``[ref]`` tag in a text markup. `on_copy` - Called when double-tapping on the label. + Fired when double-tapping on the label. `on_selection` - Called when double-tapping on the label. + Fired when double-tapping on the label. `on_cancel_selection` - Called when the highlighting is removed from the label text. + Fired when the highlighting is removed from the label text. """ - font_style = StringProperty("Body1") + font_style = StringProperty("Body") """ Label font style. - Available vanilla font_style are: `'H1'`, `'H2'`, `'H3'`, `'H4'`, `'H5'`, - `'H6'`, `'Subtitle1'`, `'Subtitle2'`, `'Body1'`, `'Body2'`, `'Button'`, - `'Caption'`, `'Overline'`, `'Icon'`. + .. versionchanged:: 2.0.0 + + Available vanilla font_style are: `'Display'`, `'Headline'`, `'Title'`, + `'Label'`, `'Body'``. :attr:`font_style` is an :class:`~kivy.properties.StringProperty` - and defaults to `'Body1'`. + and defaults to `'Body'`. """ - _capitalizing = BooleanProperty(False) - - def _get_text(self): - if self._capitalizing: - return self._text.upper() - return self._text - - def _set_text(self, value): - self._text = value + role = OptionProperty("large", options=["large", "medium", "small"]) + """ + Role of font style. - _text = StringProperty() + .. versionadded:: 2.0.0 - text = AliasProperty(_get_text, _set_text, bind=["_text", "_capitalizing"]) - """Text of the label.""" + Available options are: `'large'`, `'medium'`, `'small'`. - theme_text_color = OptionProperty( - "Primary", - allownone=True, - options=[ - "Primary", - "Secondary", - "Hint", - "Error", - "Custom", - "ContrastParentBackground", - ], - ) + :attr:`role` is an :class:`~kivy.properties.StringProperty` + and defaults to `'large'`. """ - Label color scheme name. - Available options are: `'Primary'`, `'Secondary'`, `'Hint'`, `'Error'`, - `'Custom'`, `'ContrastParentBackground'`. + text = StringProperty() + """ + Text of the label. - :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` - and defaults to `None`. + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. """ text_color = ColorProperty(None) @@ -690,55 +653,26 @@ def _set_text(self, value): and defaults to `False`. """ - _text_color_str = StringProperty() + radius = VariableListProperty([0], length=4) + """ + Label radius. - parent_background = ColorProperty(None) - can_capitalize = BooleanProperty(True) - canvas_bg = ObjectProperty() + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + _canvas_bg = ObjectProperty() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.bind( - font_style=self.update_font_style, - can_capitalize=self.update_font_style, - ) - self.theme_cls.bind(theme_style=self._do_update_theme_color) self.register_event_type("on_copy") self.register_event_type("on_selection") self.register_event_type("on_cancel_selection") - self.on_theme_text_color(None, self.theme_text_color) - self.update_font_style(None, "") - self.on_opposite_colors(None, self.opposite_colors) - Clock.schedule_once(self.check_font_styles) - - def check_font_styles(self, interval: Union[int, float] = 0) -> bool: - if self.font_style not in list(self.theme_cls.font_styles.keys()): - raise ValueError( - f"MDLabel.font_style is set to an invalid option '{self.font_style}'." - f"Must be one of: {list(self.theme_cls.font_styles)}" - ) - else: - return True - - def update_font_style(self, instance_label, font_style: str) -> None: - if self.check_font_styles() is True: - font_info = self.theme_cls.font_styles[self.font_style] - self.font_name = font_info[0] - if self.font_style in list(self.theme_cls.font_styles.keys())[0:14]: - self.font_size = sp(font_info[1]) - - if font_info[2] and self.can_capitalize: - self._capitalizing = True - else: - self._capitalizing = False - - # TODO: Add letter spacing change - # self.letter_spacing = font_info[3] def do_selection(self) -> None: if not self.is_selected: self.md_bg_color = ( - self.theme_cls.primary_light + self.theme_cls.secondaryContainerColor if not self.color_selection else self.color_selection ) @@ -746,7 +680,7 @@ def do_selection(self) -> None: def cancel_selection(self) -> None: if self.is_selected: self.md_bg_color = ( - self.theme_cls.bg_normal + self.parent.md_bg_color if not self.color_deselection else self.color_deselection ) @@ -754,6 +688,8 @@ def cancel_selection(self) -> None: self.is_selected = False def on_double_tap(self, touch, *args) -> None: + """Fired by double-clicking on the widget.""" + if self.allow_copy and self.collide_point(*touch.pos): Clipboard.copy(self.text) self.dispatch("on_copy") @@ -762,73 +698,44 @@ def on_double_tap(self, touch, *args) -> None: self.dispatch("on_selection") self.is_selected = True - def on_window_touch(self, *args): + def on_window_touch(self, *args) -> None: + """Fired at the on_touch_down event.""" + if self.is_selected: self.cancel_selection() def on_copy(self, *args) -> None: """ - Called when double-tapping on the label. + Fired when double-tapping on the label. .. versionadded:: 1.2.0 """ def on_selection(self, *args) -> None: """ - Called when double-tapping on the label. + Fired when double-tapping on the label. .. versionadded:: 1.2.0 """ def on_cancel_selection(self, *args) -> None: """ - Called when the highlighting is removed from the label text. + Fired when the highlighting is removed from the label text. .. versionadded:: 1.2.0 """ def on_allow_selection(self, instance_label, selection: bool) -> None: + """Fired when the :attr:`allow_selection` value changes.""" + if selection: Window.bind(on_touch_down=self.on_window_touch) else: Window.unbind(on_touch_down=self.on_window_touch) - def on_theme_text_color( - self, instance_label, theme_text_color: str - ) -> None: - op = self.opposite_colors - if op: - self._text_color_str = __MDLabel_colors__.get("OP", "").get( - theme_text_color, "" - ) - else: - self._text_color_str = __MDLabel_colors__.get(theme_text_color, "") - if self._text_color_str: - self._do_update_theme_color() - else: - # 'Custom' and 'ContrastParentBackground' lead here, as well as the - # generic None value it's not yet been set - self._text_color_str = "" - if theme_text_color == "Custom" and self.text_color: - color = self.text_color - elif ( - theme_text_color == "ContrastParentBackground" - and self.parent_background - ): - color = get_contrast_text_color(self.parent_background) - else: - color = [0, 0, 0, 1] - - if self.theme_cls.theme_style_switch_animation: - Animation( - color=color, - d=self.theme_cls.theme_style_switch_animation_duration, - t="linear", - ).start(self) - else: - self.color = color + def on_text_color(self, instance_label, color: list | str) -> None: + """Fired when the :attr:`text_color` value changes.""" - def on_text_color(self, instance_label, color: Union[list, str]) -> None: if self.theme_text_color == "Custom": if self.theme_cls.theme_style_switch_animation: Animation( @@ -839,116 +746,92 @@ def on_text_color(self, instance_label, color: Union[list, str]) -> None: else: self.color = self.text_color - def on_opposite_colors(self, *args) -> None: - self.on_theme_text_color(self, self.theme_text_color) + def on_md_bg_color(self, instance_label, color: list | str) -> None: + """Fired when the :attr:`md_bg_color` value changes.""" - def on_md_bg_color(self, instance_label, color: Union[list, str]) -> None: - self.canvas.remove_group("Background_instruction") - self.canvas.before.clear() - with self.canvas.before: - Color(rgba=color) - self.canvas_bg = Rectangle(pos=self.pos, size=self.size) - self.bind(pos=self.update_canvas_bg_pos) + def on_md_bg_color(*args) -> None: + from kivymd.uix.selectioncontrol import MDCheckbox - def on_size(self, instance_label, size: list) -> None: - if self.canvas_bg: - self.canvas_bg.size = size + if not issubclass(self.__class__, (MDCheckbox, MDIcon)): + self.canvas.remove_group("Background_instruction") - def update_canvas_bg_pos(self, instance_label, pos: list) -> None: - if self.canvas_bg: - self.canvas_bg.pos = pos + # FIXME: IndexError + # try: + # self.canvas.before.clear() + # except IndexError: + # pass - def _do_update_theme_color(self, *args): - if self._text_color_str: - if not self.disabled: - color = getattr(self.theme_cls, self._text_color_str) - else: - color = getattr(self.theme_cls, "disabled_hint_text_color") + with self.canvas.before: + Color(rgba=color) + self._canvas_bg = RoundedRectangle( + pos=self.pos, size=self.size, radius=self.radius + ) + self.bind(pos=self.update_canvas_bg_pos) - if self.theme_cls.theme_style_switch_animation: - Animation( - color=color, - d=self.theme_cls.theme_style_switch_animation_duration, - t="linear", - ).start(self) - else: - self.color = color + Clock.schedule_once(on_md_bg_color) + + def on_size(self, instance_label, size: list) -> None: + """Fired when the parent window of the application is resized.""" + if self._canvas_bg: + self._canvas_bg.size = size + + def update_canvas_bg_pos(self, instance_label, pos: list) -> None: + if self._canvas_bg: + self._canvas_bg.pos = pos -class MDIcon(MDFloatLayout, MDLabel): + +class MDIcon(MDLabel): """ Icon class. - For more information, see in the :class:`~MDLabel` and - :class:`~kivymd.uix.floatlayout.MDFloatLayout` classes documentation. + For more information, see in the + :class:`~MDLabel` class documentation. """ - icon = StringProperty("android") + icon = StringProperty("blank") """ Label icon name. :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `'android'`. - """ - - badge_icon = StringProperty() + and defaults to `'blank'`. """ - Label badge icon name. - .. versionadded:: 1.0.0 - - :attr:`badge_icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - badge_icon_color = ColorProperty([1, 1, 1, 1]) + source = StringProperty(None, allownone=True) """ - Badge icon color in (r, g, b, a) or string format. - - .. versionadded:: 1.0.0 + Path to icon. - :attr:`badge_icon_color` is an :class:`~kivy.properties.ColorProperty` + :attr:`source` is an :class:`~kivy.properties.StringProperty` and defaults to `None`. """ - badge_bg_color = ColorProperty(None) + icon_color = ColorProperty(None) """ - Badge icon background color in (r, g, b, a) or string format. + Icon color in (r, g, b, a) or string format. - .. versionadded:: 1.0.0 + .. versionadded:: 2.0.0 - :attr:`badge_bg_color` is an :class:`~kivy.properties.ColorProperty` + :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - badge_font_size = NumericProperty() - """ - Badge font size. - - .. versionadded:: 1.0.0 - - :attr:`badge_font_size` is an :class:`~kivy.properties.NumericProperty` - and defaults to `0`. + icon_color_disabled = ColorProperty(None) """ + The icon color in (r, g, b, a) or string format of the button when + the button is disabled. - source = StringProperty(None, allownone=True) - """ - Path to icon. + .. versionadded:: 2.0.0 - :attr:`source` is an :class:`~kivy.properties.StringProperty` + :attr:`icon_color_disabled` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - _size = ListProperty((0, 0)) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - Clock.schedule_once(self.adjust_size) + # kivymd.uix.badge.badge.MDBadge object. + _badge = ObjectProperty() - def adjust_size(self, *args) -> None: - from kivymd.uix.selectioncontrol import MDCheckbox + def add_widget(self, widget, index=0, canvas=None): + from kivymd.uix.badge import MDBadge - if not isinstance(self, MDCheckbox): - self.size_hint = (None, None) - self._size = self.texture_size[1], self.texture_size[1] - self.adaptive_size = True + if isinstance(widget, MDBadge): + self._badge = widget + return super().add_widget(widget) diff --git a/kivymd/uix/list/__init__.py b/kivymd/uix/list/__init__.py index a0e061cfe..096715aa6 100644 --- a/kivymd/uix/list/__init__.py +++ b/kivymd/uix/list/__init__.py @@ -1,33 +1,16 @@ # NOQA F401 from .list import ( - BaseListItem, - CheckboxLeftWidget, - IconLeftWidget, - IconLeftWidgetWithoutTouch, - IconRightWidget, - IconRightWidgetWithoutTouch, - ILeftBody, - ILeftBodyTouch, - ImageLeftWidget, - ImageLeftWidgetWithoutTouch, - ImageRightWidget, - ImageRightWidgetWithoutTouch, - IRightBody, - IRightBodyTouch, MDList, - OneLineAvatarIconListItem, - OneLineAvatarListItem, - OneLineIconListItem, - OneLineListItem, - OneLineRightIconListItem, - ThreeLineAvatarIconListItem, - ThreeLineAvatarListItem, - ThreeLineIconListItem, - ThreeLineListItem, - ThreeLineRightIconListItem, - TwoLineAvatarIconListItem, - TwoLineAvatarListItem, - TwoLineIconListItem, - TwoLineListItem, - TwoLineRightIconListItem, + MDListItem, + BaseListItem, + BaseListItemText, + BaseListItemIcon, + MDListItemLeadingIcon, + MDListItemTrailingIcon, + MDListItemHeadlineText, + MDListItemTertiaryText, + MDListItemLeadingAvatar, + MDListItemSupportingText, + MDListItemTrailingCheckbox, + MDListItemTrailingSupportingText, ) diff --git a/kivymd/uix/list/list.kv b/kivymd/uix/list/list.kv index 467d1c823..f7e7bae8a 100644 --- a/kivymd/uix/list/list.kv +++ b/kivymd/uix/list/list.kv @@ -1,173 +1,152 @@ -#:import m_res kivymd.material_resources - - cols: 1 adaptive_height: True padding: 0, self._list_vertical_padding - - size_hint_y: None - - canvas: + + # Divider. + canvas.after: Color: rgba: ( \ - self.theme_cls.divider_color \ - if root.divider is not None \ - else (0, 0, 0, 0) \ + ( \ + self.theme_cls.surfaceVariantColor \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor \ + ) \ + if self.theme_divider_color == "Primary" else \ + self.divider_color ) \ - if not root.divider_color \ - else \ - root.divider_color + if self.divider else self.theme_cls.transparentColor Line: - points: - ( \ - root.x ,root.y, root.x + self.width, root.y) \ - if root.divider == "Full" else \ - (root.x + root._txt_left_pad, root.y, \ - root.x + self.width - root._txt_left_pad-root._txt_right_pad, \ - root.y \ - ) - Color: - rgba: root.bg_color if root.bg_color else (0, 0, 0, 0) - RoundedRectangle: - pos: self.pos - size: self.size - radius: root.radius - - BoxLayout: - id: _text_container - orientation: "vertical" - pos: root.pos - padding: - root._txt_left_pad, root._txt_top_pad, \ - root._txt_right_pad, root._txt_bot_pad - - MDLabel: - id: _lbl_primary - text: root.text - font_style: root.font_style - theme_text_color: root.theme_text_color - text_color: root.text_color - size_hint_y: None - height: self.texture_size[1] - markup: True - shorten_from: "right" - shorten: True - - MDLabel: - id: _lbl_secondary - text: "" if root._num_lines == 1 else root.secondary_text - font_style: root.secondary_font_style - theme_text_color: root.secondary_theme_text_color - text_color: root.secondary_text_color - size_hint_y: None - height: 0 if root._num_lines == 1 else self.texture_size[1] - shorten: True - shorten_from: "right" - markup: True - - MDLabel: - id: _lbl_tertiary - text: "" if root._num_lines == 1 else root.tertiary_text - font_style: root.tertiary_font_style - theme_text_color: root.tertiary_theme_text_color - text_color: root.tertiary_text_color - size_hint_y: None - height: 0 if root._num_lines == 1 else self.texture_size[1] - shorten: True - shorten_from: "right" - markup: True - - - - - BoxLayout: - id: _left_container - size_hint: None, None - x: root.x + dp(16) - y: root.y + root.height / 2 - self.height / 2 - size: dp(40), dp(40) - - - - - BoxLayout: - id: _left_container - size_hint: None, None - x: root.x + dp(16) - y: root.y + root.height - root._txt_top_pad - self.height - dp(5) - size: dp(40), dp(40) - - - - - BoxLayout: - id: _left_container - size_hint: None, None - x: root.x + dp(16) - y: root.y + root.height / 2 - self.height / 2 - size: dp(48), dp(48) - - - - - BoxLayout: - id: _left_container - size_hint: None, None - x: root.x + dp(16) - y: root.y + root.height - root._txt_top_pad - self.height - dp(5) - size: dp(48), dp(48) - - - - - BoxLayout: - id: _right_container - size_hint: None, None - x: root.x + root.width - m_res.HORIZ_MARGINS - self.width - y: root.y + root.height / 2 - self.height / 2 - size: dp(48), dp(48) - - - - - BoxLayout: - id: _right_container - size_hint: None, None - x: root.x + root.width - m_res.HORIZ_MARGINS - self.width - y: root.y + root.height / 2 - self.height / 2 - size: dp(48), dp(48) - - - - - BoxLayout: - id: _right_container - size_hint: None, None - x: root.x + root.width - m_res.HORIZ_MARGINS - self.width - y: root.y + root.height / 2 - self.height / 2 - size: dp(48), dp(48) + width: 1 + points: self.x ,self.y, self.x + self.width, self.y + size_hint_y: None + spacing: "16dp" + padding: + "16dp", \ + "12dp" if len(text_container.children) == 3 else "8dp", \ + "24dp", \ + "12dp" if len(text_container.children) == 3 else "8dp" + # FIXME: The design of the material suggests specifying the + # background color of the disabled widget as the "onSurface" + # color when hovering the mouse cursor. But this color is very + # dark/light. So I chose the color "onSurfaceColor" color with 12 + # percent transparency. + md_bg_color: + self.theme_cls.surfaceColor \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color + height: + { \ + 0: "100dp", \ + 1: "56dp", \ + 2: "72dp", \ + 3: "88dp", \ + } \ + [len(text_container.children)] + on_disabled: + leading_container.children[0].disabled = args[1] - BoxLayout: - id: _right_container - size_hint: None, None - x: root.x + root.width - m_res.HORIZ_MARGINS - self.width - y: root.y + root.height / 2 - self.height / 2 - size: dp(48), dp(48) + id: leading_container + size_hint_x: None + width: 0 + AnchorLayout: + anchor_y: "center" - + BoxLayout: + id: text_container + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + spacing: "2dp" + on_children: + if leading_container.children: \ + leading_container.children[0].pos_hint = {"top": 1} \ + if len(args[1]) == 3 else {"center_y": .5} BoxLayout: - id: _right_container - size_hint: None, None - x: root.x + root.width - m_res.HORIZ_MARGINS - self.width - y: root.y + root.height - root._txt_top_pad - self.height - dp(5) - size: dp(48), dp(48) + id: trailing_container + size_hint_x: None + width: 0 + on_children: + if text_container.children: \ + self.children[0].pos_hint = {"top": 1} \ + if len(text_container.children) == 3 else {"center_y": .5} + + + + size_hint: None, None + size: "24dp", "24dp" + text_color: + ( \ + self.theme_cls.onSurfaceVariantColor \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color \ + if self.icon_color else \ + self.theme_cls.transparentColor \ + ) \ + ) \ + if not self.disabled else self.disabled_color + disabled_color: + self.theme_cls.onSurfaceColor[:-1] + \ + [self.icon_button_standard_opacity_value_disabled_icon] \ + if not self.icon_color_disabled else self.icon_color_disabled + + + + adaptive_width: True + font_style: "Label" + role: "small" + + + + size_hint: None, None + size: "40dp", "40dp" + radius: self.height / 2 + # FIXME: The design of the material suggests specifying the + # background color of the disabled widget as the "onSurface" + # color when hovering the mouse cursor. But this color is very + # dark/light. So I chose the color "onSurfaceColor" color with 12 + # percent transparency. + md_bg_color: + self.theme_cls.primaryContainerColor \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] \ + + ( \ + [self._list_item.list_opacity_value_disabled_leading_avatar] \ + if self._list_item else [0] \ + ) + + + + adaptive_height: True + markup: True + shorten_from: "right" + font_style: "Body" + role: "medium" + shorten: True + text_color: + self.theme_cls.onSurfaceVariantColor \ + if root.theme_text_color == "Primary" else \ + ( \ + root.text_color \ + if root.text_color else \ + self.theme_cls.onSurfaceVariantColor \ + ) + + + + font_style: "Body" + role: "large" + bold: True + # FIXME: `RecursionError: maximum recursion depth exceeded while calling + # a Python object` when use `text_color` property. + -text_color: self.theme_cls.onSurfaceColor if root.theme_text_color == "Primary" else (root.text_color if root.text_color else self.theme_cls.onSurfaceColor) diff --git a/kivymd/uix/list/list.py b/kivymd/uix/list/list.py index 035ca8a1e..e05609f9a 100755 --- a/kivymd/uix/list/list.py +++ b/kivymd/uix/list/list.py @@ -4,146 +4,51 @@ .. seealso:: - `Material Design spec, Lists `_ + `Material Design spec, Lists `_ .. rubric:: Lists are continuous, vertical indexes of text or images. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/lists.png :align: center -The class :class:`~MDList` in combination with a :class:`~BaseListItem` like -:class:`~OneLineListItem` will create a list that expands as items are added to -it, working nicely with `Kivy's` :class:`~kivy.uix.scrollview.ScrollView`. - -Due to the variety in sizes and controls in the `Material Design spec`, -this module suffers from a certain level of complexity to keep the widgets -compliant, flexible and performant. - -For this `KivyMD` provides list items that try to cover the most common usecases, -when those are insufficient, there's a base class called :class:`~BaseListItem` -which you can use to create your own list items. This documentation will only -cover the provided ones, for custom implementations please refer to this -module's source code. - -`KivyMD` provides the following list items classes for use: - -Text only ListItems -------------------- - -- OneLineListItem_ -- TwoLineListItem_ -- ThreeLineListItem_ - -ListItems with widget containers --------------------------------- - -These widgets will take other widgets that inherit from :class:`~ILeftBody`, -:class:`ILeftBodyTouch`, :class:`~IRightBody` or :class:`~IRightBodyTouch` and -put them in their corresponding container. - -As the name implies, :class:`~ILeftBody` and :class:`~IRightBody` will signal -that the widget goes into the left or right container, respectively. - -:class:`~ILeftBodyTouch` and :class:`~IRightBodyTouch` do the same thing, -except these widgets will also receive touch events that occur within their -surfaces. - -`KivyMD` provides base classes such as :class:`~ImageLeftWidget`, -:class:`~ImageRightWidget`, :class:`~IconRightWidget`, :class:`~IconLeftWidget`, -based on the above classes. - -.. rubric:: Allows the use of items with custom widgets on the left. - -- OneLineAvatarListItem_ -- TwoLineAvatarListItem_ -- ThreeLineAvatarListItem_ - -- OneLineIconListItem_ -- TwoLineIconListItem_ -- ThreeLineIconListItem_ - -.. rubric:: It allows the use of elements with custom widgets on the left - and the right. - -- OneLineAvatarIconListItem_ -- TwoLineAvatarIconListItem_ -- ThreeLineAvatarIconListItem_ - -- OneLineRightIconListItem_ -- TwoLineRightIconListItem_ -- ThreeLineRightIconListItem_ +- Use lists to help users find a specific item and act on it; +- Order list items in logical ways (like alphabetical or numerical); +- Three sizes: one-line, two-line, and three-line; +- Keep items short and easy to scan; +- Show icons, text, and actions in a consistent format; Usage ----- -.. tabs:: - - .. tab:: Declarative KV style - - .. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.list import OneLineListItem - - KV = ''' - MDScrollView: - - MDList: - id: container - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - def on_start(self): - for i in range(20): - self.root.ids.container.add_widget( - OneLineListItem(text=f"Single-line item {i}") - ) - - Example().run() +.. code-block:: kv - .. tab:: Declarative python style + MDListItem: - .. code-block:: python + MDListItemLeadingIcon: # MDListItemLeadingAvatar - from kivymd.app import MDApp - from kivymd.uix.list import OneLineListItem + MDListItemHeadlineText: + MDListItemSupportingText: - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return ( - MDScrollView( - MDList( - id="container" - ) - ) - ) + MDListItemTertiaryText: - def on_start(self): - for i in range(20): - self.root.ids.container.add_widget( - OneLineListItem(text=f"Single-line item {i}") - ) + MDListItemTrailingIcon: # MDListItemTrailingCheckbox - Example().run() +Anatomy +------- -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/lists.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/lists-anatomy.png :align: center -Events of List --------------- +Example: +======== + +One line list item +------------------ .. tabs:: - .. tab:: Declarative KV style + .. tab:: Declarative KV styles .. code-block:: python @@ -152,1484 +57,567 @@ def on_start(self): from kivymd.app import MDApp KV = ''' - MDScrollView: + MDScreen: + md_bg_color: self.theme_cls.backgroundColor - MDList: + MDListItem: + pos_hint: {"center_x": .5, "center_y": .5} + size_hint_x: .8 - OneLineAvatarIconListItem: - on_release: print("Click!") - - IconLeftWidget: - icon: "github" - - OneLineAvatarIconListItem: - on_release: print("Click 2!") - - IconLeftWidget: - icon: "gitlab" + MDListItemHeadlineText: + text: "Headline" ''' class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) Example().run() - .. tab:: Declarative python style + .. tab:: Declarative Python styles .. code-block:: python + from kivymd.uix.list import MDListItem, MDListItemHeadlineText + from kivymd.uix.screen import MDScreen from kivymd.app import MDApp - from kivymd.uix.scrollview import MDScrollView - from kivymd.uix.list import MDList, OneLineAvatarIconListItem, IconLeftWidget class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" return ( - MDScrollView( - MDList( - OneLineAvatarIconListItem( - IconLeftWidget( - icon="github" - ), - on_release=lambda x: print("Click!") - ), - OneLineAvatarIconListItem( - IconLeftWidget( - icon="gitlab" - ), - on_release=lambda x: print("Click 2!") + MDScreen( + MDListItem( + MDListItemHeadlineText( + text="Headline", ), - ) + pos_hint={"center_x": .5, "center_y": .5}, + size_hint_x=0.8, + ), + md_bg_color=self.theme_cls.backgroundColor, ) ) Example().run() -.. OneLineListItem: -OneLineListItem ---------------- - -.. code-block:: kv - - OneLineListItem: - text: "Single-line item" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineListItem.png - :align: center - -.. TwoLineListItem: -TwoLineListItem ---------------- - -.. code-block:: kv - - TwoLineListItem: - text: "Two-line item" - secondary_text: "Secondary text here" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineListItem.png - :align: center - -.. ThreeLineListItem: -ThreeLineListItem ------------------ - -.. code-block:: kv - - ThreeLineListItem: - text: "Three-line item" - secondary_text: "This is a multi-line label where you can" - tertiary_text: "fit more text than usual" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineListItem.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/headline-list.gif :align: center -.. OneLineAvatarListItem: -OneLineAvatarListItem ---------------------- +Two line list item +------------------ .. tabs:: - .. tab:: Declarative KV style + .. tab:: Declarative KV styles .. code-block:: kv - OneLineAvatarListItem: - text: "Single-line item with avatar" + MDListItem: + + MDListItemHeadlineText: + text: "Headline" - ImageLeftWidget: - source: "kivymd/images/logo/kivymd-icon-256.png" + MDListItemSupportingText: + text: "Supporting text" - .. tab:: Declarative python style + .. tab:: Declarative Python styles .. code-block:: python - OneLineAvatarListItem( - ImageLeftWidget( - source="kivymd/images/logo/kivymd-icon-256.png" + MDListItem( + MDListItemHeadlineText( + text="Headline", + ), + MDListItemSupportingText( + text="Supporting text", ), - text="Single-line item with avatar", ) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineAvatarListItem.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/headline-supporting-list.png :align: center -.. TwoLineAvatarListItem: -TwoLineAvatarListItem ---------------------- +Three line list item +-------------------- .. tabs:: - .. tab:: Declarative KV style + .. tab:: Declarative KV styles .. code-block:: kv - TwoLineAvatarListItem: - text: "Two-line item with avatar" - secondary_text: "Secondary text here" + MDListItem: + + MDListItemHeadlineText: + text: "Headline" - ImageLeftWidget: - source: "kivymd/images/logo/kivymd-icon-256.png" + MDListItemSupportingText: + text: "Supporting text" - .. tab:: Declarative python style + MDListItemTertiaryText: + text: "Tertiary text" + + .. tab:: Declarative Python styles .. code-block:: python - OneLineAvatarListItem( - ImageLeftWidget( - source="kivymd/images/logo/kivymd-icon-256.png" + MDListItem( + MDListItemHeadlineText( + text="Headline", + ), + MDListItemSupportingText( + text="Supporting text", + ), + MDListItemTertiaryText( + text="Tertiary text", ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", ) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineAvatarListItem.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/headline-supporting-tertiary-list.png :align: center -.. ThreeLineAvatarListItem: -ThreeLineAvatarListItem ------------------------ +List item with leading icon +--------------------------- .. tabs:: - .. tab:: Declarative KV style + .. tab:: Declarative KV styles .. code-block:: kv - ThreeLineAvatarListItem: - text: "Three-line item with avatar" - secondary_text: "Secondary text here" - tertiary_text: "fit more text than usual" - - ImageLeftWidget: - source: "kivymd/images/logo/kivymd-icon-256.png" - - .. tab:: Declarative python style - - .. code-block:: python - - OneLineAvatarListItem( - ImageLeftWidget( - source="kivymd/images/logo/kivymd-icon-256.png" - ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", - tertiary_text: "fit more text than usual" - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineAvatarListItem.png - :align: center - -.. OneLineRightIconListItem: -OneLineRightIconListItem ------------------------- - -.. tabs:: + MDListItem: - .. tab:: Declarative KV style + MDListItemLeadingIcon: + icon: "account" - .. code-block:: kv + MDListItemHeadlineText: + text: "Headline" - OneLineRightIconListItem: - text: "Single-line item with avatar" + MDListItemSupportingText: + text: "Supporting text" - ImageRightWidget: - source: "kivymd/images/logo/kivymd-icon-256.png" + MDListItemTertiaryText: + text: "Tertiary text" - .. tab:: Declarative python style + .. tab:: Declarative Python styles .. code-block:: python - OneLineRightIconListItem( - ImageRightWidget( - source="kivymd/images/logo/kivymd-icon-256.png" + MDListItem( + MDListItemLeadingIcon( + icon="account", + ), + MDListItemHeadlineText( + text="Headline", + ), + MDListItemSupportingText( + text="Supporting text", + ), + MDListItemTertiaryText( + text="Tertiary text", ), - text="Single-line item with avatar", ) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineRightIconListItem.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/headline-supporting-tertiary-leading-icon-list.png :align: center -.. TwoLineRightIconListItem: -TwoLineRightIconListItem ------------------------- +List item with trailing icon +---------------------------- .. tabs:: - .. tab:: Declarative KV style + .. tab:: Declarative KV styles .. code-block:: kv - TwoLineRightIconListItem: - text: "Single-line item with avatar" - secondary_text: "Secondary text here" - - ImageRightWidget: - source: "kivymd/images/logo/kivymd-icon-256.png" - - .. tab:: Declarative python style - - .. code-block:: python - - TwoLineRightIconListItem( - ImageRightWidget( - source="kivymd/images/logo/kivymd-icon-256.png" - ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineRightIconListItem.png - :align: center - -.. ThreeLineRightIconListItem: -ThreeLineRightIconListItem --------------------------- + MDListItem: -.. tabs:: + MDListItemLeadingIcon: + icon: "account" - .. tab:: Declarative KV style + MDListItemHeadlineText: + text: "Headline" - .. code-block:: kv + MDListItemSupportingText: + text: "Supporting text" - ThreeLineRightIconListItem: - text: "Single-line item with avatar" - secondary_text: "Secondary text here" - tertiary_text: "fit more text than usual" + MDListItemTertiaryText: + text: "Tertiary text" - ImageRightWidget: - source: "kivymd/images/logo/kivymd-icon-256.png" + MDListItemTrailingIcon: + icon: "trash-can-outline" - .. tab:: Declarative python style + .. tab:: Declarative Python styles .. code-block:: python - ThreeLineRightIconListItem( - ImageRightWidget( - source="kivymd/images/logo/kivymd-icon-256.png" + MDListItem( + MDListItemLeadingIcon( + icon="account", + ), + MDListItemHeadlineText( + text="Headline", + ), + MDListItemSupportingText( + text="Supporting text", + ), + MDListItemTertiaryText( + text="Tertiary text", + ), + MDListItemTrailingIcon( + icon="trash-can-outline", ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", - tertiary_text: "fit more text than usual", ) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineRightIconListItem.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/headline-supporting-tertiary-leading-trailing-icon-list.png :align: center -.. OneLineIconListItem: -OneLineIconListItem -------------------- +List item with trailing check +---------------------------- .. tabs:: - .. tab:: Declarative KV style + .. tab:: Declarative KV styles .. code-block:: kv - OneLineIconListItem: - text: "Single-line item with avatar" - - IconLeftWidget: - icon: "language-python" - - .. tab:: Declarative python style - - .. code-block:: python - - OneLineIconListItem( - IconLeftWidget( - icon="language-python" - ), - text="Single-line item with avatar" - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineIconListItem.png - :align: center - -.. TwoLineIconListItem: -TwoLineIconListItem -------------------- + MDListItem: -.. tabs:: + MDListItemLeadingIcon: + icon: "account" - .. tab:: Declarative KV style + MDListItemHeadlineText: + text: "Headline" - .. code-block:: kv + MDListItemSupportingText: + text: "Supporting text" - TwoLineIconListItem: - text: "Two-line item with avatar" - secondary_text: "Secondary text here" + MDListItemTertiaryText: + text: "Tertiary text" - IconLeftWidget: - icon: "language-python" + MDListItemTrailingCheckbox: - .. tab:: Declarative python style + .. tab:: Declarative Python styles .. code-block:: python - TwoLineIconListItem( - IconLeftWidget( - icon="language-python" + MDListItem( + MDListItemLeadingIcon( + icon="account", + ), + MDListItemHeadlineText( + text="Headline", + ), + MDListItemSupportingText( + text="Supporting text", + ), + MDListItemTertiaryText( + text="Tertiary text", + ), + MDListItemTrailingCheckbox( ), - text="Single-line item with avatar", - secondary_text: "Secondary text here" ) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineIconListItem.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/headline-supporting-tertiary-leading-trailing-check-list.png :align: center +""" -.. ThreeLineIconListItem: -ThreeLineIconListItem ---------------------- - -.. tabs:: +from __future__ import annotations - .. tab:: Declarative KV style +__all__ = ( + "BaseListItemText", + "BaseListItem", + "BaseListItemIcon", + "MDList", + "MDListItem", + "MDListItemHeadlineText", + "MDListItemSupportingText", + "MDListItemTrailingSupportingText", + "MDListItemLeadingIcon", + "MDListItemTrailingIcon", + "MDListItemTrailingCheckbox", + "MDListItemLeadingAvatar", + "MDListItemTertiaryText", +) - .. code-block:: kv +import os - ThreeLineIconListItem: - text: "Three-line item with avatar" - secondary_text: "Secondary text here" - tertiary_text: "fit more text than usual" +from kivy import Logger +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import ( + NumericProperty, + ObjectProperty, + BooleanProperty, + ColorProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout - IconLeftWidget: - icon: "language-python" +from kivymd.uix.selectioncontrol import MDCheckbox +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CircularRippleBehavior, + DeclarativeBehavior, + RectangularRippleBehavior, + BackgroundColorBehavior, +) +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior +from kivymd.uix.fitimage import FitImage +from kivymd.uix.gridlayout import MDGridLayout +from kivymd.uix.label import MDLabel, MDIcon - .. tab:: Declarative python style +with open( + os.path.join(uix_path, "list", "list.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) - .. code-block:: python - ThreeLineIconListItem( - IconLeftWidget( - icon="language-python" - ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", - tertiary_text: "fit more text than usual", - ) +class MDList(MDGridLayout): + """ + ListItem container. + Best used in conjunction with a :class:`kivy.uix.ScrollView`. -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineIconListItem.png - :align: center + When adding (or removing) a widget, it will resize itself to fit its + children, plus top and bottom paddings as described by the `MD` spec. -.. OneLineAvatarIconListItem: -OneLineAvatarIconListItem -------------------------- + For more information, see in the + :class:`~kivymd.uix.gridlayout.MDGridLayout` class documentation. + """ -.. tabs:: + _list_vertical_padding = NumericProperty("8dp") - .. tab:: Declarative KV style + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.adaptive_height = True - .. code-block:: kv - OneLineAvatarIconListItem: - text: "One-line item with avatar" +class BaseListItem( + DeclarativeBehavior, + BackgroundColorBehavior, + RectangularRippleBehavior, + ButtonBehavior, + ThemableBehavior, + StateLayerBehavior, +): + """ + Base class for list items. - IconLeftWidget: - icon: "plus" + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.behaviors.state_layer_behavior.StateLayerBehavior` + classes documentation. + """ - IconRightWidget: - icon: "minus" + divider = BooleanProperty(False) + """ + Should I use divider for a list item. - .. tab:: Declarative python style + :attr:`divider` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ - .. code-block:: python + divider_color = ColorProperty(None) + """ + The divider color in (r, g, b, a) or string format. - OneLineAvatarIconListItem( - IconLeftWidget( - icon="plus" - ), - IconRightWidget( - icon="minus" - ), - text="Single-line item with avatar", - ) + :attr:`divider_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineAvatarIconListItem.png - :align: center + md_bg_color_disabled = ColorProperty(None) + """ + The background color in (r, g, b, a) or string format of the list item when + the list item is disabled. -.. TwoLineAvatarIconListItem: -TwoLineAvatarIconListItem -------------------------- + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ -.. tabs:: - .. tab:: Declarative KV style +class BaseListItemText(MDLabel): + """ + Base class for text labels of a list item. - .. code-block:: kv + For more information, see in the :class:`~kivymd.uix.label.label.MDLabel` + class documentation. + """ - TwoLineAvatarIconListItem: - text: "Two-line item with avatar" - secondary_text: "Secondary text here" - IconLeftWidget: - icon: "plus" +class BaseListItemIcon(MDIcon): + """ + Base class for leading/trailing icon of list item. - IconRightWidget: - icon: "minus" + For more information, see in the :class:`~kivymd.uix.label.label.MDIcon` + class documentation. + """ - .. tab:: Declarative python style + icon_color = ColorProperty(None) + """ + Icon color in (r, g, b, a) or string format. - .. code-block:: python + :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ - TwoLineAvatarIconListItem( - IconLeftWidget( - icon="plus" - ), - IconRightWidget( - icon="minus" - ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", - ) + icon_color_disabled = ColorProperty(None) + """ + The icon color in (r, g, b, a) or string format of the list item when + the list item is disabled. -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineAvatarIconListItem.png - :align: center + :attr:`icon_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ -.. ThreeLineAvatarIconListItem: -ThreeLineAvatarIconListItem ---------------------------- -.. tabs:: +class MDListItemHeadlineText(BaseListItemText): + """ + Implements a class for headline text of list item. - .. tab:: Declarative KV style + For more information, see in the :class:`~BaseListItemText` + class documentation. + """ - .. code-block:: kv - ThreeLineAvatarIconListItem: - text: "Three-line item with avatar" - secondary_text: "Secondary text here" - tertiary_text: "fit more text than usual" +class MDListItemSupportingText(BaseListItemText): + """ + Implements a class for secondary text of list item. - IconLeftWidget: - icon: "plus" + For more information, see in the :class:`~BaseListItemText` + class documentation. + """ - IconRightWidget: - icon: "minus" - .. tab:: Declarative python style +class MDListItemTertiaryText(BaseListItemText): + """ + Implements a class for tertiary text of list item. - .. code-block:: python + For more information, see in the :class:`~BaseListItemText` + class documentation. + """ - ThreeLineAvatarIconListItem( - IconLeftWidget( - icon="plus" - ), - IconRightWidget( - icon="minus" - ), - text="Single-line item with avatar", - secondary_text: "Secondary text here", - tertiary_text: "fit more text than usual", - ) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineAvatarIconListItem.png - :align: center +class MDListItemTrailingSupportingText(BaseListItemText): + """ + Implements a class for trailing text of list item. -Custom list item ----------------- + For more information, see in the :class:`~BaseListItemText` + class documentation. + """ -.. tabs:: - .. tab:: Declarative KV style +class MDListItemLeadingIcon(BaseListItemIcon): + """ + Implements a class for leading icon class. - .. code-block:: python + For more information, see in the :class:`~BaseListItemIcon` + class documentation. + """ - from kivy.lang import Builder - from kivy.properties import StringProperty - from kivymd.app import MDApp - from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem - from kivymd.uix.selectioncontrol import MDCheckbox - from kivymd.icon_definitions import md_icons +class MDListItemLeadingAvatar(CircularRippleBehavior, ButtonBehavior, FitImage): + """ + Implements a class for leading avatar class. + For more information, see in the + :class:`~kivymd.uix.behaviors.ripple_behavior.CircularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.fitimage.fitimage.FitImage` + classes documentation. + """ - KV = ''' - : + _list_item = ObjectProperty() - IconLeftWidget: - icon: root.icon - RightCheckbox: +class MDListItemTrailingIcon(BaseListItemIcon): + """ + Implements a class for trailing icon class. + For more information, see in the :class:`~BaseListItemIcon` + class documentation. + """ - MDScrollView: - MDList: - id: scroll - ''' +class MDListItemTrailingCheckbox(MDCheckbox): + """ + Implements a class for trailing checkbox class. + For more information, see in the + :class:`~kivymd.uix.selectioncontrol.selectioncontrol.MDCheckbox` + class documentation. + """ - class ListItemWithCheckbox(OneLineAvatarIconListItem): - '''Custom list item.''' - icon = StringProperty("android") +class MDListItem(BaseListItem, BoxLayout): + """ + Implements a list item. + For more information, see in the + :class:`~BaseListItem` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. + """ - class RightCheckbox(IRightBodyTouch, MDCheckbox): - '''Custom right container.''' + def add_widget(self, widget, *args, **kwargs): + if isinstance( + widget, + ( + MDListItemHeadlineText, + MDListItemSupportingText, + MDListItemTertiaryText, + ), + ): + if len(self.ids.text_container.children) < 3: + self.ids.text_container.add_widget(widget) + elif len(self.ids.text_container.children) > 3: + self._set_warnings(widget) + elif isinstance( + widget, (MDListItemLeadingIcon, MDListItemLeadingAvatar) + ): + if not self.ids.leading_container.children: + widget._list_item = self + self.ids.leading_container.add_widget(widget) + Clock.schedule_once( + lambda x: self._set_with_container( + self.ids.leading_container, widget + ) + ) + else: + self._set_warnings(widget) + elif isinstance( + widget, + ( + MDListItemTrailingIcon, + MDListItemTrailingCheckbox, + MDListItemTrailingSupportingText, + ), + ): + if not self.ids.trailing_container.children: + self.ids.trailing_container.add_widget(widget) + Clock.schedule_once( + lambda x: self._set_with_container( + self.ids.trailing_container, widget + ) + ) + else: + self._set_warnings(widget) + else: + return super().add_widget(widget) + def _set_warnings(self, widget): + Logger.warning( + f"KivyMD: " + f"Do not use more than one <{widget.__class__.__name__}> " + f"widget. This is contrary to the material design rules " + f"of version 3" + ) - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - def on_start(self): - icons = list(md_icons.keys()) - for i in range(30): - self.root.ids.scroll.add_widget( - ListItemWithCheckbox(text=f"Item {i}", icon=icons[i]) - ) - - - Example().run() - - .. tab:: Declarative python style - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem - from kivymd.uix.selectioncontrol import MDCheckbox - from kivymd.uix.scrollview import MDScrollView - from kivymd.uix.list import MDList - from kivymd.icon_definitions import md_icons - - - class RightCheckbox(IRightBodyTouch, MDCheckbox): - '''Custom right container.''' - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return ( - MDScrollView( - MDList( - id="scroll" - ) - ) - ) - - def on_start(self): - icons = list(md_icons.keys()) - for i in range(30): - self.root.ids.scroll.add_widget( - OneLineAvatarIconListItem( - IconLeftWidget( - icon=icons[i] - ), - RightCheckbox(), - text=f"Item {i}", - ) - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-list-item.png - :align: center - -.. tabs:: - - .. tab:: Declarative KV style - - .. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.list import IRightBodyTouch - - KV = ''' - OneLineAvatarIconListItem: - text: "One-line item with avatar" - on_size: - self.ids._right_container.width = container.width - self.ids._right_container.x = container.width - - IconLeftWidget: - icon: "cog" - - YourContainer: - id: container - - MDIconButton: - icon: "minus" - - MDIconButton: - icon: "plus" - ''' - - - class YourContainer(IRightBodyTouch, MDBoxLayout): - adaptive_width = True - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - - Example().run() - - .. tab:: Declarative python style - - .. code-block:: python - - from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.list import IRightBodyTouch - from kivymd.uix.button import MDIconButton - from kivymd.uix.list import OneLineAvatarIconListItem, IconLeftWidget - - - class YourContainer(IRightBodyTouch, MDBoxLayout): - adaptive_width = True - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return ( - OneLineAvatarIconListItem( - IconLeftWidget( - icon="cog" - ), - YourContainer( - MDIconButton( - icon="minus" - ), - MDIconButton( - icon="plus" - ), - id="container" - ), - text="One-line item with avatar" - ) - ) - - def on_start(self): - container = self.root.ids.container - self.root.ids._right_container.width = container.width - container.x = container.width - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-list-right-container.png - :align: center - -Behavior --------- - -When using the `AvatarListItem` and `IconListItem` classes, when an icon is clicked, -the event of this icon is triggered: - -.. tabs:: - - .. tab:: Declarative KV style - - .. code-block:: kv - - OneLineIconListItem: - text: "Single-line item with icon" - - IconLeftWidget: - icon: "language-python" - - .. tab:: Declarative python style - - .. code-block:: python - - OneLineIconListItem( - IconLeftWidget( - icon="language-python" - ), - text="Single-line item with avatar", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/list-icon-trigger.gif - :align: center - -You can disable the icon event using the `WithoutTouch` classes: - -.. tabs:: - - .. tab:: Declarative KV style - - .. code-block:: kv - - OneLineIconListItem: - text: "Single-line item with icon" - - IconLeftWidgetWithoutTouch: - icon: "language-python" - - .. tab:: Declarative python style - - .. code-block:: python - - OneLineIconListItem( - IconLeftWidgetWithoutTouch( - icon="language-python" - ), - text="Single-line item with avatar", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/list-icon-without-trigger.gif - :align: center -""" - -__all__ = ( - "BaseListItem", - "MDList", - "ILeftBodyTouch", - "IRightBodyTouch", - "OneLineListItem", - "TwoLineListItem", - "ThreeLineListItem", - "OneLineAvatarListItem", - "TwoLineAvatarListItem", - "ThreeLineAvatarListItem", - "OneLineIconListItem", - "TwoLineIconListItem", - "ThreeLineIconListItem", - "OneLineRightIconListItem", - "TwoLineRightIconListItem", - "ThreeLineRightIconListItem", - "OneLineAvatarIconListItem", - "TwoLineAvatarIconListItem", - "ThreeLineAvatarIconListItem", - "ImageLeftWidget", - "ImageRightWidget", - "IconRightWidget", - "IconLeftWidget", - "CheckboxLeftWidget", - "IconLeftWidgetWithoutTouch", - "IconRightWidgetWithoutTouch", - "ImageRightWidgetWithoutTouch", - "ImageLeftWidgetWithoutTouch", -) - -import os - -from kivy.lang import Builder -from kivy.metrics import dp -from kivy.properties import ( - BooleanProperty, - ColorProperty, - ListProperty, - NumericProperty, - OptionProperty, - StringProperty, - VariableListProperty, -) -from kivy.uix.behaviors import ButtonBehavior -from kivy.uix.floatlayout import FloatLayout - -import kivymd.material_resources as m_res -from kivymd import uix_path -from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import ( - CircularRippleBehavior, - DeclarativeBehavior, - RectangularRippleBehavior, -) -from kivymd.uix.button import MDIconButton -from kivymd.uix.fitimage import FitImage -from kivymd.uix.gridlayout import MDGridLayout -from kivymd.uix.selectioncontrol import MDCheckbox - -with open( - os.path.join(uix_path, "list", "list.kv"), encoding="utf-8" -) as kv_file: - Builder.load_string(kv_file.read()) - - -class MDList(MDGridLayout): - """ - ListItem container. Best used in conjunction with a - :class:`kivy.uix.ScrollView`. - - When adding (or removing) a widget, it will resize itself to fit its - children, plus top and bottom paddings as described by the `MD` spec. - - For more information, see in the - :class:`~kivymd.uix.gridlayout.MDGridLayout` classes documentation. - """ - - _list_vertical_padding = NumericProperty("8dp") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.adaptive_height = True - - -class BaseListItem( - DeclarativeBehavior, - ThemableBehavior, - RectangularRippleBehavior, - ButtonBehavior, - FloatLayout, -): - """ - Base class to all ListItems. Not supposed to be instantiated on its own. - - For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and - :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivy.uix.floatlayout.FloatLayout` classes documentation. - """ - - text = StringProperty() - """ - Text shown in the first line. - - :attr:`text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - text_color = ColorProperty(None) - """ - Text color in (r, g, b, a) or string format used - if :attr:`~theme_text_color` is set to `'Custom'`. - - :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - font_style = StringProperty("Subtitle1") - """ - Text font style. - See `font-definitions `_ - for more information. - - :attr:`font_style` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Subtitle1'`. - """ - - theme_text_color = StringProperty("Primary", allownone=True) - """ - The name of the color scheme for for the primary text. - - :attr:`theme_text_color` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Primary'`. - """ - - secondary_text = StringProperty() - """ - Text shown in the second line. - - :attr:`secondary_text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - tertiary_text = StringProperty() - """ - The text is displayed on the third line. - - :attr:`tertiary_text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - secondary_text_color = ColorProperty(None) - """ - Text color in (r, g, b, a) or string format used for secondary text - if :attr:`~secondary_theme_text_color` is set to `'Custom'`. - - :attr:`secondary_text_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - tertiary_text_color = ColorProperty(None) - """ - Text color in (r, g, b, a) or string format used for tertiary text - if :attr:`~tertiary_theme_text_color` is set to 'Custom'. - - :attr:`tertiary_text_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - secondary_theme_text_color = StringProperty("Secondary", allownone=True) - """ - The name of the color scheme for for the secondary text. - - :attr:`secondary_theme_text_color` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Secondary'`. - """ - - tertiary_theme_text_color = StringProperty("Secondary", allownone=True) - """ - The name of the color scheme for for the tertiary text. - - :attr:`tertiary_theme_text_color` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Secondary'`. - """ - - secondary_font_style = StringProperty("Body1") - """ - Font style for secondary line. - See `font-definitions `_ - for more information. - - :attr:`secondary_font_style` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Body1'`. - """ - - tertiary_font_style = StringProperty("Body1") - """ - Font style for tertiary line. - See `font-definitions `_ - for more information. - - :attr:`tertiary_font_style` is a :class:`~kivy.properties.StringProperty` - and defaults to `'Body1'`. - """ - - divider = OptionProperty( - "Full", options=["Full", "Inset", None], allownone=True - ) - """ - Divider mode. Available options are: `'Full'`, `'Inset'` - and default to `'Full'`. - - :attr:`divider` is a :class:`~kivy.properties.OptionProperty` - and defaults to `'Full'`. - """ - - divider_color = ColorProperty(None) - """ - Divider color in (r, g, b, a) or string format. - - .. versionadded:: 1.0.0 - - :attr:`divider_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - bg_color = ColorProperty(None) - """ - Background color for list item in (r, g, b, a) or string format. - - :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` - 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]`. - """ - - _txt_left_pad = NumericProperty("16dp") - _txt_top_pad = NumericProperty() - _txt_bot_pad = NumericProperty() - _txt_right_pad = NumericProperty(m_res.HORIZ_MARGINS) - _num_lines = 3 - _no_ripple_effect = BooleanProperty(False) - _touchable_widgets = ListProperty() - - def on_touch_down(self, touch): - if self.propagate_touch_to_touchable_widgets(touch, "down"): - return - super().on_touch_down(touch) - - def on_touch_move(self, touch, *args): - if self.propagate_touch_to_touchable_widgets(touch, "move", *args): - return - super().on_touch_move(touch, *args) - - def on_touch_up(self, touch): - if self.propagate_touch_to_touchable_widgets(touch, "up"): - return - super().on_touch_up(touch) - - def propagate_touch_to_touchable_widgets(self, touch, touch_event, *args): - triggered = False - for i in self._touchable_widgets: - if i.collide_point(touch.x, touch.y): - triggered = True - if touch_event == "down": - i.on_touch_down(touch) - elif touch_event == "move": - i.on_touch_move(touch, *args) - elif touch_event == "up": - i.on_touch_up(touch) - return triggered - - def add_widget(self, widget): - if issubclass(widget.__class__, ILeftBody): - self.ids._left_container.add_widget(widget) - elif issubclass(widget.__class__, ILeftBodyTouch): - self.ids._left_container.add_widget(widget) - self._touchable_widgets.append(widget) - elif issubclass(widget.__class__, IRightBody): - self.ids._right_container.add_widget(widget) - elif issubclass(widget.__class__, IRightBodyTouch): - self.ids._right_container.add_widget(widget) - self._touchable_widgets.append(widget) - else: - return super().add_widget(widget) - - def remove_widget(self, widget): - super().remove_widget(widget) - if widget in self._touchable_widgets: - self._touchable_widgets.remove(widget) - - -class ILeftBody: - """ - Pseudo-interface for widgets that go in the left container for - ListItems that support it. - - Implements nothing and requires no implementation, for annotation only. - """ - - -class ILeftBodyTouch: - """ - Same as :class:`~ILeftBody`, but allows the widget to receive touch - events instead of triggering the ListItem's ripple effect. - """ - - -class IRightBody: - """ - Pseudo-interface for widgets that go in the right container for - ListItems that support it. - - Implements nothing and requires no implementation, for annotation only. - """ - - -class IRightBodyTouch: - """ - Same as :class:`~IRightBody`, but allows the widget to receive touch - events instead of triggering the ``ListItem``'s ripple effect - """ - - -class OneLineListItem(BaseListItem): - """ - A one line list item. - - For more information, see in the :class:`~BaseListItem` - classes documentation. - """ - - _txt_top_pad = NumericProperty("16dp") - _txt_bot_pad = NumericProperty("15dp") - _height = NumericProperty() - _num_lines = 1 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.height = dp(48) if not self._height else self._height - - -class TwoLineListItem(BaseListItem): - """ - A two line list item. - - For more information, see in the :class:`~BaseListItem` - classes documentation. - """ - - _txt_top_pad = NumericProperty("20dp") - _txt_bot_pad = NumericProperty("15dp") - _height = NumericProperty() - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.height = dp(72) if not self._height else self._height - - -class ThreeLineListItem(BaseListItem): - """ - A three line list item. - - For more information, see in the :class:`~BaseListItem` - classes documentation. - """ - - _txt_top_pad = NumericProperty("16dp") - _txt_bot_pad = NumericProperty("15dp") - _height = NumericProperty() - _num_lines = 3 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.height = dp(88) if not self._height else self._height - - -class OneLineAvatarListItem(BaseListItem): - """ - A one line list item with left image. - - For more information, see in the :class:`~BaseListItem` - classes documentation. - """ - - _txt_left_pad = NumericProperty("72dp") - _txt_top_pad = NumericProperty("20dp") - _txt_bot_pad = NumericProperty("19dp") - _height = NumericProperty() - _num_lines = 1 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.height = dp(56) if not self._height else self._height - - -class TwoLineAvatarListItem(OneLineAvatarListItem): - """ - A two line list item with left image. - - For more information, see in the :class:`~OneLineAvatarListItem` - classes documentation. - """ - - _txt_top_pad = NumericProperty("20dp") - _txt_bot_pad = NumericProperty("15dp") - _height = NumericProperty() - _num_lines = 2 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.height = dp(72) if not self._height else self._height - - -class ThreeLineAvatarListItem(ThreeLineListItem): - """ - A three line list item with left image. - - For more information, see in the :class:`~ThreeLineListItem` - classes documentation. - """ - - _txt_left_pad = NumericProperty("72dp") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - -class OneLineIconListItem(OneLineListItem): - """ - A one line list item with left icon. - - For more information, see in the :class:`~OneLineListItem` - classes documentation. - """ - - _txt_left_pad = NumericProperty("72dp") - - -class TwoLineIconListItem(OneLineIconListItem): - """ - A two line list item with left icon. - - For more information, see in the :class:`~OneLineIconListItem` - classes documentation. - """ - - _txt_top_pad = NumericProperty("20dp") - _txt_bot_pad = NumericProperty("15dp") - _height = NumericProperty() - _num_lines = 2 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.height = dp(72) if not self._height else self._height - - -class ThreeLineIconListItem(ThreeLineListItem): - """ - A three line list item with left icon. - - For more information, see in the :class:`~ThreeLineListItem` - classes documentation. - """ - - _txt_left_pad = NumericProperty("72dp") - - -class OneLineRightIconListItem(OneLineListItem): - """ - A one line list item with right icon/image. - - For more information, see in the :class:`~OneLineListItem` - classes documentation. - """ - - _txt_right_pad = NumericProperty("40dp") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS - - -class TwoLineRightIconListItem(OneLineRightIconListItem): - """ - A two line list item with right icon/image. - - For more information, see in the :class:`~OneLineRightIconListItem` - classes documentation. - """ - - _txt_top_pad = NumericProperty("20dp") - _txt_bot_pad = NumericProperty("15dp") - _height = NumericProperty() - _num_lines = 2 - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.height = dp(72) if not self._height else self._height - - -class ThreeLineRightIconListItem(ThreeLineListItem): - """ - A three line list item with right icon/image. - - For more information, see in the :class:`~ThreeLineRightIconListItem` - classes documentation. - """ - - _txt_right_pad = NumericProperty("40dp") - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS - - -class OneLineAvatarIconListItem(OneLineAvatarListItem): - """ - A one line list item with left/right icon/image/widget. - - For more information, see in the :class:`~OneLineAvatarListItem` - classes documentation. - """ - - _txt_right_pad = NumericProperty("40dp") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS - - -class TwoLineAvatarIconListItem(TwoLineAvatarListItem): - """ - A two line list item with left/right icon/image/widget. - - For more information, see in the :class:`~TwoLineAvatarListItem` - classes documentation. - """ - - _txt_right_pad = NumericProperty("40dp") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS - - -class ThreeLineAvatarIconListItem(ThreeLineAvatarListItem): - """ - A three line list item with left/right icon/image/widget. - - For more information, see in the :class:`~ThreeLineAvatarListItem` - classes documentation. - """ - - _txt_right_pad = NumericProperty("40dp") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS - - -class TouchBehavior: - def on_release(self): - if issubclass(self.parent.parent.__class__, BaseListItem): - self.parent.parent.dispatch("on_release") - - -class ImageLeftWidget( - CircularRippleBehavior, ButtonBehavior, ILeftBodyTouch, FitImage -): - """ - The widget implements the left image for use in ListItem classes. - - For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~ILeftBodyTouch` and - :class:`~kivymd.uix.fitimage.FitImage` classes documentation. - """ - - -class ImageLeftWidgetWithoutTouch( - CircularRippleBehavior, TouchBehavior, ButtonBehavior, ILeftBody, FitImage -): - """ - Disables the image event. - The widget implements the left image for use in `ListItem` classes. - - For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~TouchBehavior` and - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~ILeftBody` and - :class:`~kivymd.uix.fitimage.FitImage` classes documentation. - - .. versionadded:: 1.0.0 - """ - - _no_ripple_effect = True - - -class ImageRightWidget( - CircularRippleBehavior, ButtonBehavior, IRightBodyTouch, FitImage -): - """ - The widget implements the right image for use in ListItem classes. - - For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~IRightBodyTouch` and - :class:`~kivymd.uix.fitimage.FitImage` classes documentation. - """ - - -class ImageRightWidgetWithoutTouch( - CircularRippleBehavior, TouchBehavior, ButtonBehavior, IRightBody, FitImage -): - """ - Disables the image event. - The widget implements the right image for use in `ListItem` classes. - - For more information, see in the - :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and - :class:`~TouchBehavior` and - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~IRightBody` and - :class:`~kivymd.uix.fitimage.FitImage` classes documentation. - - .. versionadded:: 1.0.0 - """ - - _no_ripple_effect = True - - -class IconRightWidget(IRightBodyTouch, MDIconButton): - """ - The widget implements the right icon for use in ListItem classes. - - For more information, see in the - :class:`~IRightBodyTouch` and - :class:`~kivymd.uix.button.MDIconButton` - classes documentation. - """ - - pos_hint = {"center_y": 0.5} - - -class IconRightWidgetWithoutTouch(TouchBehavior, IRightBody, MDIconButton): - """ - Disables the icon event. - The widget implements the right icon for use in ListItem classes. - - For more information, see in the - :class:`~TouchBehavior` and - :class:`~IRightBody` and - :class:`~kivymd.uix.button.MDIconButton` - classes documentation. - - .. versionadded:: 1.0.0 - """ - - pos_hint = {"center_y": 0.5} - _no_ripple_effect = True - - -class IconLeftWidget(ILeftBodyTouch, MDIconButton): - """ - The widget implements the left icon for use in ListItem classes. - - For more information, see in the - :class:`~ILeftBodyTouch` and - :class:`~kivymd.uix.button.MDIconButton` - classes documentation. - """ - - pos_hint = {"center_y": 0.5} - - -class IconLeftWidgetWithoutTouch(TouchBehavior, ILeftBody, MDIconButton): - """ - Disables the icon event. - The widget implements the left icon for use in ListItem classes. - - For more information, see in the - :class:`~TouchBehavior` and - :class:`~ILeftBody` and - :class:`~kivymd.uix.button.MDIconButton` - classes documentation. - - .. versionadded:: 1.0.0 - """ - - pos_hint = {"center_y": 0.5} - _no_ripple_effect = True - - -class CheckboxLeftWidget(ILeftBodyTouch, MDCheckbox): - """ - The widget implements the left checkbox element for use in ListItem classes. - - For more information, see in the - :class:`~ILeftBodyTouch` and - :class:`~kivymd.uix.selectioncontrol.MDCheckbox` - classes documentation. - """ + def _set_with_container(self, container, widget): + container.width = widget.width diff --git a/kivymd/uix/menu/menu.kv b/kivymd/uix/menu/menu.kv index a53c0715e..8a52ba411 100644 --- a/kivymd/uix/menu/menu.kv +++ b/kivymd/uix/menu/menu.kv @@ -22,7 +22,6 @@ MDLabel: text: root.text pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" size_hint_x: None @@ -34,25 +33,26 @@ + container.padding[2] \ + container.spacing \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color MDTrailingTextContainer: id: trailing_container text: root.trailing_text adaptive_width: True - theme_text_color: "Custom" if root.trailing_text_color else "Primary" + theme_text_color: "Custom" text_color: - root.trailing_text_color \ - if root.trailing_text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.trailing_text_color else \ + root.trailing_text_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -74,16 +74,15 @@ size_hint: None, None size: "48dp", "48dp" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.leading_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.leading_icon_color \ - if root.leading_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.leading_icon_color else \ + root.leading_icon_color MDLabel: text: root.text pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" size_hint_x: None @@ -97,10 +96,11 @@ + container.spacing \ + dp(18) \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color Widget: @@ -108,16 +108,16 @@ id: trailing_container text: root.trailing_text adaptive_width: True - theme_text_color: "Custom" if root.trailing_text_color else "Primary" + theme_text_color: "Custom" text_color: - root.trailing_text_color \ - if root.trailing_text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.trailing_text_color else \ + root.trailing_text_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -140,7 +140,6 @@ size_hint_x: None shorten_from: "right" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" width: @@ -152,10 +151,11 @@ + container.spacing \ + dp(18) \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color Widget: @@ -165,16 +165,16 @@ size: "48dp", "48dp" pos_hint: {"center_y": .5} icon: root.trailing_icon - theme_text_color: "Custom" if root.trailing_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.trailing_icon_color \ - if root.trailing_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.trailing_icon_color else \ + root.trailing_icon_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -190,21 +190,21 @@ size_hint: None, None size: "48dp", "48dp" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.trailing_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.trailing_icon_color \ - if root.trailing_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.trailing_icon_color else \ + root.trailing_icon_color MDLabel: text: root.trailing_text adaptive_size: True pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.trailing_text_color else "Primary" + theme_text_color: "Custom" text_color: - root.trailing_text_color \ - if root.trailing_text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color @@ -222,7 +222,6 @@ size_hint_x: None shorten_from: "right" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" width: @@ -233,10 +232,11 @@ + container.padding[2] \ + container.spacing \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color MDTrailingIconTextContainer: id: trailing_container @@ -245,10 +245,10 @@ trailing_text_color: root.trailing_text_color trailing_icon_color: root.trailing_icon_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -263,18 +263,18 @@ text: root.text valign: "center" padding_x: "12dp" - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -296,16 +296,15 @@ size_hint: None, None size: "48dp", "48dp" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.leading_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.leading_icon_color \ - if root.leading_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.leading_icon_color else \ + root.leading_icon_color MDLabel: text: root.text pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" size_hint_x: None @@ -319,10 +318,11 @@ + container.spacing \ + dp(18) \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color Widget: @@ -333,10 +333,10 @@ trailing_icon_color: root.trailing_icon_color trailing_text_color: root.trailing_text_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -358,11 +358,11 @@ size_hint: None, None size: "48dp", "48dp" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.leading_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.leading_icon_color \ - if root.leading_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.leading_icon_color else \ + root.leading_icon_color MDLabel: id: label @@ -371,7 +371,6 @@ size_hint_x: None shorten_from: "right" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" width: @@ -384,10 +383,11 @@ + container.spacing \ + dp(18) \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color Widget: @@ -397,16 +397,16 @@ size: "48dp", "48dp" pos_hint: {"center_y": .5} icon: root.trailing_icon - theme_text_color: "Custom" if root.trailing_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.trailing_icon_color \ - if root.trailing_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.trailing_icon_color else \ + root.trailing_icon_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -428,11 +428,11 @@ size_hint: None, None size: "48dp", "48dp" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.leading_icon_color else "Primary" + theme_text_color: "Custom" text_color: - root.leading_icon_color \ - if root.leading_icon_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.leading_icon_color else \ + root.leading_icon_color MDLabel: id: label @@ -441,7 +441,6 @@ size_hint_x: None shorten_from: "right" pos_hint: {"center_y": .5} - theme_text_color: "Custom" if root.text_color else "Primary" shorten: True shorten_from: "right" width: @@ -452,15 +451,16 @@ + container.padding[2] \ + container.spacing \ ) + theme_text_color: "Custom" text_color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + app.theme_cls.onSurfaceVariantColor \ + if not root.text_color else \ + root.text_color - MDSeparator: + MDDivider: md_bg_color: ( \ - self.theme_cls.divider_color \ + app.theme_cls.outlineVariantColor \ if not root.divider_color \ else root.divider_color \ ) \ @@ -470,14 +470,18 @@ 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 - radius: root.radius size_hint: None, None + focus_behavior: False + style: "elevated" + elevation_level: 2 + shadow_softness: 1.5 + shadow_radius: 4 + theme_bg_color: root.theme_bg_color +# md_bg_color: +# app.theme_cls.surfaceContainerColor \ +# if root.theme_bg_color == "Primary" else \ +# root.md_bg_color + MDBoxLayout: id: content_header diff --git a/kivymd/uix/menu/menu.py b/kivymd/uix/menu/menu.py index 012eef466..b8acebc05 100755 --- a/kivymd/uix/menu/menu.py +++ b/kivymd/uix/menu/menu.py @@ -636,14 +636,14 @@ def build(self): ) from kivy.uix.recycleview import RecycleView -import kivymd.material_resources as m_res from kivymd import uix_path from kivymd.uix.behaviors import StencilBehavior, RectangularRippleBehavior from kivymd.uix.behaviors.motion_behavior import MotionDropDownMenuBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.card import MDCard from kivymd.uix.label import MDLabel -from kivymd.uix.list import IRightBody + +# from kivymd.uix.list import IRightBody with open( os.path.join(uix_path, "menu", "menu.kv"), encoding="utf-8" @@ -762,7 +762,7 @@ class BaseDropdownItem(RectangularRippleBehavior, ButtonBehavior, MDBoxLayout): """ -class MDTrailingTextContainer(BaseDropdownItem, IRightBody, MDLabel): +class MDTrailingTextContainer(BaseDropdownItem, MDLabel): """ Implements a container for trailing text. @@ -775,7 +775,7 @@ class MDTrailingTextContainer(BaseDropdownItem, IRightBody, MDLabel): """ -class MDTrailingIconTextContainer(BaseDropdownItem, IRightBody, MDBoxLayout): +class MDTrailingIconTextContainer(BaseDropdownItem, MDBoxLayout): """ Implements a container for trailing icons and trailing text. @@ -1058,42 +1058,6 @@ class MDDropdownMenu(MotionDropDownMenuBehavior, StencilBehavior, MDCard): and defaults to `'[dp(7)]'`. """ - elevation = NumericProperty(m_res.DROP_DOWN_MENU_ELEVATION) - """ - See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation` - attribute. - - :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` - and defaults to `2`. - """ - - shadow_radius = VariableListProperty([6], length=4) - """ - See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_radius` - attribute. - - :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty` - and defaults to `[6]`. - """ - - shadow_softness = NumericProperty(m_res.DROP_DOWN_MENU_SOFTNESS) - """ - See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness` - attribute. - - :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty` - and defaults to `6`. - """ - - shadow_offset = ListProperty(m_res.DROP_DOWN_MENU_OFFSET) - """ - See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset` - attribute. - - :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty` - and defaults to `(0, -2)`. - """ - _items = [] _start_coords = [] _tar_x = 0 @@ -1424,7 +1388,7 @@ def _remove_menu(self, *args): from kivy.metrics import dp from kivymd.app import MDApp - from kivymd.uix.button import MDRaisedButton + from kivymd.uix.button import MDButton from kivymd.uix.screen import MDScreen class Test(MDApp): @@ -1452,7 +1416,11 @@ def on_start(self): ] for pos_hint in pos_hints: self.screen.add_widget( - MDRaisedButton(pos_hint=pos_hint, on_release=self.open_menu) + MDButton( + text="Press me", + pos_hint=pos_hint, + on_release=self.open_menu, + ) ) def build(self): diff --git a/kivymd/uix/navigationbar/__init__.py b/kivymd/uix/navigationbar/__init__.py new file mode 100644 index 000000000..a492e0200 --- /dev/null +++ b/kivymd/uix/navigationbar/__init__.py @@ -0,0 +1,7 @@ +# NOQA F401 +from .navigationbar import ( + MDNavigationBar, + MDNavigationItem, + MDNavigationItemLabel, + MDNavigationItemIcon, +) diff --git a/kivymd/uix/navigationbar/navigationbar.kv b/kivymd/uix/navigationbar/navigationbar.kv new file mode 100644 index 000000000..719d06491 --- /dev/null +++ b/kivymd/uix/navigationbar/navigationbar.kv @@ -0,0 +1,133 @@ + + size_hint_y: None + height: "80dp" + elevation_level: 0 + md_bg_color: + self.theme_cls.surfaceContainerColor \ + if root.theme_bg_color == "Primary" else \ + root.md_bg_color + + + + size_hint: None, None + size: "24sp", "24sp" + # theme_text_color: "Custom" + icon_color: + ( \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.active else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + if self.theme_icon_color == "Primary" else \ + ( \ + self.icon_color_active if self.icon_color_active else \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.active else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + ) \ + if self.parent.parent.active else \ + ( \ + self.icon_color_normal if self.icon_color_normal else \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.active else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + ) \ + ) \ + if self.parent else self.theme_cls.transparentColor + 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: + ( \ + ( \ + self.theme_cls.secondaryContainerColor \ + if not self.parent.parent.indicator_color else \ + self.parent.parent.indicator_color \ + ) \ + if self.parent.parent.active else \ + self.theme_cls.transparentColor \ + ) \ + if self.parent else self.theme_cls.transparentColor + RoundedRectangle: + radius: [16,] + size: + ( \ + (self.parent.parent._selected_region_width, dp(32)) \ + ) \ + if self.parent else (0, dp(32)) + pos: + ( \ + (self.center_x - self.parent.parent._selected_region_width / 2, \ + self.center_y - dp(16)) \ + ) \ + if self.parent else (0, 0) + + + + adaptive_size: True + role: "medium" + text_color: + ( \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.active else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + if self.theme_text_color == "Primary" else \ + ( \ + self.text_color_active if self.text_color_active else \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.active else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + ) \ + if self.parent.parent.active else \ + ( \ + self.text_color_normal if self.text_color_normal else \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.active else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + ) \ + ) \ + if self.parent else self.theme_cls.transparentColor + + + + + MDNavigationItemIconContainer: + id: icon_container + size_hint: None, None + size: self.minimum_size + pos_hint: {"center_x": .5} + y: + ( \ + (root.parent.height - (self.height + dp(16))) \ + if len(label_container.children) else \ + (root.parent.height / 2 - self.height / 2) \ + ) \ + if root.parent else 0 + + MDNavigationItemLabelContainer: + id: label_container + size_hint: None, None + size: self.minimum_size + pos_hint: {"center_x": .5} + y: + "16dp" \ + if len(icon_container.children) else \ + ( \ + (root.parent.height / 2 - self.height / 2) if root.parent else 0 \ + ) diff --git a/kivymd/uix/navigationbar/navigationbar.py b/kivymd/uix/navigationbar/navigationbar.py new file mode 100644 index 000000000..e2f45cc05 --- /dev/null +++ b/kivymd/uix/navigationbar/navigationbar.py @@ -0,0 +1,624 @@ +""" +Components/Navigation bar +========================= + +.. seealso:: + + `Material Design 3 spec, Navigation bar `_ + +.. 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 + + + + MDNavigationBar: + + MDNavigationItem: + + MDNavigationItemIcon: + + MDNavigationItemLabel: + + [...] + +Anatomy +======= + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigationbar-item-anatomy.png + :align: center + +Example +------- + +.. tabs:: + + .. tab:: Declarative KV style + + .. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem + from kivymd.uix.screen import MDScreen + + + class BaseMDNavigationItem(MDNavigationItem): + icon = StringProperty() + text = StringProperty() + + + class BaseScreen(MDScreen): + image_size = StringProperty() + + + KV = ''' + + + MDNavigationItemIcon: + icon: root.icon + + MDNavigationItemLabel: + text: root.text + + + + + FitImage: + source: f"https://picsum.photos/{root.image_size}/{root.image_size}" + size_hint: .9, .9 + pos_hint: {"center_x": .5, "center_y": .5} + radius: dp(24) + + + MDBoxLayout: + orientation: "vertical" + md_bg_color: self.theme_cls.backgroundColor + + MDScreenManager: + id: screen_manager + + BaseScreen: + name: "Screen 1" + image_size: "1024" + + BaseScreen: + name: "Screen 2" + image_size: "800" + + BaseScreen: + name: "Screen 3" + image_size: "600" + + + MDNavigationBar: + on_switch_tabs: app.on_switch_tabs(*args) + + BaseMDNavigationItem + icon: "gmail" + text: "Screen 1" + active: True + + BaseMDNavigationItem + icon: "twitter" + text: "Screen 2" + + BaseMDNavigationItem + icon: "linkedin" + text: "Screen 3" + ''' + + + class Example(MDApp): + def on_switch_tabs( + self, + bar: MDNavigationBar, + item: MDNavigationItem, + item_icon: str, + item_text: str, + ): + self.root.ids.screen_manager.current = item_text + + def build(self): + return Builder.load_string(KV) + + + Example().run() + + .. tab:: Declarative python style + + .. code-block:: python + + from kivy.metrics import dp + from kivy.properties import StringProperty + + from kivymd.uix.fitimage import FitImage + from kivymd.uix.screen import MDScreen + from kivymd.uix.screenmanager import MDScreenManager + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.navigationbar import ( + MDNavigationBar, + MDNavigationItem, + MDNavigationItemLabel, + MDNavigationItemIcon, + ) + from kivymd.app import MDApp + + + class BaseMDNavigationItem(MDNavigationItem): + icon = StringProperty() + text = StringProperty() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_widget(MDNavigationItemIcon(icon=self.icon)) + self.add_widget(MDNavigationItemLabel(text=self.text)) + + + class BaseScreen(MDScreen): + image_size = StringProperty() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_widget( + FitImage( + source=f"https://picsum.photos/{self.image_size}/{self.image_size}", + size_hint=(0.9, 0.9), + pos_hint={"center_x": 0.5, "center_y": 0.5}, + radius=dp(24), + ), + ) + + + class Example(MDApp): + def on_switch_tabs( + self, + bar: MDNavigationBar, + item: MDNavigationItem, + item_icon: str, + item_text: str, + ): + self.root.get_ids().screen_manager.current = item_text + + def build(self): + return MDBoxLayout( + MDScreenManager( + BaseScreen( + name="Screen 1", + image_size="1024", + ), + BaseScreen( + name="Screen 2", + image_size="800", + ), + BaseScreen( + name="Screen 3", + image_size="600", + ), + id="screen_manager", + ), + MDNavigationBar( + BaseMDNavigationItem( + icon="gmail", + text="Screen 1", + active=True, + ), + BaseMDNavigationItem( + icon="twitter", + text="Screen 2", + ), + BaseMDNavigationItem( + icon="linkedin", + text="Screen 3", + ), + on_switch_tabs=self.on_switch_tabs, + ), + orientation="vertical", + md_bg_color=self.theme_cls.backgroundColor, + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigationbar-usage.gif + :align: center + +API break +========= + +1.2.0 version +------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + + class Example(MDApp): + def build(self): + return Builder.load_string( + ''' + MDScreen: + + MDBottomNavigation: + + MDBottomNavigationItem: + name: 'screen 1' + text: 'Mail' + icon: 'gmail' + badge_icon: "numeric-10" + + MDLabel: + text: 'Screen 1' + halign: 'center' + + MDBottomNavigationItem: + name: 'screen 2' + text: 'Twitter' + icon: 'twitter' + + MDLabel: + text: 'Screen 2' + halign: 'center' + ''' + ) + + + Example().run() + +2.0.0 version +------------- + +MDNavigationBar in version 2.0.0 no longer provides a screen manager for +content placement. You have to implement it yourself. This is due to the fact +that when using MDNavigationBar and MDTabs widgets at the same time, there +were conflicts between their screen managers. + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem + from kivymd.uix.screen import MDScreen + + + class BaseMDNavigationItem(MDNavigationItem): + icon = StringProperty() + text = StringProperty() + + + class BaseScreen(MDScreen): + ... + + + KV = ''' + + + MDNavigationItemIcon: + icon: root.icon + + MDNavigationItemLabel: + text: root.text + + + + + MDLabel: + text: root.name + halign: "center" + + + MDBoxLayout: + orientation: "vertical" + md_bg_color: self.theme_cls.backgroundColor + + MDScreenManager: + id: screen_manager + + BaseScreen: + name: "Screen 1" + + BaseScreen: + name: "Screen 2" + + + MDNavigationBar: + on_switch_tabs: app.on_switch_tabs(*args) + + BaseMDNavigationItem + icon: "gmail" + text: "Screen 1" + active: True + + BaseMDNavigationItem + icon: "twitter" + text: "Screen 2" + ''' + + + class Example(MDApp): + def on_switch_tabs( + self, + bar: MDNavigationBar, + item: MDNavigationItem, + item_icon: str, + item_text: str, + ): + self.root.ids.screen_manager.current = item_text + + def build(self): + return Builder.load_string(KV) + + + Example().run() +""" + +from __future__ import annotations + +__all__ = ( + "MDNavigationItem", + "MDNavigationBar", + "MDNavigationItemLabel", + "MDNavigationItemIcon", +) + +import os + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + NumericProperty, + StringProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.relativelayout import RelativeLayout + +from kivymd.uix.label import MDLabel, MDIcon +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd import uix_path +from kivymd.uix.behaviors import ( + DeclarativeBehavior, + CommonElevationBehavior, + RectangularRippleBehavior, +) +from kivymd.utils.set_bars_colors import set_bars_colors + +with open( + os.path.join(uix_path, "navigationbar", "navigationbar.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDNavigationItemLabel(MDLabel): + """ + Implements a text label for the :class:`~MDNavigationItem` class. + + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ + + text_color_active = ColorProperty(None) + """ + Item icon color in (r, g, b, a) or string format. + + :attr:`text_color_active` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_color_normal = ColorProperty(None) + """ + Item icon color in (r, g, b, a) or string format. + + :attr:`text_color_normal` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + +class MDNavigationItemIcon(MDIcon): + """ + Implements a icon for the :class:`~MDNavigationItem` class. + + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivymd.uix.label.label.MDIcon` class documentation. + """ + + icon_color_active = ColorProperty(None) + """ + Item icon color in (r, g, b, a) or string format. + + :attr:`icon_color_active` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_color_normal = ColorProperty(None) + """ + Item icon color in (r, g, b, a) or string format. + + :attr:`icon_color_normal` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + +class MDNavigationItem( + DeclarativeBehavior, + RectangularRippleBehavior, + ButtonBehavior, + RelativeLayout, +): + """ + Bottom item class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivy.uix.anchorlayout.AnchorLayout` and + :class:`~kivy.uix.behaviors.ButtonBehavior` + classes documentation. + + .. versionchanged:: 2.0.0 + Rename class from `MDBottomNavigationItem` to `MDNavigationItem`. + """ + + active = BooleanProperty(False) + """ + Indicates if the bar item is active or inactive. + + :attr:`active` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + indicator_color = ColorProperty(None) + """ + The background color in (r, g, b, a) or string format of the highlighted + item. + + .. versionadded:: 1.0.0 + + .. versionchanged:: 2.0.0 + Rename property from `selected_color_background` to `indicator_color`. + + :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + indicator_transition = StringProperty("in_out_sine") + """ + Animation type of the active element indicator. + + :attr:`indicator_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `'in_out_sine'`. + """ + + indicator_duration = NumericProperty(0.1) + """ + Duration of animation of the active element indicator. + + :attr:`indicator_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.1`. + """ + + _selected_region_width = NumericProperty(dp(0)) + + def on_active(self, instance, value) -> None: + """Fired when the values of :attr:`active` change.""" + + def on_active(*args): + Animation( + _selected_region_width=dp(64) if value else 0, + t=self.indicator_transition, + d=self.indicator_duration, + ).start(self) + + Clock.schedule_once(on_active) + + def on_release(self) -> None: + """Fired when clicking on a panel item.""" + + self.parent.set_active_item(self) + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDNavigationItemLabel): + self.ids.label_container.add_widget(widget) + elif isinstance(widget, MDNavigationItemIcon): + self.ids.icon_container.add_widget(widget) + elif isinstance( + widget, + (MDNavigationItemIconContainer, MDNavigationItemLabelContainer), + ): + return super().add_widget(widget) + + +class MDNavigationBar(CommonElevationBehavior, MDBoxLayout): + """ + A navigation bar class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivymd.uix.boxlayout.MDBoxLayout` + classes documentation. + + :Events: + :attr:`on_switch_tabs` + Fired when switching tabs. + + .. versionadded:: 1.0.0 + + .. versionchanged:: 2.0.0 + Rename class from `MDBottomNavigation` to `MDNavigationBar`. + """ + + 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`. + """ + + def __init__(self, *args, **kwargs): + self.register_event_type("on_switch_tabs") + super().__init__(*args, **kwargs) + Clock.schedule_once(self.set_status_bar_color) + + def set_active_item(self, item: MDNavigationItem) -> None: + """Sets the currently active element on the panel.""" + + for widget in self.children: + if item is widget: + widget.active = True + self.dispatch( + "on_switch_tabs", + widget, + widget.ids.icon_container.children[0].icon + if len(widget.ids.icon_container.children) + else "", + widget.ids.label_container.children[0].text + if len(widget.ids.label_container.children) + else "", + ) + else: + widget.active = False + + def set_status_bar_color(self, interval: int | float) -> None: + """Sets the color of the lower system navigation bar.""" + + if self.set_bars_color: + set_bars_colors(self.md_bg_color, None, self.theme_cls.theme_style) + + def on_switch_tabs( + self, item: MDNavigationItem, item_icon: str, item_text: str + ) -> None: + """Fired when switching tabs.""" + + +class MDNavigationItemIconContainer(BoxLayout): + pass + + +class MDNavigationItemLabelContainer(BoxLayout): + pass diff --git a/kivymd/uix/navigationdrawer/__init__.py b/kivymd/uix/navigationdrawer/__init__.py index 1e859f918..db6485ffa 100644 --- a/kivymd/uix/navigationdrawer/__init__.py +++ b/kivymd/uix/navigationdrawer/__init__.py @@ -4,6 +4,9 @@ MDNavigationDrawerDivider, MDNavigationDrawerHeader, MDNavigationDrawerItem, + MDNavigationDrawerItemLeadingIcon, + MDNavigationDrawerItemText, + MDNavigationDrawerItemTrailingText, MDNavigationDrawerLabel, MDNavigationDrawerMenu, MDNavigationLayout, diff --git a/kivymd/uix/navigationdrawer/navigationdrawer.kv b/kivymd/uix/navigationdrawer/navigationdrawer.kv index dbb484aaa..702fce195 100644 --- a/kivymd/uix/navigationdrawer/navigationdrawer.kv +++ b/kivymd/uix/navigationdrawer/navigationdrawer.kv @@ -1,134 +1,83 @@ #:import Window kivy.core.window.Window -#:import m_res kivymd.material_resources -: + + type: "filled" size_hint_x: None - width: Window.width - dp(56) if Window.width <= dp(376) else dp(320) - md_bg_color: self.theme_cls.bg_light - padding: + width: Window.width - dp(56) if Window.width <= dp(360) else dp(320) + theme_bg_color: "Custom" + shadow_radius: self.radius + md_bg_color: + self.theme_cls.surfaceContainerLowColor \ + if not self.background_color else \ + self.background_color x: (self.width * (self.open_progress - 1)) \ if self.anchor == "left" \ else (Window.width - self.width * self.open_progress) - canvas: - Clear - Color: - rgba: self.md_bg_color - RoundedRectangle: - size: self.size - pos: self.pos - source: root.background - radius: root.radius - adaptive_height: True + padding: "20dp", "0dp", "16dp", "16dp" + text_color: + self.theme_cls.onSurfaceColor \ + if self.theme_text_color == "Primary" else \ + self.text_color - MDLabel: - text: root.text - adaptive_size: True - markup: True + + radius: self.height / 2 + ripple_color: self.theme_cls.onSecondaryContainerColor[:-1] + [0.12] + theme_bg_color: "Custom" + md_bg_color: + self.theme_cls.surfaceContainerLowColor \ + if not self.inactive_indicator_color else \ + self.inactive_indicator_color + + + + text_color: + self.theme_cls.onSurfaceVariantColor \ + if self.theme_text_color == "Primary" else \ + self.text_color - - adaptive_height: True - MDSeparator: - color: root.color if root.color else app.theme_cls.divider_color + + font_style: "Label" + role: "large" + text_color: + self.theme_cls.onSurfaceVariantColor \ + if self.theme_text_color == "Primary" else \ + self.text_color + + + + icon_color: + self.theme_cls.onSurfaceVariantColor \ + if self.theme_icon_color == "Primary" else \ + self.icon_color - adaptive_height: True + size_hint_y: None + height: self.minimum_height - FitImage: - id: logo - source: root.source - size_hint: None, None - size: label_box.height, label_box.height - - MDBoxLayout: - id: label_box - orientation: "vertical" - adaptive_height: True - - MDLabel: - id: title - adaptive_height: True - halign: root.title_halign - text: root.title - font_style: root.title_font_style - font_size: root.title_font_size - color: - root.title_color \ - if root.title_color else \ - app.theme_cls.text_color - - MDLabel: - id: text - adaptive_height: True - text: root.text - halign: root.text_halign - font_style: root.text_font_style - font_size: root.text_font_size - color: - root.text_color \ - if root.text_color else \ - app.theme_cls.text_color + + padding: 0, "4dp", 0, "4dp" + size_hint_y: None + height: self.minimum_height - - radius: self.height / 2 if self.radius == [0, 0, 0, 0] else self.radius - divider: None - theme_text_color: "Custom" - text_color: self.text_color if not self.selected else self.selected_color - _txt_left_pad: "56dp" - on_size: - self.ids._left_container.x = "4dp" - self.ids._right_container.width = right_label.texture_size[0] - on_release: - if not self.selected: self._text_color = self.text_color - self._text_right_color = root.text_right_color if root.text_right_color else app.theme_cls.text_color - self._drawer_menu.reset_active_color(self) - - IconLeftWidgetWithoutTouch: - icon: root.icon - theme_icon_color: "Custom" - icon_color: - ( \ - app.theme_cls.text_color \ - if not root.icon_color else \ - root.icon_color \ - ) \ - if not root.selected else \ - root.selected_color - - MDLabel: - id: right_label - text: root.right_text - pos_hint: {"center_y": .5} - adaptive_size: True - markup: True - color: - ( \ - root.text_right_color \ - if root.text_right_color else \ - app.theme_cls.text_color \ - ) \ - if not root.selected else \ - root.selected_color - x: - root.x \ - + root.width \ - - m_res.HORIZ_MARGINS \ - - root.ids._right_container.width - dp(24) \ - - self.texture_size[0] \ - + dp(24) + MDDivider: - MDList: + GridLayout: id: menu + cols: 1 + size_hint_y: None + height: self.minimum_height spacing: root.spacing + diff --git a/kivymd/uix/navigationdrawer/navigationdrawer.py b/kivymd/uix/navigationdrawer/navigationdrawer.py index ac2eac309..2a726f83b 100755 --- a/kivymd/uix/navigationdrawer/navigationdrawer.py +++ b/kivymd/uix/navigationdrawer/navigationdrawer.py @@ -4,19 +4,22 @@ .. seealso:: - `Material Design 2 spec, Navigation drawer `_ and - `Material Design 3 spec, Navigation drawer `_ + `Material Design, Navigation drawer `_ .. rubric:: Navigation drawers provide access to destinations in your app. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer.png :align: center +- Use navigation drawers in expanded layouts and modal navigation drawers in compact and medium layouts +- Can be open or closed by default +- Two types: standard and modal + When using the class :class:`~MDNavigationDrawer` skeleton of your `KV` markup should look like this: -Anatomy -------- +Usage +----- .. code-block:: kv @@ -32,8 +35,8 @@ MDNavigationDrawer: - # This custom rule should implement what will be appear in your - # MDNavigationDrawer. + # This custom rule should implement what will be displayed in + # your MDNavigationDrawer. ContentNavigationDrawer: A simple example @@ -47,11 +50,11 @@ from kivy.lang import Builder - from kivymd.uix.boxlayout import MDBoxLayout from kivymd.app import MDApp KV = ''' MDScreen: + md_bg_color: self.theme_cls.backgroundColor MDNavigationLayout: @@ -59,31 +62,39 @@ MDScreen: - MDTopAppBar: - title: "Navigation Drawer" - elevation: 4 - pos_hint: {"top": 1} - md_bg_color: "#e7e4c0" - specific_text_color: "#4a4939" - left_action_items: - [['menu', lambda x: nav_drawer.set_state("open")]] + MDButton: + pos_hint: {"center_x": .5, "center_y": .5} + on_release: nav_drawer.set_state("toggle") + MDButtonText: + text: "Open Drawer" MDNavigationDrawer: id: nav_drawer - radius: (0, 16, 16, 0) + radius: 0, dp(16), dp(16), 0 - ContentNavigationDrawer: - ''' + MDNavigationDrawerMenu: + MDNavigationDrawerLabel: + text: "Mail" - class ContentNavigationDrawer(MDBoxLayout): - pass + MDNavigationDrawerItem: + + MDNavigationDrawerItemLeadingIcon: + icon: "account" + + MDNavigationDrawerItemText: + text: "Inbox" + + MDNavigationDrawerItemTrailingText: + text: "24" + + MDNavigationDrawerDivider: + ''' class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) @@ -93,454 +104,416 @@ def build(self): .. code-block:: python - from kivymd.app import MDApp - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.navigationdrawer import MDNavigationLayout, MDNavigationDrawer - from kivymd.uix.screen import MDScreen - from kivymd.uix.screenmanager import MDScreenManager - from kivymd.uix.toolbar import MDTopAppBar + from kivy.metrics import dp - - class ContentNavigationDrawer(MDBoxLayout): - pass + from kivymd.uix.button import MDButton, MDButtonText + from kivymd.uix.screenmanager import MDScreenManager + from kivymd.uix.navigationdrawer import ( + MDNavigationLayout, + MDNavigationDrawer, + MDNavigationDrawerMenu, + MDNavigationDrawerLabel, + MDNavigationDrawerItem, + MDNavigationDrawerItemLeadingIcon, + MDNavigationDrawerItemText, + MDNavigationDrawerItemTrailingText, + MDNavigationDrawerDivider, + ) + from kivymd.uix.screen import MDScreen + from kivymd.app import MDApp class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" - return( - MDScreen( - MDNavigationLayout( - MDScreenManager( - MDScreen( - MDTopAppBar( - title="Navigation Drawer", - elevation=4, - pos_hint={"top": 1}, - md_bg_color="#e7e4c0", - specific_text_color="#4a4939", - left_action_items=[ - ['menu', lambda x: self.nav_drawer_open()] - ], - ) - - ) + return MDScreen( + MDNavigationLayout( + MDScreenManager( + MDScreen( + MDButton( + MDButtonText( + text="Open Drawer", + ), + on_release=lambda x: self.root.get_ids().nav_drawer.set_state( + "toggle" + ), + pos_hint={"center_x": 0.5, "center_y": 0.5}, + ), ), - MDNavigationDrawer( - ContentNavigationDrawer(), - id="nav_drawer", - radius=(0, 16, 16, 0), + ), + MDNavigationDrawer( + MDNavigationDrawerMenu( + MDNavigationDrawerLabel( + text="Mail", + ), + MDNavigationDrawerItem( + MDNavigationDrawerItemLeadingIcon( + icon="account", + ), + MDNavigationDrawerItemText( + text="Inbox", + ), + MDNavigationDrawerItemTrailingText( + text="24", + ), + ), + MDNavigationDrawerDivider( + ), ), + id="nav_drawer", + radius=(0, dp(16), dp(16), 0), ), ), + md_bg_color=self.theme_cls.backgroundColor, ) - def nav_drawer_open(self, *args): - nav_drawer = self.root.children[0].ids.nav_drawer - nav_drawer.set_state("open") - Example().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer.gif +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-anatomy.png :align: center .. Note:: :class:`~MDNavigationDrawer` is an empty :class:`~kivymd.uix.card.MDCard` panel. -Standard content for the navigation bar ---------------------------------------- - -.. tabs:: - - .. tab:: Declarative KV styles - - .. code-block:: python +Item anatomy +------------ - from kivy.lang import Builder +.. code-block:: kv - from kivymd.app import MDApp + MDNavigationDrawerItem: - KV = ''' - - focus_color: "#e7e4c0" - text_color: "#4a4939" - icon_color: "#4a4939" - ripple_color: "#c5bdd2" - selected_color: "#0c6c4d" + MDNavigationDrawerItemLeadingIcon: + icon: "account" + MDNavigationDrawerItemText: + text: "Inbox" - - text_color: "#4a4939" - icon_color: "#4a4939" - focus_behavior: False - selected_color: "#4a4939" - _no_ripple_effect: True + MDNavigationDrawerItemTrailingText: + text: "24" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-item-anatomy.png + :align: center - MDScreen: +Type drawer +=========== - MDNavigationLayout: +Standard +-------- - MDScreenManager: +.. code-block:: kv - MDScreen: + MDNavigationDrawer: + drawer_type: "standard" - MDTopAppBar: - title: "Navigation Drawer" - elevation: 4 - pos_hint: {"top": 1} - md_bg_color: "#e7e4c0" - specific_text_color: "#4a4939" - left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]] +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-type-standard.gif + :align: center - MDNavigationDrawer: - id: nav_drawer - radius: (0, 16, 16, 0) +Modal +----- - MDNavigationDrawerMenu: +.. code-block:: kv - MDNavigationDrawerHeader: - title: "Header title" - title_color: "#4a4939" - text: "Header text" - spacing: "4dp" - padding: "12dp", 0, 0, "56dp" + MDNavigationDrawer: + drawer_type: "modal" - MDNavigationDrawerLabel: - text: "Mail" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-type-modal.gif + :align: center - DrawerClickableItem: - icon: "gmail" - right_text: "+99" - text_right_color: "#4a4939" - text: "Inbox" +Anchoring screen edge for drawer +================================ - DrawerClickableItem: - icon: "send" - text: "Outbox" + Left + ---- - MDNavigationDrawerDivider: + .. code-block:: kv - MDNavigationDrawerLabel: - text: "Labels" + MDNavigationDrawer: + anchor: "left" - DrawerLabelItem: - icon: "information-outline" - text: "Label" + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-ancjor-left.png + :align: center - DrawerLabelItem: - icon: "information-outline" - text: "Label" - ''' + Right + ----- + .. code-block:: kv - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) + MDNavigationDrawer: + anchor: "right" + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-ancjor-right.png + :align: center - Example().run() +API break +========= - .. tab:: Declarative python styles +1.2.0 version +------------- - .. code-block:: python +.. code-block:: python - from kivymd.app import MDApp - from kivymd.uix.navigationdrawer import ( - MDNavigationLayout, - MDNavigationDrawer, - MDNavigationDrawerMenu, - MDNavigationDrawerHeader, - MDNavigationDrawerLabel, - MDNavigationDrawerDivider, - MDNavigationDrawerItem, - ) - from kivymd.uix.screen import MDScreen - from kivymd.uix.screenmanager import MDScreenManager - from kivymd.uix.toolbar import MDTopAppBar + from kivy.lang import Builder + from kivymd.app import MDApp - class BaseNavigationDrawerItem(MDNavigationDrawerItem): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.radius = 24 - self.text_color = "#4a4939" - self.icon_color = "#4a4939" - self.focus_color = "#e7e4c0" + KV = ''' + + focus_color: "#e7e4c0" + text_color: "#4a4939" + icon_color: "#4a4939" + ripple_color: "#c5bdd2" + selected_color: "#0c6c4d" - class DrawerLabelItem(BaseNavigationDrawerItem): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.focus_behavior = False - self._no_ripple_effect = True - self.selected_color = "#4a4939" + + text_color: "#4a4939" + icon_color: "#4a4939" + focus_behavior: False + selected_color: "#4a4939" + _no_ripple_effect: True - class DrawerClickableItem(BaseNavigationDrawerItem): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.ripple_color = "#c5bdd2" - self.selected_color = "#0c6c4d" + MDScreen: + MDNavigationLayout: - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return( - MDScreen( - MDNavigationLayout( - MDScreenManager( - MDScreen( - MDTopAppBar( - title="Navigation Drawer", - elevation=4, - pos_hint={"top": 1}, - md_bg_color="#e7e4c0", - specific_text_color="#4a4939", - left_action_items=[ - ['menu', lambda x: self.nav_drawer_open()] - ], - ) - - ) - ), - MDNavigationDrawer( - MDNavigationDrawerMenu( - MDNavigationDrawerHeader( - title="Header title", - title_color="#4a4939", - text="Header text", - spacing="4dp", - padding=("12dp", 0, 0, "56dp"), - ), - MDNavigationDrawerLabel( - text="Mail", - ), - DrawerClickableItem( - icon="gmail", - right_text="+99", - text_right_color="#4a4939", - text="Inbox", - ), - DrawerClickableItem( - icon="send", - text="Outbox", - ), - MDNavigationDrawerDivider(), - MDNavigationDrawerLabel( - text="Labels", - ), - DrawerLabelItem( - icon="information-outline", - text="Label", - ), - DrawerLabelItem( - icon="information-outline", - text="Label", - ), - ), - id="nav_drawer", - radius=(0, 16, 16, 0), - ) - ) - ) - ) + MDScreenManager: - def nav_drawer_open(self, *args): - nav_drawer = self.root.children[0].ids.nav_drawer - nav_drawer.set_state("open") + MDScreen: + MDRaisedButton: + text: "Open Drawer" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: nav_drawer.set_state("toggle") - Example().run() + MDNavigationDrawer: + id: nav_drawer + radius: (0, dp(16), dp(16), 0) -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-standatd-content.gif - :align: center + MDNavigationDrawerMenu: -Switching screens in the ``ScreenManager`` and using the common ``MDTopAppBar`` ------------------------------------------------------------------------------ + MDNavigationDrawerHeader: + title: "Header title" + title_color: "#4a4939" + text: "Header text" + spacing: "4dp" + padding: "12dp", 0, 0, "56dp" -.. tabs:: + MDNavigationDrawerLabel: + text: "Mail" - .. tab:: Declarative KV styles + DrawerClickableItem: + icon: "gmail" + right_text: "+99" + text_right_color: "#4a4939" + text: "Inbox" - .. code-block:: python + DrawerClickableItem: + icon: "send" + text: "Outbox" - from kivy.lang import Builder - from kivy.properties import ObjectProperty + MDNavigationDrawerDivider: - from kivymd.app import MDApp - from kivymd.uix.scrollview import MDScrollView + MDNavigationDrawerLabel: + text: "Labels" - KV = ''' - + DrawerLabelItem: + icon: "information-outline" + text: "Label" - MDList: + DrawerLabelItem: + icon: "information-outline" + text: "Label" + ''' - OneLineListItem: - text: "Screen 1" - on_press: - root.nav_drawer.set_state("close") - root.screen_manager.current = "scr 1" - OneLineListItem: - text: "Screen 2" - on_press: - root.nav_drawer.set_state("close") - root.screen_manager.current = "scr 2" + class Example(MDApp): + def build(self): + return Builder.load_string(KV) - MDScreen: + Example().run() - MDTopAppBar: - pos_hint: {"top": 1} - elevation: 4 - title: "MDNavigationDrawer" - left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]] +2.2.0 version +------------- - MDNavigationLayout: +.. code-block:: python - MDScreenManager: - id: screen_manager + from kivy.lang import Builder + from kivy.properties import StringProperty, ColorProperty - MDScreen: - name: "scr 1" + from kivymd.app import MDApp + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.navigationdrawer import ( + MDNavigationDrawerItem, MDNavigationDrawerItemTrailingText + ) - MDLabel: - text: "Screen 1" - halign: "center" + KV = ''' + + active_indicator_color: "#e7e4c0" - MDScreen: - name: "scr 2" + MDNavigationDrawerItemLeadingIcon: + icon: root.icon + theme_icon_color: "Custom" + icon_color: "#4a4939" - MDLabel: - text: "Screen 2" - halign: "center" + MDNavigationDrawerItemText: + text: root.text + theme_text_color: "Custom" + text_color: "#4a4939" - MDNavigationDrawer: - id: nav_drawer - radius: (0, 16, 16, 0) - ContentNavigationDrawer: - screen_manager: screen_manager - nav_drawer: nav_drawer - ''' + + adaptive_height: True + padding: "18dp", 0, 0, "12dp" + MDNavigationDrawerItemLeadingIcon: + icon: root.icon + theme_icon_color: "Custom" + icon_color: "#4a4939" + pos_hint: {"center_y": .5} - class ContentNavigationDrawer(MDScrollView): - screen_manager = ObjectProperty() - nav_drawer = ObjectProperty() + MDNavigationDrawerLabel: + text: root.text + theme_text_color: "Custom" + text_color: "#4a4939" + pos_hint: {"center_y": .5} + padding: "6dp", 0, "16dp", 0 + theme_line_height: "Custom" + line_height: 0 - class Example(MDApp): - def build(self): - self.theme_cls.primary_palette = "Orange" - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) + MDScreen: + md_bg_color: self.theme_cls.backgroundColor + MDNavigationLayout: - Example().run() + MDScreenManager: - .. tab:: Declarative python styles + MDScreen: - .. code-block:: python + MDButton: + pos_hint: {"center_x": .5, "center_y": .5} + on_release: nav_drawer.set_state("toggle") - from kivymd.app import MDApp - from kivymd.uix.label import MDLabel - from kivymd.uix.list import MDList, OneLineListItem - from kivymd.uix.navigationdrawer import MDNavigationLayout, MDNavigationDrawer - from kivymd.uix.screen import MDScreen - from kivymd.uix.screenmanager import MDScreenManager - from kivymd.uix.scrollview import MDScrollView - from kivymd.uix.toolbar import MDTopAppBar + MDButtonText: + text: "Open Drawer" + MDNavigationDrawer: + id: nav_drawer + radius: 0, dp(16), dp(16), 0 + + MDNavigationDrawerMenu: + + MDNavigationDrawerHeader: + orientation: "vertical" + padding: 0, 0, 0, "12dp" + adaptive_height: True + + MDLabel: + text: "Header title" + theme_text_color: "Custom" + theme_line_height: "Custom" + line_height: 0 + text_color: "#4a4939" + adaptive_height: True + padding_x: "16dp" + font_style: "Display" + role: "small" + + MDLabel: + text: "Header text" + padding_x: "18dp" + adaptive_height: True + font_style: "Title" + role: "large" + + MDNavigationDrawerDivider: + + DrawerItem: + icon: "gmail" + text: "Inbox" + trailing_text: "+99" + trailing_text_color: "#4a4939" + + DrawerItem: + icon: "send" + text: "Outbox" + + MDNavigationDrawerDivider: + + MDNavigationDrawerLabel: + text: "Labels" + padding_y: "12dp" + + DrawerLabel: + icon: "information-outline" + text: "Label" + + DrawerLabel: + icon: "information-outline" + text: "Label" + ''' + + + class DrawerLabel(MDBoxLayout): + icon = StringProperty() + text = StringProperty() + + + class DrawerItem(MDNavigationDrawerItem): + icon = StringProperty() + text = StringProperty() + trailing_text = StringProperty() + trailing_text_color = ColorProperty() + + _trailing_text_obj = None + + def on_trailing_text(self, instance, value): + self._trailing_text_obj = MDNavigationDrawerItemTrailingText( + text=value, + theme_text_color="Custom", + text_color=self.trailing_text_color, + ) + self.add_widget(self._trailing_text_obj) - class Example(MDApp): - def build(self): - self.theme_cls.primary_palette = "Orange" - self.theme_cls.theme_style = "Dark" - return ( - MDScreen( - MDTopAppBar( - pos_hint={"top": 1}, - elevation=4, - title="MDNavigationDrawer", - left_action_items=[["menu", lambda x: self.nav_drawer_open()]], - ), - MDNavigationLayout( - MDScreenManager( - MDScreen( - MDLabel( - text="Screen 1", - halign="center", - ), - name="scr 1", - ), - MDScreen( - MDLabel( - text="Screen 2", - halign="center", - ), - name="scr 2", - ), - id="screen_manager", - ), - MDNavigationDrawer( - MDScrollView( - MDList( - OneLineListItem( - text="Screen 1", - on_press=self.switch_screen, - ), - OneLineListItem( - text="Screen 2", - on_press=self.switch_screen, - ), - ), - ), - id="nav_drawer", - radius=(0, 16, 16, 0), - ), - id="navigation_layout", - ) - ) - ) + def on_trailing_text_color(self, instance, value): + self._trailing_text_obj.text_color = value - def switch_screen(self, instance_list_item: OneLineListItem): - self.root.ids.navigation_layout.ids.screen_manager.current = { - "Screen 1": "scr 1", "Screen 2": "scr 2" - }[instance_list_item.text] - self.root.children[0].ids.nav_drawer.set_state("close") - def nav_drawer_open(self): - nav_drawer = self.root.children[0].ids.nav_drawer - nav_drawer.set_state("open") + class Example(MDApp): + def build(self): + return Builder.load_string(KV) - Example().run() + Example().run() """ __all__ = ( "MDNavigationLayout", "MDNavigationDrawer", "MDNavigationDrawerItem", + "MDNavigationDrawerItemLeadingIcon", + "MDNavigationDrawerItemTrailingText", + "MDNavigationDrawerItemText", "MDNavigationDrawerMenu", "MDNavigationDrawerHeader", "MDNavigationDrawerLabel", "MDNavigationDrawerDivider", + "BaseNavigationDrawerItem", ) import os -from typing import Union from kivy.animation import Animation, AnimationTransition -from kivy.clock import Clock from kivy.core.window import Window from kivy.graphics.context_instructions import Color from kivy.graphics.vertex_instructions import Rectangle from kivy.lang import Builder +from kivy.metrics import dp from kivy.properties import ( AliasProperty, BooleanProperty, @@ -551,16 +524,24 @@ def nav_drawer_open(self): StringProperty, VariableListProperty, ) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout from kivy.uix.screenmanager import ScreenManager +from kivymd.uix.appbar import MDTopAppBar +from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.label import MDLabel from kivymd import uix_path from kivymd.uix.behaviors.focus_behavior import FocusBehavior -from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.card import MDCard -from kivymd.uix.floatlayout import MDFloatLayout -from kivymd.uix.list import MDList, OneLineAvatarIconListItem +from kivymd.uix.list import ( + MDListItem, + MDListItemLeadingIcon, + MDListItemSupportingText, + MDListItemTrailingSupportingText, +) from kivymd.uix.scrollview import MDScrollView -from kivymd.uix.toolbar import MDTopAppBar with open( os.path.join(uix_path, "navigationdrawer", "navigationdrawer.kv"), @@ -573,10 +554,33 @@ class NavigationDrawerContentError(Exception): pass -class MDNavigationLayout(MDFloatLayout): +class BaseNavigationDrawerItem: + """ + Implement the base class for the menu list item. + + .. versionadded:: 2.0.0 + """ + + selected = BooleanProperty(False) + """ + Is the item selected. + + :attr:`selected` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + # kivymd.uix.navigationdrawer.MDNavigationDrawerMenu object. + _drawer_menu = ObjectProperty() + # kivymd.uix.navigationdrawer.MDNavigationDrawerItem object. + _drawer_item = ObjectProperty() + + +class MDNavigationLayout(DeclarativeBehavior, FloatLayout): """ For more information, see in the - :class:`~kivymd.uix.floatlayout.MDFloatLayout` class documentation. + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` + classes documentation. """ _scrim_color = ObjectProperty(None) @@ -593,7 +597,7 @@ def update_pos(self, instance_navigation_drawer, pos_x: float) -> None: manager = self._screen_manager if not drawer or not manager: return - if drawer.type == "standard": + if drawer.drawer_type == "standard": manager.size_hint_x = None if drawer.anchor == "left": manager.x = drawer.width + drawer.x @@ -601,7 +605,7 @@ def update_pos(self, instance_navigation_drawer, pos_x: float) -> None: else: manager.x = 0 manager.width = drawer.x - elif drawer.type == "modal": + elif drawer.drawer_type == "modal": manager.size_hint_x = None manager.x = 0 if drawer.anchor == "left": @@ -634,7 +638,8 @@ def add_widget(self, widget, index=0, canvas=None): """ if not isinstance( - widget, (MDNavigationDrawer, ScreenManager, MDTopAppBar) + widget, + (MDNavigationDrawer, ScreenManager, MDTopAppBar), ): raise NavigationDrawerContentError( "The MDNavigationLayout must contain " @@ -656,324 +661,142 @@ def add_widget(self, widget, index=0, canvas=None): return super().add_widget(widget) -class MDNavigationDrawerLabel(MDBoxLayout): +class MDNavigationDrawerLabel(MDLabel): """ - Implements a label for a menu for :class:`~MDNavigationDrawer` class. + Implements a label class. - For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` + For more information, see in the :class:`~kivymd.uix.label.label.MDLabel` class documentation. .. versionadded:: 1.0.0 - - .. code-block:: kv - - MDNavigationDrawer: - - MDNavigationDrawerMenu: - - MDNavigationDrawerLabel: - text: "Mail" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-label.png - :align: center - """ - - text = StringProperty() - """ - Text label. - - :attr:`text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - padding = VariableListProperty(["20dp", 0, 0, "8dp"]) - """ - Padding between layout box and children: [padding_left, padding_top, - padding_right, padding_bottom]. - - Padding also accepts a two argument form [padding_horizontal, - padding_vertical] and a one argument form [padding]. - - :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` - and defaults to `['20dp', 0, 0, '8dp']`. """ -class MDNavigationDrawerDivider(MDBoxLayout): +class MDNavigationDrawerDivider(BoxLayout): """ - Implements a divider for a menu for :class:`~MDNavigationDrawer` class. + Implements a divider class. - For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` - class documentation. + For more information, see in the + :class:`~kivy.uix.boxlayout.BoxLayout` class documentation. .. versionadded:: 1.0.0 - - .. code-block:: kv - - MDNavigationDrawer: - - MDNavigationDrawerMenu: - - MDNavigationDrawerLabel: - text: "Mail" - - MDNavigationDrawerDivider: - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-divider.png - :align: center - """ - - padding = VariableListProperty(["20dp", "12dp", 0, "12dp"]) - """ - Padding between layout box and children: [padding_left, padding_top, - padding_right, padding_bottom]. - - Padding also accepts a two argument form [padding_horizontal, - padding_vertical] and a one argument form [padding]. - - :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` - and defaults to `['20dp', '12dp', 0, '12dp']`. """ - color = ColorProperty(None) - """ - Divider color in (r, g, b, a) or string format. - :attr:`color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. +class MDNavigationDrawerHeader(DeclarativeBehavior, BoxLayout): """ + Implements a header class. - -class MDNavigationDrawerHeader(MDBoxLayout): - """ - Implements a header for a menu for :class:`~MDNavigationDrawer` class. - - For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` - class documentation. + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. .. versionadded:: 1.0.0 - - .. code-block:: kv - - MDNavigationDrawer: - - MDNavigationDrawerMenu: - - MDNavigationDrawerHeader: - title: "Header title" - text: "Header text" - spacing: "4dp" - padding: "12dp", 0, 0, "56dp" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-header.png - :align: center - """ - - source = StringProperty() """ - Image logo path. - - .. code-block:: kv - MDNavigationDrawer: - - MDNavigationDrawerMenu: - - MDNavigationDrawerHeader: - title: "Header title" - text: "Header text" - source: "logo.png" - spacing: "4dp" - padding: "12dp", 0, 0, "56dp" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-header-source.png - :align: center - :attr:`source` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. +class MDNavigationDrawerItem( + MDListItem, FocusBehavior, BaseNavigationDrawerItem +): """ + Implements an item for the :class:`~MDNavigationDrawer` menu list. - title = StringProperty() - """ - Title shown in the first line. - - :attr:`title` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ + For more information, see in the + :class:`~kivymd.uix.list.list.MDListItem` and + :class:`~kivymd.uix.behaviors.focus_behavior.FocusBehavior` and + :class:`~BaseNavigationDrawerItem` + classes documentation. - title_halign = StringProperty("left") + .. versionadded:: 1.0.0 """ - Title halign first line. - :attr:`title_halign` is a :class:`~kivy.properties.StringProperty` - and defaults to `'left'`. + active_indicator_color = ColorProperty(None) """ + The active indicator color in (r, g, b, a) or string format. - title_color = ColorProperty(None) - """ - Title text color in (r, g, b, a) or string format. + .. versionadded:: 2.0.0 - :attr:`title_color` is a :class:`~kivy.properties.ColorProperty` + :attr:`active_indicator_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - title_font_style = StringProperty("H4") - """ - Title shown in the first line. - - :attr:`title_font_style` is a :class:`~kivy.properties.StringProperty` - and defaults to `'H4'`. - """ - - title_font_size = StringProperty("34sp") + inactive_indicator_color = ColorProperty(None) """ - Title shown in the first line. + The inactive indicator color in (r, g, b, a) or string format. - :attr:`title_font_size` is a :class:`~kivy.properties.StringProperty` - and defaults to `'34sp'`. - """ - - text = StringProperty() - """ - Text shown in the second line. - - :attr:`text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - text_halign = StringProperty("left") - """ - Text halign first line. - - :attr:`text_halign` is a :class:`~kivy.properties.StringProperty` - and defaults to `'left'`. - """ - - text_color = ColorProperty(None) - """ - Title text color in (r, g, b, a) or string format. + .. versionadded:: 2.0.0 - :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` + :attr:`inactive_indicator_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - text_font_style = StringProperty("H6") - """ - Title shown in the first line. - - :attr:`text_font_style` is a :class:`~kivy.properties.StringProperty` - and defaults to `'H6'`. - """ - - text_font_size = StringProperty("20sp") - """ - Title shown in the first line. - - :attr:`text_font_size` is a :class:`~kivy.properties.StringProperty` - and defaults to `'20sp'`. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - Clock.schedule_once(self.check_content) + def add_widget(self, widget, *args, **kwargs): + if isinstance( + widget, + ( + MDNavigationDrawerItemLeadingIcon, + MDNavigationDrawerItemText, + MDNavigationDrawerItemTrailingText, + ), + ): + widget._drawer_item = self + return super().add_widget(widget) - def check_content(self, interval: Union[int, float]) -> None: - """Removes widgets that the user has not added to the container.""" + def on_release(self, *args) -> None: + """ + Fired when the item is released + (i.e. the touch/click that pressed the item goes away). + """ - if not self.title: - self.ids.label_box.remove_widget(self.ids.title) - if not self.text: - self.ids.label_box.remove_widget(self.ids.text) - if not self.source: - self.remove_widget(self.ids.logo) + self.selected = not self.selected + self._drawer_menu.update_items_color(self) -class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior): +class MDNavigationDrawerItemLeadingIcon( + MDListItemLeadingIcon, BaseNavigationDrawerItem +): """ - Implements an item for the :class:`~MDNavigationDrawer` menu list. + Implements the leading icon for the menu list item. For more information, see in the - :class:`~kivymd.uix.list.OneLineAvatarIconListItem` and - :class:`~kivymd.uix.behaviors.FocusBehavior` - class documentation. - - .. versionadded:: 1.0.0 - - .. code-block:: kv - - MDNavigationDrawer: - - MDNavigationDrawerMenu: - - MDNavigationDrawerHeader: - title: "Header title" - text: "Header text" - spacing: "4dp" - padding: "12dp", 0, 0, "56dp" + :class:`~kivymd.uix.list.list.MDListItemLeadingIcon` and + :class:`~BaseNavigationDrawerItem` + classes documentation. - MDNavigationDrawerItem - icon: "gmail" - right_text: "+99" - text: "Inbox" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-item.png - :align: center + .. versionadded:: 2.0.0 """ - selected = BooleanProperty(False) - """ - Is the item selected. - - :attr:`selected` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - icon = StringProperty() - """ - Icon item. - - :attr:`icon` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - icon_color = ColorProperty(None) +class MDNavigationDrawerItemText( + MDListItemSupportingText, BaseNavigationDrawerItem +): """ - Icon color in (r, g, b, a) or string format item. + Implements the text for the menu list item. - :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - selected_color = ColorProperty([0, 0, 0, 1]) - """ - The color in (r, g, b, a) or string format of the icon and text of the - selected item. + For more information, see in the + :class:`~kivymd.uix.list.list.MDListItemSupportingText` and + :class:`~BaseNavigationDrawerItem` + classes documentation. - :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 1]`. + .. versionadded:: 2.0.0 """ - right_text = StringProperty() - """ - Right text item. - :attr:`right_text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. +class MDNavigationDrawerItemTrailingText( + MDListItemTrailingSupportingText, BaseNavigationDrawerItem +): """ + Implements the supporting text for the menu list item. - text_right_color = ColorProperty(None) - """ - Right text color item in (r, g, b, a) or string format. + For more information, see in the + :class:`~kivymd.uix.list.list.MDListItemTrailingSupportingText` and + :class:`~BaseNavigationDrawerItem` + classes documentation. - :attr:`text_right_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + .. versionadded:: 2.0.0 """ - _text_color = None - _text_right_color = None - # kivymd.uix.navigationdrawer.navigationdrawer.MDNavigationDrawerMenu - _drawer_menu = ObjectProperty() - class MDNavigationDrawerMenu(MDScrollView): """ @@ -1004,63 +827,59 @@ class MDNavigationDrawerMenu(MDScrollView): """ def add_widget(self, widget, *args, **kwargs): - if isinstance(widget, MDList): + if isinstance(widget, GridLayout): return super().add_widget(widget, *args, **kwargs) else: if isinstance(widget, MDNavigationDrawerItem): widget._drawer_menu = self self.ids.menu.add_widget(widget) - def reset_active_color(self, item: MDNavigationDrawerItem) -> None: + def update_items_color(self, item: MDNavigationDrawerItem) -> None: for widget in self.ids.menu.children: if issubclass(widget.__class__, MDNavigationDrawerItem): - if widget != item: - widget.selected = False + if widget is not item: + widget.md_bg_color = ( + widget.theme_cls.surfaceContainerLowColor + if not widget.inactive_indicator_color + else widget.inactive_indicator_color + ) else: - widget.selected = True - - if ( - issubclass(widget.__class__, MDNavigationDrawerItem) - and widget != item - ): - if widget._text_color: - widget.text_color = widget._text_color + widget.md_bg_color = ( + widget.theme_cls.secondaryContainerColor + if not widget.active_indicator_color + else widget.active_indicator_color + ) class MDNavigationDrawer(MDCard): - type = OptionProperty("modal", options=("standard", "modal")) """ - Type of drawer. Modal type will be on top of screen. Standard type will be - at left or right of screen. Also it automatically disables - :attr:`close_on_click` and :attr:`enable_swiping` to prevent closing - drawer for standard type. + Navigation drawer class. - For more information, see in the :class:`~kivymd.uix.card.MDCard` + For more information, see in the :class:`~kivymd.uix.card.card.MDCard` class documentation. - Standard - -------- - - .. code-block:: kv - - MDNavigationDrawer: - type: "standard" + :Events: - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-standard.gif - :align: center + .. versionadded:: 2.0.0 - Modal - ----- + `on_open`: + Fired when the navigation drawer is opened. + `on_close`: + Fired when the navigation drawer is closed. + """ - .. code-block:: kv + drawer_type = OptionProperty("modal", options=("standard", "modal")) + """ + Type of drawer. Modal type will be on top of screen. Standard type will be + at left or right of screen. Also it automatically disables + :attr:`close_on_click` and :attr:`enable_swiping` to prevent closing + drawer for standard type. - MDNavigationDrawer: - type: "modal" + .. versionchanged:: 2.0.0 - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-modal.gif - :align: center + Rename from `type` to `drawer_type`. - :attr:`type` is a :class:`~kivy.properties.OptionProperty` + :attr:`drawer_type` is a :class:`~kivy.properties.OptionProperty` and defaults to `'modal'`. """ @@ -1069,28 +888,6 @@ class documentation. Anchoring screen edge for drawer. Set it to `'right'` for right-to-left languages. Available options are: `'left'`, `'right'`. - Left - ---- - - .. code-block:: kv - - MDNavigationDrawer: - anchor: "left" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-type-left.png - :align: center - - Right - ----- - - .. code-block:: kv - - MDNavigationDrawer: - anchor: "right" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-type-right.png - :align: center - :attr:`anchor` is a :class:`~kivy.properties.OptionProperty` and defaults to `'left'`. """ @@ -1102,20 +899,11 @@ class documentation. multiplied with :attr:`_scrim_alpha`. Set fourth channel to 0 if you want to disable scrim. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-scrim-color.png - :align: center - - .. code-block:: kv - - MDNavigationDrawer: - scrim_color: 0, 0, 0, .8 - # scrim_color: 0, 0, 0, .2 - :attr:`scrim_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0.5]`. """ - padding = VariableListProperty([16, 16, 12, 16]) + padding = VariableListProperty([dp(16), dp(16), dp(12), dp(16)]) """ Padding between layout box and children: [padding_left, padding_top, padding_right, padding_bottom]. @@ -1128,13 +916,13 @@ class documentation. .. code-block:: kv MDNavigationDrawer: - padding: 56, 56, 12, 16 + padding: "56dp" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-padding.png :align: center :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and - defaults to '[16, 16, 12, 16]'. + defaults to '[dp(16), dp(16), dp(12), dp(16)]'. """ close_on_click = BooleanProperty(True) @@ -1215,7 +1003,7 @@ class documentation. def _get_scrim_alpha(self): _scrim_alpha = 0 - if self.type == "modal": + if self.drawer_type == "modal": _scrim_alpha = self._scrim_alpha_transition(self.open_progress) if ( isinstance(self.parent, MDNavigationLayout) @@ -1288,8 +1076,42 @@ def _get_scrim_alpha_transition(self): and defaults to `0.2`. """ + background_color = ColorProperty(None) + """ + The drawer background color in (r, g, b, a) or string format. + + .. versionadded:: 2.0.0 + + :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + theme_elevation_level = "Custom" + """ + Drawer elevation level scheme name. + + .. versionadded:: 2.0.0 + + Available options are: `'Primary'`, `'Custom'`. + + :attr:`theme_elevation_level` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Custom'`. + """ + + elevation_level = 1 + """ + Drawer elevation level (values from 0 to 5) + + .. versionadded:: 2.2.0 + + :attr:`elevation_level` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `2`. + """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.register_event_type("on_open") + self.register_event_type("on_close") self.bind( open_progress=self.update_status, status=self.update_status, @@ -1297,6 +1119,9 @@ def __init__(self, *args, **kwargs): ) Window.bind(on_keyboard=self._handle_keyboard) + def set_properties_widget(self) -> None: + pass + def set_state(self, new_state="toggle", animation=True) -> None: """ Change state of the side panel. @@ -1310,26 +1135,30 @@ def set_state(self, new_state="toggle", animation=True) -> None: Animation.cancel_all(self, "open_progress") self.status = "opening_with_animation" if animation: - Animation( + anim = Animation( open_progress=1.0, d=self.opening_time * (1 - self.open_progress), t=self.opening_transition, - ).start(self) + ) + anim.bind(on_complete=self._check_state) + anim.start(self) else: self.open_progress = 1 else: # "close" Animation.cancel_all(self, "open_progress") self.status = "closing_with_animation" if animation: - Animation( + anim = Animation( open_progress=0.0, d=self.closing_time * self.open_progress, t=self.closing_transition, - ).start(self) + ) + anim.bind(on_complete=self._check_state) + anim.start(self) else: self.open_progress = 0 - def update_status(self, *_) -> None: + def update_status(self, *args) -> None: status = self.status if status == "closed": self.state = "close" @@ -1365,7 +1194,7 @@ def on_touch_down(self, touch): for child in self.children[:]: if child.dispatch("on_touch_down", touch): return True - if self.type == "standard" and not self.collide_point( + if self.drawer_type == "standard" and not self.collide_point( touch.ox, touch.oy ): return False @@ -1412,7 +1241,7 @@ def on_touch_up(self, touch): touch.ox, touch.oy ): self.set_state("close", animation=True) - elif self.type == "standard" and not self.collide_point( + elif self.drawer_type == "standard" and not self.collide_point( touch.ox, touch.oy ): return False @@ -1421,17 +1250,35 @@ def on_touch_up(self, touch): return True def on_radius(self, instance_navigation_drawer, radius_value: list) -> None: + """Fired when the :attr:`radius` value changes.""" + self._radius = radius_value - def on_type(self, instance_navigation_drawer, drawer_type: str) -> None: - if self.type == "standard": + def on_drawer_type( + self, instance_navigation_drawer, drawer_type: str + ) -> None: + """Fired when the :attr:`drawer_type` value changes.""" + + if self.drawer_type == "standard": self.enable_swiping = False self.close_on_click = False else: self.enable_swiping = True self.close_on_click = True + def on_open(self, *args) -> None: + """Fired when the navigation drawer is opened.""" + + def on_close(self, *args) -> None: + """Fired when the navigation drawer is closed.""" + def _handle_keyboard(self, window, key, *largs): if key == 27 and self.status == "opened" and self.close_on_click: self.set_state("close") return True + + def _check_state(self, *args): + if self.state == "open": + self.dispatch("on_open") + elif self.state == "close": + self.dispatch("on_close") diff --git a/kivymd/uix/navigationrail/__init__.py b/kivymd/uix/navigationrail/__init__.py index 7d3ff7582..9076b4cea 100644 --- a/kivymd/uix/navigationrail/__init__.py +++ b/kivymd/uix/navigationrail/__init__.py @@ -2,6 +2,8 @@ from .navigationrail import ( MDNavigationRail, MDNavigationRailFabButton, + MDNavigationRailItemIcon, + MDNavigationRailItemLabel, MDNavigationRailItem, MDNavigationRailMenuButton, ) diff --git a/kivymd/uix/navigationrail/navigationrail.kv b/kivymd/uix/navigationrail/navigationrail.kv index e2623bbc5..712afdb9b 100644 --- a/kivymd/uix/navigationrail/navigationrail.kv +++ b/kivymd/uix/navigationrail/navigationrail.kv @@ -1,157 +1,109 @@ - pos_hint: {"center_x": .5, "top": 1} + pos_hint: {"center_x": .5} + theme_icon_color: "Custom" + icon_color: self.theme_cls.onSurfaceVariantColor type: "standard" pos_hint: {"center_x": .5} - - - - size_hint: None, 1 - width: "80dp" - - PanelRoot: - id: box_buttons - - PanelItems: - id: box_items - orientation: "vertical" - spacing: "12dp" - adaptive_size: True - pos_hint: {"center_x": .5} + theme_bg_color: "Custom" + md_bg_color: self.theme_cls.primaryColor + theme_icon_color: "Custom" + icon_color: self.theme_cls.onPrimaryColor orientation: "vertical" size_hint: None, None - size: self.navigation_rail.width if self.navigation_rail else 100, "56dp" - - RelativeLayout: - id: container - size_hint: None, None - size: root.size + width: "80dp" + height: self.minimum_height + spacing: + ( \ + { \ + "selected": "8dp", \ + "labeled": "16dp", \ + "unselected": "16dp", \ + }[self._navigation_rail.type] \ + ) \ + if self._navigation_rail else 0 - RippleWidget: - id: ripple_widget - size_hint: None, None - size: (container.width, container.width) - radius: container.width / 2 - scale_value_x: 0 - scale_value_y: 0 - scale_value_z: 0 - opacity: 0 - md_bg_color: - root.navigation_rail.ripple_color_item \ - if root.navigation_rail and \ - root.navigation_rail.ripple_color_item else \ - app.theme_cls.primary_color - MDIcon: - id: icon - icon: root.icon - opposite_colors: root.opposite_colors - font_size: "24sp" - pos_hint: {"center_x": .5} - badge_icon: root.badge_icon - badge_font_size: root.badge_font_size - badge_icon_color: - root.badge_icon_color \ - if root.badge_icon_color else \ - (1, 1, 1, 1) - badge_bg_color: - root.badge_bg_color \ - if root.badge_bg_color else \ - app.theme_cls.error_color - theme_text_color: "Custom" - text_color: + + canvas.before: + Color: + rgba: + ( \ ( \ - root.navigation_rail.icon_color_item_normal \ - if root.navigation_rail \ - and root.navigation_rail.icon_color_item_normal else \ - app.theme_cls.text_color \ + self.theme_cls.secondaryContainerColor \ + if not self.active_indicator_color else \ + self.active_indicator_color \ + )[:-1] + [self._alpha] \ ) \ - if not root.active else \ + if self._layer_color == self.theme_cls.transparentColor else \ + self._layer_color + RoundedRectangle: + group: "navigation-rail-rounded-rectangle" + radius: ( \ - root.navigation_rail.icon_color_item_active \ - if root.navigation_rail.icon_color_item_active else \ - app.theme_cls.text_color \ - ) - y: - container.height - \ + [ \ ( \ - (self.height + dp(4)) \ - if root.navigation_rail and \ - root.navigation_rail.type == "unselected" else \ - (self.height - dp(8)) \ - ) - - canvas.before: - Color: - rgba: - ( \ - ( \ - ( \ - app.theme_cls.primary_color \ - if not root.navigation_rail.selected_color_background else \ - root.navigation_rail.selected_color_background \ - ) \ - if root._release else \ - (0, 0, 0, 0) \ - ) \ - ) \ - if root.active else \ - (0, 0, 0, 0) - RoundedRectangle: - radius: - [root._selected_region_width / 2,] \ - if root.navigation_rail and \ - root.navigation_rail.type == "unselected" else \ - [root._selected_region_width / 4,] - size: - root._selected_region_width, \ - root._selected_region_width \ - if root.navigation_rail and \ - root.navigation_rail.type == "unselected" else \ - root._selected_region_width / 2 - pos: - self.center_x - self.width - dp(4), \ - self.center_y - root._selected_region_width / 2 \ - if root.navigation_rail and \ - root.navigation_rail.type == "unselected" else \ - self.center_y - root._selected_region_width / 4 - - MDLabel: - id: label - text: root.text - size_hint_x: None - text_size: None, root.height - adaptive_height: True - opposite_colors: root.opposite_colors - pos_hint: {"center_x": .5} - y: "16" - font_style: "Body2" - theme_text_color: "Custom" - font_name: - root.navigation_rail.font_name \ - if root.navigation_rail else \ - "Roboto" - text_color: + dp(16) \ + if self._navigation_rail.type != "unselected" else \ + dp(28) \ + ), \ + ] \ + ) \ + if self._navigation_rail else [0, ] + pos: + self.center_x - self._selected_region_width / 2, \ ( \ - root.navigation_rail.text_color_item_normal \ - if root.navigation_rail and \ - root.navigation_rail.text_color_item_normal else \ - app.theme_cls.text_color \ + self.y \ + - ( \ + dp(4) \ + if self._navigation_rail.type != "unselected" else \ + dp(16) \ ) \ - if not root.active else \ + ) \ + if self._navigation_rail else 0 + size: + self._selected_region_width, \ ( \ - root.navigation_rail.text_color_item_active \ - if root.navigation_rail.text_color_item_active else \ - app.theme_cls.text_color \ - ) - opacity: - (0 if root.navigation_rail and \ - root.navigation_rail.type == "unselected" else 1) \ - if root.navigation_rail and \ - root.navigation_rail.type != "selected" else \ - (0 if not root.active else 1) + self.height \ + + ( \ + dp(8) \ + if self._navigation_rail.type != "unselected" else \ + self.width + dp(8)\ + ) \ + ) \ + if self._navigation_rail else 0 + + pos_hint: {"center_x": .5} + + + + size_hint_y: None + height: 0 + halign: "center" + text_color: self.theme_cls.onSurfaceVariantColor + font_style: "Label" + role: "medium" + + + + size_hint: None, 1 + width: "80dp" + md_bg_color: self.theme_cls.surfaceColor + + BoxLayout: + id: box_items + orientation: "vertical" + size_hint: None, None + size: self.minimum_size + pos_hint: {"center_x": .5} + spacing: + { \ + "selected": "12dp", \ + "labeled": "24dp", \ + "unselected": "36dp", \ + }[root.type] diff --git a/kivymd/uix/navigationrail/navigationrail.py b/kivymd/uix/navigationrail/navigationrail.py index 762c74d6c..239048104 100644 --- a/kivymd/uix/navigationrail/navigationrail.py +++ b/kivymd/uix/navigationrail/navigationrail.py @@ -6,16 +6,19 @@ .. seealso:: - `Material Design spec, Navigation rail `_ - -.. rubric:: Navigation rails provide access to primary destinations in apps - when using tablet and desktop screens. + `Material Design spec, Navigation rail `_ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail.png :align: center -Usage ------ +.. rubric:: Navigation rails let people switch between UI views on mid-sized + devices. + +- Can contain 3-7 destinations plus an optional FAB +- Always put the rail in the same place, even on different screens of an app + +Example +------- .. tabs:: @@ -24,34 +27,54 @@ .. code-block:: python from kivy.lang import Builder + from kivy.properties import StringProperty from kivymd.app import MDApp + from kivymd.uix.navigationrail import MDNavigationRailItem KV = ''' + + + MDNavigationRailItemIcon: + icon: root.icon + + MDNavigationRailItemLabel: + text: root.text + + MDBoxLayout: MDNavigationRail: + type: "selected" + + MDNavigationRailMenuButton: + icon: "menu" - MDNavigationRailItem: - text: "Python" - icon: "language-python" + MDNavigationRailFabButton: + icon: "home" - MDNavigationRailItem: - text: "JavaScript" - icon: "language-javascript" + CommonNavigationRailItem: + icon: "folder-outline" + text: "Files" - MDNavigationRailItem: - text: "CPP" - icon: "language-cpp" + CommonNavigationRailItem: + icon: "bookmark-outline" + text: "Bookmark" - MDNavigationRailItem: - text: "Git" - icon: "git" + CommonNavigationRailItem: + icon: "library-outline" + text: "Library" MDScreen: + md_bg_color: self.theme_cls.secondaryContainerColor ''' + class CommonNavigationRailItem(MDNavigationRailItem): + text = StringProperty() + icon = StringProperty() + + class Example(MDApp): def build(self): return Builder.load_string(KV) @@ -63,36 +86,64 @@ def build(self): .. code-block:: python + from kivy.clock import Clock + from kivy.properties import StringProperty + from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem + from kivymd.uix.navigationrail import ( + MDNavigationRailItem, + MDNavigationRail, + MDNavigationRailMenuButton, + MDNavigationRailFabButton, + MDNavigationRailItemIcon, + MDNavigationRailItemLabel, + ) + from kivymd.uix.screen import MDScreen + + + class CommonNavigationRailItem(MDNavigationRailItem): + text = StringProperty() + icon = StringProperty() + + def on_icon(self, instance, value): + def on_icon(*ars): + self.add_widget(MDNavigationRailItemIcon(icon=value)) + Clock.schedule_once(on_icon) + + def on_text(self, instance, value): + def on_text(*ars): + self.add_widget(MDNavigationRailItemLabel(text=value)) + Clock.schedule_once(on_text) class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return ( - MDBoxLayout( - MDNavigationRail( - MDNavigationRailItem( - text="Python", - icon="language-python", - ), - MDNavigationRailItem( - text="JavaScript", - icon="language-javascript", - ), - MDNavigationRailItem( - text="CPP", - icon="language-cpp", - ), - MDNavigationRailItem( - text="Git", - icon="git", - ), - ) - ) + return MDBoxLayout( + MDNavigationRail( + MDNavigationRailMenuButton( + icon="menu", + ), + MDNavigationRailFabButton( + icon="home", + ), + CommonNavigationRailItem( + icon="bookmark-outline", + text="Files", + ), + CommonNavigationRailItem( + icon="folder-outline", + text="Bookmark", + ), + CommonNavigationRailItem( + icon="library-outline", + text="Library", + ), + type="selected", + ), + MDScreen( + md_bg_color=self.theme_cls.secondaryContainerColor, + ), ) @@ -104,476 +155,220 @@ def build(self): Anatomy ------- -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy.png - :align: center +.. code-block:: kv -1. Container -2. Label text (optional) -3. Icon -4. Active indicator -5. Badge (optional) -6. Large badge (optional) -7. Large badge label (optional) -8. Menu icon (optional) + MDNavigationRail: -Example -======= + # Optional. + MDNavigationRailMenuButton: + icon: "menu" -.. tabs:: + # Optional. + MDNavigationRailFabButton: + icon: "home" - .. tab:: Declarative KV and imperative python styles + MDNavigationRailItem - .. code-block:: python + MDNavigationRailItemIcon: + icon: icon - from kivy.clock import Clock - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.behaviors import CommonElevationBehavior - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.button import MDFillRoundFlatIconButton - from kivymd.uix.label import MDLabel - from kivymd.uix.screen import MDScreen - - KV = ''' - #:import FadeTransition kivy.uix.screenmanager.FadeTransition - - - - elevation: 1 - shadow_radius: 12 - -height: "56dp" - - - - focus_color: "#e7e4c0" - unfocus_color: "#fffcf4" + MDNavigationRailItemLabel: + text: text + [...] - MDScreen: - - MDNavigationLayout: - - ScreenManager: - - MDScreen: - - MDBoxLayout: - orientation: "vertical" - - MDBoxLayout: - adaptive_height: True - md_bg_color: "#fffcf4" - padding: "12dp" - - MDLabel: - text: "12:00" - adaptive_height: True - pos_hint: {"center_y": .5} - - MDBoxLayout: - - MDNavigationRail: - id: navigation_rail - md_bg_color: "#fffcf4" - selected_color_background: "#e7e4c0" - ripple_color_item: "#e7e4c0" - on_item_release: app.switch_screen(*args) +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy.png + :align: center - MDNavigationRailMenuButton: - on_release: nav_drawer.set_state("open") +Anatomy item +------------ - MDNavigationRailFabButton: - md_bg_color: "#b0f0d6" +.. code-block:: kv - MDNavigationRailItem: - text: "Python" - icon: "language-python" + MDNavigationRailItem - MDNavigationRailItem: - text: "JavaScript" - icon: "language-javascript" + MDNavigationRailItemIcon: + icon: icon - MDNavigationRailItem: - text: "CPP" - icon: "language-cpp" + MDNavigationRailItemLabel: + text: text - MDNavigationRailItem: - text: "Swift" - icon: "language-swift" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy-item.png + :align: center - ScreenManager: - id: screen_manager - transition: - FadeTransition(duration=.2, clearcolor=app.theme_cls.bg_dark) +Configurations +============== - MDNavigationDrawer: - id: nav_drawer - radius: 0, 16, 16, 0 - md_bg_color: "#fffcf4" - elevation: 2 - width: "240dp" +Rail types +---------- - MDNavigationDrawerMenu: +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type.png + :align: center - MDBoxLayout: - orientation: "vertical" - adaptive_height: True - spacing: "12dp" - padding: 0, 0, 0, "12dp" +1. Selected +2. Unselected +3. Labeled - MDIconButton: - icon: "menu" +Selected +-------- - MDBoxLayout: - adaptive_height: True - padding: "12dp", 0, 0, 0 +.. code-block:: kv - ExtendedButton: - text: "Compose" - icon: "pencil" + MDNavigationRail: + type: "selected" # default - DrawerClickableItem: - text: "Python" - icon: "language-python" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-selected.gif + :align: center - DrawerClickableItem: - text: "JavaScript" - icon: "language-javascript" +Unselected +---------- - DrawerClickableItem: - text: "CPP" - icon: "language-cpp" +.. code-block:: kv - DrawerClickableItem: - text: "Swift" - icon: "language-swift" - ''' + MDNavigationRail: + type: "unselected" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-unselected.gif + :align: center - class ExtendedButton(MDFillRoundFlatIconButton, CommonElevationBehavior): - ''' - Implements a button of type - `Extended FAB `_. +Labeled +------- - .. rubric:: - Extended FABs help people take primary actions. - They're wider than FABs to accommodate a text label and larger target - area. +.. code-block:: kv - This type of buttons is not yet implemented in the standard widget set - of the KivyMD library, so we will implement it ourselves in this class. - ''' + MDNavigationRail: + type: "labeled" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.padding = "16dp" - Clock.schedule_once(self.set_spacing) +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-labeled.gif + :align: center - def set_spacing(self, interval): - self.ids.box.spacing = "12dp" +Rail anchored +------------- - def set_radius(self, *args): - if self.rounded_button: - value = self.height / 4 - self.radius = [value, value, value, value] - self._radius = value +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anchored.png + :align: center +1. Top +2. Center +3. Bottom - class Example(MDApp): - def build(self): - self.theme_cls.material_style = "M3" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) +Top +--- - def switch_screen( - self, instance_navigation_rail, instance_navigation_rail_item - ): - ''' - Called when tapping on rail menu items. Switches application screens. - ''' +.. code-block:: kv - self.root.ids.screen_manager.current = ( - instance_navigation_rail_item.icon.split("-")[1].lower() - ) + MDNavigationRail: + anchor: "top" - def on_start(self): - '''Creates application screens.''' - - navigation_rail_items = self.root.ids.navigation_rail.get_items()[:] - navigation_rail_items.reverse() - - for widget in navigation_rail_items: - name_screen = widget.icon.split("-")[1].lower() - screen = MDScreen( - name=name_screen, - md_bg_color="#edd769", - radius=[18, 0, 0, 0], - ) - box = MDBoxLayout(padding="12dp") - label = MDLabel( - text=name_screen.capitalize(), - font_style="H1", - halign="right", - adaptive_height=True, - shorten=True, - ) - box.add_widget(label) - screen.add_widget(box) - self.root.ids.screen_manager.add_widget(screen) +Center +------ +.. code-block:: kv - Example().run() + MDNavigationRail: + anchor: "center" # default - .. tab:: Declarative python style +Bottom +------ - .. code-block:: python +.. code-block:: kv - from kivy.clock import Clock - from kivy.metrics import dp + MDNavigationRail: + anchor: "bottom" - from kivymd.app import MDApp - from kivymd.uix.behaviors import CommonElevationBehavior - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.button import MDFillRoundFlatIconButton, MDIconButton - from kivymd.uix.label import MDLabel - from kivymd.uix.navigationdrawer import ( - MDNavigationDrawerItem, - MDNavigationLayout, - MDNavigationDrawer, - MDNavigationDrawerMenu, - ) - from kivymd.uix.navigationrail import ( - MDNavigationRail, - MDNavigationRailMenuButton, - MDNavigationRailFabButton, - MDNavigationRailItem, - ) - from kivymd.uix.screen import MDScreen - from kivymd.uix.screenmanager import MDScreenManager +API break +========= +1.2.0 version +------------- - class DrawerClickableItem(MDNavigationDrawerItem): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.focus_color = "#e7e4c0" - self.unfocus_color = self.theme_cls.bg_light - self.radius = 24 +.. code-block:: kv + MDNavigationRail: - class ExtendedButton(MDFillRoundFlatIconButton, CommonElevationBehavior): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.padding = "16dp" - self.elevation = 1 - self.shadow_radius = 12 - self.height = dp(56) - Clock.schedule_once(self.set_spacing) + MDNavigationRailMenuButton: + icon: "menu" - def set_spacing(self, interval): - self.ids.box.spacing = "12dp" + MDNavigationRailFabButton: + icon: "home" - def set_radius(self, *args): - if self.rounded_button: - self._radius = self.radius = self.height / 4 + MDNavigationRailItem: + icon: icon + text: text + [...] - class Example(MDApp): - def build(self): - self.theme_cls.material_style = "M3" - self.theme_cls.primary_palette = "Orange" - return MDScreen( - MDNavigationLayout( - MDScreenManager( - MDScreen( - MDBoxLayout( - MDBoxLayout( - MDLabel( - text="12:00", - adaptive_height=True, - pos_hint={"center_y": 0.5}, - ), - adaptive_height=True, - md_bg_color="#fffcf4", - padding="12dp", - ), - MDBoxLayout( - MDNavigationRail( - MDNavigationRailMenuButton( - on_release=self.open_nav_drawer, - ), - MDNavigationRailFabButton( - md_bg_color="#b0f0d6", - ), - MDNavigationRailItem( - text="Python", - icon="language-python", - ), - MDNavigationRailItem( - text="JavaScript", - icon="language-javascript", - ), - MDNavigationRailItem( - text="CPP", - icon="language-cpp", - ), - MDNavigationRailItem( - text="Swift", - icon="language-swift", - ), - id="navigation_rail", - md_bg_color="#fffcf4", - selected_color_background="#e7e4c0", - ripple_color_item="#e7e4c0", - ), - MDScreenManager( - id="screen_manager_content", - ), - id="root_box", - ), - id="box_rail", - orientation="vertical", - ), - id="box", - ), - id="screen", - ), - id="screen_manager", - ), - MDNavigationDrawer( - MDNavigationDrawerMenu( - MDBoxLayout( - MDIconButton( - icon="menu", - ), - MDBoxLayout( - ExtendedButton( - text="Compose", - icon="pencil", - ), - adaptive_height=True, - padding=["12dp", 0, 0, 0], - ), - orientation="vertical", - adaptive_height=True, - spacing="12dp", - padding=("3dp", 0, 0, "12dp"), - ), - DrawerClickableItem( - text="Python", - icon="language-python", - ), - DrawerClickableItem( - text="JavaScript", - icon="language-javascript", - ), - DrawerClickableItem( - text="CPP", - icon="language-cpp", - ), - DrawerClickableItem( - text="Swift", - icon="language-swift", - ), - ), - id="nav_drawer", - radius=(0, 16, 16, 0), - elevation=4, - width="240dp", - ), - ) +2.2.0 version +------------- - def switch_screen(self, *args, screen_manager_content=None): - ''' - Called when tapping on rail menu items. Switches application screens. - ''' +.. code-block:: kv - instance_navigation_rail, instance_navigation_rail_item = args - screen_manager_content.current = ( - instance_navigation_rail_item.icon.split("-")[1].lower() - ) + MDNavigationRail: - def open_nav_drawer(self, *args): - self.root.ids.nav_drawer.set_state("open") - - def on_start(self): - '''Creates application screens.''' - - screen_manager = self.root.ids.screen_manager - root_box = screen_manager.ids.screen.ids.box.ids.box_rail.ids.root_box - navigation_rail = root_box.ids.navigation_rail - screen_manager_content = root_box.ids.screen_manager_content - navigation_rail_items = navigation_rail.get_items()[:] - navigation_rail_items.reverse() - navigation_rail.bind( - on_item_release=lambda *args: self.switch_screen( - *args, screen_manager_content=screen_manager_content - ) - ) + MDNavigationRailMenuButton: + icon: "menu" - for widget in navigation_rail_items: - name_screen = widget.icon.split("-")[1].lower() - screen_manager_content.add_widget( - MDScreen( - MDBoxLayout( - MDLabel( - text=name_screen.capitalize(), - font_style="H1", - halign="right", - adaptive_height=True, - shorten=True, - ), - padding="12dp", - ), - name=name_screen, - md_bg_color="#edd769", - radius=[18, 0, 0, 0], - ), - ) + MDNavigationRailFabButton: + icon: "home" + MDNavigationRailItem - Example().run() + MDNavigationRailItemIcon: + icon: icon -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-example.gif - :align: center + MDNavigationRailItemLabel: + text: text + [...] """ __all__ = ( "MDNavigationRail", "MDNavigationRailItem", + "MDNavigationRailItemIcon", + "MDNavigationRailItemLabel", "MDNavigationRailFabButton", "MDNavigationRailMenuButton", ) import os -from typing import Union from kivy.animation import Animation from kivy.clock import Clock -from kivy.core.window import Window +from kivy.graphics import ( + StencilPush, + RoundedRectangle, + StencilUse, + Color, + Ellipse, + StencilUnUse, + StencilPop, +) from kivy.lang import Builder -from kivy.logger import Logger from kivy.metrics import dp from kivy.properties import ( BooleanProperty, ColorProperty, - ListProperty, NumericProperty, - ObjectProperty, OptionProperty, - StringProperty, VariableListProperty, + ObjectProperty, ) from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.relativelayout import RelativeLayout from kivymd import uix_path -from kivymd.uix.behaviors import ScaleBehavior -from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.button import MDFloatingActionButton, MDIconButton -from kivymd.uix.card import MDCard -from kivymd.uix.floatlayout import MDFloatLayout -from kivymd.uix.widget import MDWidget +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + ScaleBehavior, + DeclarativeBehavior, + BackgroundColorBehavior, + RectangularRippleBehavior, +) +from kivymd.uix.behaviors.focus_behavior import FocusBehavior +from kivymd.uix.button import MDFabButton, MDIconButton +from kivymd.uix.label import MDIcon, MDLabel with open( os.path.join(uix_path, "navigationrail", "navigationrail.kv"), @@ -582,49 +377,22 @@ def on_start(self): Builder.load_string(kv_file.read()) -class PanelRoot(MDFloatLayout): - """ - Contains - :class:`~MDNavigationRailFabButton`, :class:`~MDNavigationRailMenuButton` - buttons and a :class:`~Paneltems` container with menu items. - """ - - -class PanelItems(MDBoxLayout): - """Box for menu items.""" - - -class RippleWidget(MDWidget, ScaleBehavior): - """ - Implements a background color for a menu item - - (:class:`~MDNavigationRailItem`). - """ - - -class MDNavigationRailFabButton(MDFloatingActionButton): +class MDNavigationRailFabButton(MDFabButton): """ - Implements an optional floating action button (FAB). + Implements a floating action button (FAB). For more information, see in the - :class:`~kivymd.uix.button.MDFloatingActionButton` class documentation. + :class:`~kivymd.uix.button.button.MDFabButton` + class documentation. """ - icon = StringProperty("pencil") + md_bg_color_disabled = ColorProperty(None) """ - Button icon name. - - .. code-block:: kv - - MDNavigationRail: + The background color in (r, g, b, a) or string format of the switch when + the widget is disabled. - MDNavigationRailFabButton: - icon: "home" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-fab-button-icon.png - :align: center - - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `'pencil'`. + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ @@ -633,228 +401,228 @@ class MDNavigationRailMenuButton(MDIconButton): Implements a menu button. For more information, see in the - :class:`~kivymd.uix.button.MDIconButton` classes documentation. + :class:`~kivymd.uix.button.button.MDIconButton` class documentation. """ - icon = StringProperty("menu") + md_bg_color_disabled = ColorProperty(None) """ - Button icon name. - - .. code-block:: kv - - MDNavigationRail: + The background color in (r, g, b, a) or string format of the switch when + the widget is disabled. - MDNavigationRailMenuButton: - icon: "home" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-menu-button-icon.png - :align: center - - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `'menu'`. + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ -class MDNavigationRailItem(ButtonBehavior, MDBoxLayout): +class MDNavigationRailItemIcon(RectangularRippleBehavior, MDIcon): """ - Implements a menu item with an icon and text. + Implements an icon for the :class:`~MDNavigationRailItem` class. For more information, see in the - :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.boxlayout.MDBoxLayout` + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivymd.uix.label.label.MDIcon` classes documentation. - """ - navigation_rail = ObjectProperty() + .. versionchanged:: 2.0.0 """ - :class:`~MDNavigationRail` object. - :attr:`navigation_rail` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. + active_indicator_color = ColorProperty(None) """ + Background color of the active indicator in (r, g, b, a) or string format. - icon = StringProperty("checkbox-blank-circle") + :attr:`active_indicator_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - Icon item. - - .. code-block:: kv - - MDNavigationRail: - MDNavigationRailItem: - icon: "language-python" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-icon.png - :align: center + _alpha = NumericProperty(0) + _active = BooleanProperty(False) + _navigation_rail = ObjectProperty() + _navigation_item = ObjectProperty() + _layer_color = ColorProperty([0, 0, 0, 0]) + _selected_region_width = NumericProperty(dp(0)) + + def anim_complete(self, *args): + super().anim_complete() + self._navigation_rail.set_active_item(self._navigation_item) + + def lay_canvas_instructions(self) -> None: + if not self.ripple_effect: + return + + canvas_rectangle = self.canvas.before.get_group( + "navigation-rail-rounded-rectangle" + )[0] + + with self.canvas.after if self.ripple_canvas_after else self.canvas.before: + if hasattr(self, "radius"): + self.radius = [ + canvas_rectangle.radius[0][0], + ] + self._round_rad = self.radius + StencilPush(group="rectangular_ripple_behavior") + RoundedRectangle( + pos=canvas_rectangle.pos, + size=canvas_rectangle.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) - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `'checkbox-blank'`. - """ - text = StringProperty() +class MDNavigationRailItemLabel(ScaleBehavior, MDLabel): """ - Text item. - - .. code-block:: kv - - MDNavigationRail: - - MDNavigationRailItem: - text: "Python" - icon: "language-python" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-text.png - :align: center + Implements an label for the :class:`~MDNavigationRailItem` class. - :attr:`text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ + For more information, see in the + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and + :class:`~kivymd.uix.label.label.MDLabel` + classes documentation. - badge_icon = StringProperty() + .. versionchanged:: 2.0.0 """ - Badge icon name. - - .. code-block:: kv - - MDNavigationRail: - MDNavigationRailItem: - text: "Python" - icon: "language-python" - badge_icon: "plus" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-icon.png - :align: center - - :attr:`badge_icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. + scale_value_y = NumericProperty(0) """ + Y-axis value. - badge_icon_color = ColorProperty(None) + :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. """ - Badge icon color in (r, g, b, a) format. - .. code-block:: kv + _active = BooleanProperty(False) - MDNavigationRail: + def on__active(self, instance, value) -> None: + """Fired when the :attr:`_active` value changes.""" - MDNavigationRailItem: - text: "Python" - icon: "language-python" - badge_icon: "plus" - badge_icon_color: 0, 0, 1, 1 + self.text_color = ( + self.theme_cls.onSurfaceColor + if value + else self.theme_cls.onSurfaceVariantColor + ) - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-icon-color.png - :align: center - :attr:`badge_icon_color` is an :class:`~kivy.properties.StringProperty` - and defaults to `None`. +class MDNavigationRailItem( + DeclarativeBehavior, + ButtonBehavior, + ThemableBehavior, + FocusBehavior, + BoxLayout, +): """ + Implements a menu item with an icon and text. - badge_bg_color = ColorProperty(None) + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.behaviors.focus_behavior.FocusBehavior` + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. """ - Badge icon background color in (r, g, b, a) format. - - .. code-block:: kv - - MDNavigationRail: - MDNavigationRailItem: - text: "Python" - icon: "language-python" - badge_icon: "plus" - badge_bg_color: "#b0f0d6" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-bg-color.png - :align: center - - :attr:`badge_bg_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + active = BooleanProperty(False) """ + Is the element active. - badge_font_size = NumericProperty(0) + :attr:`active` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. """ - Badge icon font size. - - .. code-block:: kv - - MDNavigationRail: - MDNavigationRailItem: - text: "Python" - icon: "language-python" - badge_icon: "plus" - badge_font_size: "24sp" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-font-size.png - :align: center - - :attr:`badge_font_size` is an :class:`~kivy.properties.NumericProperty` - and defaults to `0`. + radius = VariableListProperty(0, length=4) """ + Item radius. - active = BooleanProperty(False) - """ - Is the element active. + .. versionchanged:: 2.0.0 - :attr:`active` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. """ - _selected_region_width = NumericProperty("56dp") - _ripple_size = ListProperty([0, 0]) - _release = BooleanProperty(False) + _navigation_rail = ObjectProperty() + _icon_item = ObjectProperty() - def on_active( - self, instance_navigation_rail_item, value_active: bool - ) -> None: - """Called when the value of `active` changes.""" + def on_active(self, instance, value) -> None: + """Fired when the :attr:`active` value changes.""" - self.animation_size_ripple_area(1 if value_active else 0) + if value: + Clock.schedule_once(self.on_leave) - def animation_size_ripple_area(self, value: int) -> None: - """Animates the size/fade of the ripple area.""" + def on_leave(self, *args) -> None: + """Fired when the mouse goes outside the widget border.""" Animation( - scale_value_x=value, - scale_value_y=value, - scale_value_z=value, - opacity=value, - d=0.25, - t=self.navigation_rail.ripple_transition, - ).start(self.ids.ripple_widget) - - def on_press(self) -> None: - """Called when pressed on a panel element.""" + _selected_region_width=0, + d=0.2, + ).start(self._icon_item) - self._release = False - self.active = True - self.navigation_rail.deselect_item(self) - self.navigation_rail.dispatch("on_item_press", self) + def on_enter(self, *args) -> None: + """Fired when mouse enter the bbox of the widget.""" - def on_release(self) -> None: - """Called when released on a panel element.""" + if not self.active: + # FIXME: Move layer creation to + # kivymd/uix/behaviors/state_layer_behavior.py module. + self._icon_item._layer_color = self.theme_cls.onSurfaceColor[ + :-1 + ] + [0.12] + Animation( + _selected_region_width=self._icon_item.width + dp(32), + d=0.2, + ).start(self._icon_item) - self._release = True - self.animation_size_ripple_area(0) - self.navigation_rail.dispatch("on_item_release", self) + def on_leave(self, *args) -> None: + """Fired when the mouse goes outside the widget border.""" + self._icon_item._layer_color = self.theme_cls.transparentColor -class MDNavigationRail(MDCard): + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDNavigationRailItemLabel): + Clock.schedule_once(lambda x: self._check_type_rail(widget)) + elif isinstance(widget, MDNavigationRailItemIcon): + widget._navigation_rail = self._navigation_rail + widget._navigation_item = self + self._icon_item = widget + return super().add_widget(widget) + + def _check_type_rail(self, instance: MDNavigationRailItemLabel): + if self._navigation_rail.type == "labeled": + instance.scale_value_y = 1 + + +class MDNavigationRail( + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + RelativeLayout, +): """ Navigation rail class. For more information, see in the - :class:`~kivymd.uix.card.MDCard` class documentation. - - :Events: - :attr:`on_item_press` - Called on the `on_press` event of menu item - - :class:`~MDNavigationRailItem`. - - :attr:`on_item_release` - Called on the `on_release` event of menu item - - :class:`~MDNavigationRailItem`. + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.relativelayout.RelativeLayout` + classes documentation. """ radius = VariableListProperty(0, length=4) @@ -865,417 +633,145 @@ class MDNavigationRail(MDCard): and defaults to `[0, 0, 0, 0]`. """ - padding = VariableListProperty([0, "36dp", 0, "36dp"], length=4) - """ - Padding between layout box and children: - [padding_left, padding_top, padding_right, padding_bottom]. - - :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` - and defaults to `[0, '36dp', 0, '36dp']`. - """ - - anchor = OptionProperty("top", options=["top", "bottom", "center"]) + anchor = OptionProperty("center", options=["top", "bottom", "center"]) """ The position of the panel with menu items. Available options are: `'top'`, `'bottom'`, `'center'`. - .. rubric:: Top - - .. code-block:: kv - - MDNavigationRail: - anchor: "top" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anchor-top.png - :align: center - - .. rubric:: Center - - .. code-block:: kv - - MDNavigationRail: - anchor: "center" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-center.png - :align: center - - .. rubric:: Bottom - - .. code-block:: kv - - MDNavigationRail: - anchor: "bottom" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-bottom.png - :align: center - :attr:`anchor` is an :class:`~kivy.properties.OptionProperty` and defaults to `'top'`. """ type = OptionProperty( - "labeled", options=["labeled", "selected", "unselected"] + "selected", options=["labeled", "selected", "unselected"] ) """ Type of switching menu items. Available options are: `'labeled'`, `'selected'`, `'unselected'`. - .. rubric:: Labeled - - .. code-block:: kv - - MDNavigationRail: - type: "labeled" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-labeled.png - :align: center - - .. rubric:: Selected - - .. code-block:: kv - - MDNavigationRail: - type: "selected" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-selected.gif - :align: center - - .. rubric:: Unselected - - .. code-block:: kv - - MDNavigationRail: - type: "unselected" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-unselected.gif - :align: center - :attr:`type` is an :class:`~kivy.properties.OptionProperty` and defaults to `'labeled'`. """ - text_color_item_normal = ColorProperty(None) - """ - The text color in (r, g, b, a) or string format of the normal menu item - (:class:`~MDNavigationRailItem`). - - .. code-block:: kv - - MDNavigationRail: - text_color_item_normal: app.theme_cls.primary_color - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-text-color-item-normal.png - :align: center - - :attr:`text_color_item_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - text_color_item_active = ColorProperty(None) - """ - The text color in (r, g, b, a) or string format of the active menu item - (:class:`~MDNavigationRailItem`). - - .. code-block:: kv - - MDNavigationRail: - text_color_item_active: app.theme_cls.primary_color - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-text-color-item-active.png - :align: center - - :attr:`text_color_item_active` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - icon_color_item_normal = ColorProperty(None) - """ - The icon color in (r, g, b, a) or string format of the normal menu item - (:class:`~MDNavigationRailItem`). - - .. code-block:: kv - - MDNavigationRail: - icon_color_item_normal: app.theme_cls.primary_color - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-icon-color-item-normal.png - :align: center - - :attr:`icon_color_item_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - icon_color_item_active = ColorProperty(None) - """ - The icon color in (r, g, b, a) or string format of the active menu item - (:class:`~MDNavigationRailItem`). - - .. code-block:: kv - - MDNavigationRail: - icon_color_item_active: app.theme_cls.primary_color - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-icon-color-item-active.png - :align: center - - :attr:`icon_color_item_active` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - selected_color_background = ColorProperty(None) - """ - Background color which will highlight the icon of the active menu item - - :class:`~MDNavigationRailItem` - in (r, g, b, a) format. - - .. code-block:: kv - - MDNavigationRail: - selected_color_background: "#e7e4c0" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-selected-color-background.png - :align: center - - :attr:`selected_color_background` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - ripple_color_item = ColorProperty(None) - """ - Ripple effect color of menu items (:class:`~MDNavigationRailItem`) - in (r, g, b, a) format. - - .. code-block:: kv - - MDNavigationRail: - ripple_color_item: "#e7e4c0" - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-ripple-color-item.png - :align: center - - :attr:`ripple_color_item` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - ripple_transition = StringProperty("out_cubic") - """ - Type of animation of the ripple effect when a menu item is selected. - - :attr:`ripple_transition` is a :class:`~kivy.properties.StringProperty` - and defaults to `'ripple_transition'`. - """ - - current_selected_item = NumericProperty(0) - """ - Index of the menu list item (:class:`~MDNavigationRailItem`) that will be - active by default - - .. code-block:: kv - - MDNavigationRail: - current_selected_item: 1 - - MDNavigationRailItem: - ... - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-current-selected-item.png - :align: center - - :attr:`current_selected_item` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0`. - """ - - font_name = StringProperty("Roboto") - """ - Font path for menu item (:class:`~MDNavigationRailItem`) text. - - .. code-block:: kv - - MDNavigationRail: - - MDNavigationRailItem: - text: "Python" - icon: "language-python" - font_name: "nasalization-rg.ttf" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-font-name.png - :align: center - - :attr:`font_name` is an :class:`~kivy.properties.StringProperty` - and defaults to `'Roboto'`. - """ + fab_button: MDNavigationRailFabButton = ObjectProperty() + menu_button: MDNavigationRailFabButton = ObjectProperty() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - Clock.schedule_once(self.set_pos_menu_fab_buttons) - Clock.schedule_once(self.set_current_selected_item) - self.register_event_type("on_item_press") - self.register_event_type("on_item_release") - - def on_size(self, *args): - Clock.schedule_once(self.set_pos_menu_fab_buttons) - - def on_item_press(self, *args) -> None: - """ - Called on the `on_press` event of menu item - - :class:`~MDNavigationRailItem`. - """ - - def on_item_release(self, *args) -> None: - """ - Called on the `on_release` event of menu item - - :class:`~MDNavigationRailItem`. - """ - - def deselect_item( - self, selected_navigation_rail_item: MDNavigationRailItem - ) -> None: - """ - Sets the `active` value to `False` for all menu items - (:class:`~MDNavigationRailItem`) except the selected item. - Called when a menu item is touched. - """ - - for navigation_rail_item in self.ids.box_items.children: - if selected_navigation_rail_item is not navigation_rail_item: - navigation_rail_item.active = False + Clock.schedule_once(self._check_anchor) + + def on_size(self, *args) -> None: + """Fired when the application screen size changes.""" + + Clock.schedule_once(self._set_pos_menu_fab_buttons) + Clock.schedule_once(self._check_anchor) def get_items(self) -> list: - """Returns a list of :class:`~MDNavigationRailItem` objects""" + """Returns a list of :class:`~MDNavigationRailItem` objects.""" return self.ids.box_items.children - def set_pos_panel_items( - self, - instance_fab_button: Union[None, MDNavigationRailFabButton], - instance_menu_button: Union[None, MDNavigationRailFabButton], - ) -> None: - """Set :class:`~Paneltems` panel position with menu items.""" - - if self.anchor == "top": - if instance_fab_button: - self.ids.box_items.y = instance_fab_button.y - ( - len(self.ids.box_items.children) * dp(56) - + self.padding[1] * 2 - + dp(24) - ) + def set_active_item(self, item: MDNavigationRailItem) -> None: + """Sets the active menu list item.""" + + for widget in self.ids.box_items.children: + if item is widget: + widget.active = not widget.active + + for widget_item in item.children: + if isinstance(widget_item, MDNavigationRailItemLabel): + widget_item._active = widget.active + if self.type == "selected": + Animation( + scale_value_y=1 if widget.active else 0, + height=widget_item.texture_size[1] + if widget.active + else 0, + d=0.2, + ).start(widget_item) + if isinstance(widget_item, MDNavigationRailItemIcon): + widget_item._active = widget.active + widget_item._alpha = 1 if widget.active else 0 + widget_item._selected_region_width = 0 + Animation( + _selected_region_width=widget_item.width + dp(32) + if widget.active + else 0, + d=0.2, + ).start(widget_item) else: - if not instance_menu_button: - self.ids.box_items.pos_hint = {"top": 1} - else: - self.ids.box_items.y = instance_menu_button.y - ( - len(self.ids.box_items.children) * dp(56) - + self.padding[1] * 2 - ) - elif self.anchor == "center": - self.ids.box_items.pos_hint = {"center_y": 0.5} - elif self.anchor == "bottom": - self.ids.box_items.y = dp(12) - - def set_current_selected_item(self, interval: Union[int, float]) -> None: - """Sets the active menu list item (:class:`~MDNavigationRailItem`).""" + widget.active = False + for widget_item in widget.children: + widget_item._active = widget.active + if isinstance(widget_item, MDNavigationRailItemLabel): + if self.type == "selected": + Animation(scale_value_y=0, height=0, d=0.2).start( + widget_item + ) + if isinstance(widget_item, MDNavigationRailItemIcon): + Animation( + _selected_region_width=0, + _alpha=0, + d=0.2, + ).start(widget_item) - if self.ids.box_items.children: - items = self.ids.box_items.children[:] - items.reverse() + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDNavigationRailFabButton): + self.fab_button = widget + super().add_widget(widget) + elif isinstance(widget, MDNavigationRailMenuButton): + self.menu_button = widget + super().add_widget(widget) + elif isinstance(widget, MDNavigationRailItem): + self.ids.box_items.add_widget(widget) + widget._navigation_rail = self + widget._navigation_item = widget + widget.bind(on_release=self.set_active_item) + else: + return super().add_widget(widget) - if len(items) <= self.current_selected_item: - Logger.error( - f"MDNavigationRail:You have " - f"{len(self.ids.box_items.children)} menu items, but you " - f"set {self.current_selected_item} as the active item. " - f"The very first menu item will be set active." + def _set_pos_menu_fab_buttons(self, *args): + def set_pos_menu_fab_buttons(*args): + if self.fab_button and not self.menu_button: + self.fab_button.y = self.height - ( + self.fab_button.height + dp(48) ) - index = 0 - else: - index = self.current_selected_item - - items[index].dispatch("on_press") - items[index].dispatch("on_release") - - def set_pos_menu_fab_buttons(self, *args) -> None: - """ - Sets the position of the :class:`~MDNavigationRailFabButton` and - :class:`~MDNavigationRailMenuButton` buttons on the panel. - """ - - fab_button = None # MDNavigationRailFabButton - menu_button = None # MDNavigationRailMenuButton - - for widget in self.ids.box_buttons.children: - if isinstance(widget, MDNavigationRailFabButton): - fab_button = widget - if isinstance(widget, MDNavigationRailMenuButton): - menu_button = widget - - if fab_button and menu_button: - - def set_fab_button_y(interval): - fab_button.y = self.parent.height - ( - menu_button.height - + fab_button.height - + self.padding[1] - + dp(18) + elif self.menu_button and not self.fab_button: + self.menu_button.y = self.height - ( + self.menu_button.height + dp(38) ) - self.set_pos_panel_items(fab_button, menu_button) - - Clock.schedule_once(set_fab_button_y) - elif fab_button and not menu_button: - - def set_fab_button_y(interval): - fab_button.y = self.parent.height - ( - self.padding[1] + fab_button.height + elif self.fab_button and self.menu_button: + self.menu_button.y = self.height - ( + self.menu_button.height + dp(38) + ) + self.fab_button.y = self.height - ( + self.fab_button.height + dp(48) + dp(48) ) - self.set_pos_panel_items(fab_button, menu_button) - Clock.schedule_once(set_fab_button_y) - else: - Clock.schedule_once( - lambda x: self.set_pos_panel_items(fab_button, menu_button) + Clock.schedule_once(set_pos_menu_fab_buttons) + + def _check_anchor(self, *args): + def set_top_pos(*args): + anchor_button = None + if ( + self.fab_button + and not self.menu_button + or self.fab_button + and self.menu_button + ): + anchor_button = self.fab_button + elif self.menu_button and not self.fab_button: + anchor_button = self.menu_button + + self.ids.box_items.y = ( + anchor_button.y + - (len(self.ids.box_items.children) * dp(56)) + - dp(56) ) - def add_widget(self, widget, *args, **kwargs): - if isinstance(widget, MDNavigationRailFabButton): - self.ids.box_buttons.add_widget(widget) - elif isinstance(widget, MDNavigationRailMenuButton): - self.ids.box_buttons.add_widget(widget) - elif isinstance(widget, MDNavigationRailItem): - widget.navigation_rail = self - self.ids.box_items.add_widget(widget) - elif isinstance(widget, (PanelRoot, PanelItems)): - return super().add_widget(widget) + if self.anchor == "center": + self.ids.box_items.pos_hint = {"center_y": 0.5} + elif self.anchor == "top": + Clock.schedule_once(set_top_pos) + elif self.anchor == "bottom": + self.ids.box_items.y = dp(56) diff --git a/kivymd/uix/pickers/colorpicker/colorpicker.py b/kivymd/uix/pickers/colorpicker/colorpicker.py index 4dfdfd1bc..43727023a 100644 --- a/kivymd/uix/pickers/colorpicker/colorpicker.py +++ b/kivymd/uix/pickers/colorpicker/colorpicker.py @@ -562,8 +562,6 @@ def on_open(self) -> None: self._current_tab = self.gradient_tab self.ids.bottom_navigation_gradient.add_widget(self.gradient_tab) - super().on_open() - def on_select_color(self, color: list) -> None: """Called when a gradient image is clicked.""" diff --git a/kivymd/uix/recyclegridlayout.py b/kivymd/uix/recyclegridlayout.py index e519af538..a6ca3db70 100644 --- a/kivymd/uix/recyclegridlayout.py +++ b/kivymd/uix/recyclegridlayout.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -28,7 +28,7 @@ MDRecycleGridLayout: adaptive_height: True - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor Available options are: ---------------------- @@ -87,13 +87,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDRecycleGridLayout( - DeclarativeBehavior, ThemableBehavior, RecycleGridLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + RecycleGridLayout, + MDAdaptiveWidget, ): """ - Recycle grid layout layout class. For more information, see in the - :class:`~kivy.uix.recyclegridlayout.RecycleGridLayout` class documentation. + Recycle grid layout class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.recyclegridlayout.RecycleGridLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/recycleview.py b/kivymd/uix/recycleview.py index ddcf16c1b..e8467e844 100644 --- a/kivymd/uix/recycleview.py +++ b/kivymd/uix/recycleview.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -27,7 +27,7 @@ .. code-block:: kv MDRecycleView: - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor """ __all__ = ("MDRecycleView",) @@ -36,13 +36,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDRecycleView( - DeclarativeBehavior, ThemableBehavior, RecycleView, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + RecycleView, + MDAdaptiveWidget, ): """ - Recycle view class. For more information, see in the - :class:`~kivy.uix.recycleview.RecycleView` class documentation. + Recycle view class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.recycleview.RecycleView` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/relativelayout.py b/kivymd/uix/relativelayout.py index b0e58e1fa..0f243526e 100644 --- a/kivymd/uix/relativelayout.py +++ b/kivymd/uix/relativelayout.py @@ -13,7 +13,7 @@ RelativeLayout: canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor RoundedRectangle: pos: (0, 0) size: self.size @@ -26,20 +26,31 @@ MDRelativeLayout: radius: [25, ] - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor """ from kivy.uix.relativelayout import RelativeLayout from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDRelativeLayout( - DeclarativeBehavior, ThemableBehavior, RelativeLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + RelativeLayout, + MDAdaptiveWidget, ): """ - Relative layout class. For more information, see in the - :class:`~kivy.uix.relativelayout.RelativeLayout` class documentation. + Relative layout class. + + For more information see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.relativelayout.RelativeLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/screen.py b/kivymd/uix/screen.py index 1f24676cc..212d3c71e 100644 --- a/kivymd/uix/screen.py +++ b/kivymd/uix/screen.py @@ -34,15 +34,28 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior from kivymd.uix.hero import MDHeroTo -class MDScreen(DeclarativeBehavior, ThemableBehavior, Screen, MDAdaptiveWidget): +class MDScreen( + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + Screen, + MDAdaptiveWidget, +): """ Screen is an element intended to be used with a - :class:`~kivymd.uix.screenmanager.MDScreenManager`. For more information, - see in the :class:`~kivy.uix.screenmanager.Screen` class documentation. + :class:`~kivymd.uix.screenmanager.MDScreenManager`. + + For more information see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.BackgroundColorBehavior` and + :class:`~kivy.uix.screenmanager.Screen` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ hero_to = ObjectProperty(deprecated=True) @@ -71,7 +84,7 @@ class MDScreen(DeclarativeBehavior, ThemableBehavior, Screen, MDAdaptiveWidget): """ def on_hero_to(self, screen, widget: MDHeroTo) -> None: - """Called when the value of the :attr:`hero_to` attribute changes.""" + """Fired when the value of the :attr:`hero_to` attribute changes.""" if not isinstance(widget, MDHeroTo) or not issubclass( widget.__class__, MDHeroTo diff --git a/kivymd/uix/scrollview.py b/kivymd/uix/scrollview.py index 0b00061fa..cc76d5300 100644 --- a/kivymd/uix/scrollview.py +++ b/kivymd/uix/scrollview.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -27,23 +27,41 @@ .. code-block:: kv MDScrollView: - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor """ +from __future__ import annotations + __all__ = ("MDScrollView",) +from kivy.effects.dampedscroll import DampedScrollEffect from kivy.uix.scrollview import ScrollView -from kivymd.uix.behaviors import ( - DeclarativeBehavior, - SpecificBackgroundColorBehavior, -) +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior + + +class MDScrollViewEffect(DampedScrollEffect): + """ + This class is simply based on DampedScrollEffect. + If you need any documentation please look at + :class:`~kivy.effects.dampedscrolleffect`. + """ + + def on_overscroll(self, instance, overscroll: int | float) -> None: + ... -class MDScrollView( - DeclarativeBehavior, SpecificBackgroundColorBehavior, ScrollView -): +class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): """ - ScrollView class. For more information, see in the - :class:`~kivy.uix.scrollview.ScrollView` class documentation. + ScrollView class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.scrollview.ScrollView` + classes documentation. """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.effect_cls = MDScrollViewEffect diff --git a/kivymd/uix/segmentedbutton/__init__.py b/kivymd/uix/segmentedbutton/__init__.py index f516e1c7a..91ad5befc 100644 --- a/kivymd/uix/segmentedbutton/__init__.py +++ b/kivymd/uix/segmentedbutton/__init__.py @@ -1,4 +1,6 @@ from .segmentedbutton import ( # NOQA F401 MDSegmentedButton, MDSegmentedButtonItem, + MDSegmentButtonIcon, + MDSegmentButtonLabel, ) diff --git a/kivymd/uix/segmentedbutton/segmentedbutton.kv b/kivymd/uix/segmentedbutton/segmentedbutton.kv index c4b63ffc3..23ad70e59 100644 --- a/kivymd/uix/segmentedbutton/segmentedbutton.kv +++ b/kivymd/uix/segmentedbutton/segmentedbutton.kv @@ -1,32 +1,124 @@ - size_hint: None, None - height: "40dp" - opacity: 0 + size_hint_y: None + height: self.minimum_height + radius: [self.height / 2, ] + + # FIXME: The use of an additional container is due to the Kivy bug - + # https://github.com/kivy/kivy/issues/8470 + MDSegmentedButtonContainer: + id: container + size_hint: 1, None + opacity: 1 + size_hint_min_x: 0 + height: + { \ + "large": "40dp", \ + "normal": "36dp", \ + "medium": "32dp", \ + "small": "28dp", \ + }[root.type] + + + + font_style: "Label" + role: "large" + theme_line_height: "Custom" + line_height: 1 + markup: True + adaptive_width: True + pos_hint: {"center_y": .5} + padding: "12dp", 0, "12dp", 0 + text_color: + ( \ + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.ids.container.parent.active else \ + self.theme_cls.onSurfaceColor \ + ) \ + if self.theme_text_color == "Primary" else \ + ( \ + self.text_color \ + if self.text_color else app.theme_cls.transparentColor \ + ) \ + ) \ + if self.parent else app.theme_cls.transparentColor + + + + theme_font_size: "Custom" + font_size: "18sp" + pos_hint: {"center_y": .5} + padding: + "12dp", \ + 0, \ + 0 \ + if self.parent.parent.ids.container.parent._label else \ + "12dp", \ + 0 + icon_color: + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self.parent.parent.ids.container.parent.active else \ + self.theme_cls.onSurfaceColor \ + ) \ + if self.theme_icon_color == "Primary" else self.icon_color - size_hint: None, None - height: self.parent.height + md_bg_color: + ( \ + self.theme_cls.transparentColor \ + if not self.active else \ + ( \ + self.theme_cls.secondaryContainerColor \ + if not self.selected_color else self.selected_color \ + ) \ + ) \ + if not self.disabled else \ + ( \ + ( \ + self.theme_cls.onSurfaceColor[:-1] \ + + [self.segmented_button_opacity_value_disabled_container] \ + if not self.md_bg_color_disabled else self.md_bg_color_disabled \ + ) \ + if not self.active else \ + ( \ + self.md_bg_color[:-1] \ + + [self.segmented_button_opacity_value_disabled_container_active] \ + if not self.md_bg_color_disabled else self.md_bg_color_disabled \ + ) \ + ) line_color: - self.theme_cls.disabled_hint_text_color \ - if self.parent.line_color == [0, 0, 0, 0] else \ - self.parent.line_color - - SegmentButtonIcon: - id: scale_icon - icon: root.icon - size_hint: None, None - size: "24dp", "24dp" - pos_hint: {"center_y": .5} - scale_value_x: 1 if root.icon else 0 - scale_value_y: 1 if root.icon else 0 - x: label_text.x - dp(32) - - MDLabel: - id: label_text - text: root.text - adaptive_size: True - pos_hint: {"center_y": .5} - x: - root.center_x - (self.texture_size[0] / 2) \ - + (dp(16) if root.icon else 0) + ( \ + self.theme_cls.outlineColor \ + if self.theme_line_color == "Primary" else \ + ( \ + self._line_color \ + if self._line_color else \ + self.line_color \ + ) \ + ) \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + \ + [self.segmented_button_opacity_value_disabled_line] + + MDSegmentedButtonItemContainer: + id: container + size_hint_x: None + width: self.minimum_width + pos_hint: {"center_x": .5} + + + + icon: "check" + pos_hint: {"center_y": .5} + theme_font_size: "Custom" + font_size: 0 + icon_color: + ( \ + self.theme_cls.onSecondaryContainerColor \ + if self._item.active else \ + self.theme_cls.onSurfaceColor \ + ) \ + if not self._segmented_button.selected_icon_color else \ + self._segmented_button.selected_icon_color diff --git a/kivymd/uix/segmentedbutton/segmentedbutton.py b/kivymd/uix/segmentedbutton/segmentedbutton.py index 0729dba1e..a06b174c7 100644 --- a/kivymd/uix/segmentedbutton/segmentedbutton.py +++ b/kivymd/uix/segmentedbutton/segmentedbutton.py @@ -16,131 +16,209 @@ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-preview.png :align: center -Usage +- Segmented buttons can contain icons, label text, or both +- Two types: single-select and multi-select +- Use for simple choices between two to five items (for more items or complex + choices, use chips) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-types.png + :align: center + +1. Single-select segmented button +2. Multi-select segmented button + +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-anatomy.png + :align: center + +Icons ----- +Icons may be used as labels by themselves or alongside text. +If an icon is used without label text, it must clearly communicate the option +it represents. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-icons.png + :align: center + +Use with text and icon +---------------------- + .. code-block:: kv - MDScreen: + MDSegmentedButton: - MDSegmentedButton: + MDSegmentedButtonItem: - MDSegmentedButtonItem: - icon: ... - text: ... + MDSegmentButtonIcon: + icon: "language-python" - MDSegmentedButtonItem: - icon: ... - text: ... + MDSegmentButtonLabel: + text: "Python" - MDSegmentedButtonItem: - icon: ... - text: ... + MDSegmentedButtonItem: -Example -------- + MDSegmentButtonIcon: + icon: "language-javascript" -.. code-block:: python + MDSegmentButtonLabel: + text: "Java-Script" - from kivy.lang import Builder + MDSegmentedButtonItem: - from kivymd.app import MDApp + MDSegmentButtonIcon: + icon: "language-swift" - KV = ''' - MDScreen: + MDSegmentButtonLabel: + text: "Swift" - MDSegmentedButton: - pos_hint: {"center_x": .5, "center_y": .5} +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-use-with-text-and-icon.gif + :align: center - MDSegmentedButtonItem: - text: "Walking" +Use without text with an icon +---------------------------- - MDSegmentedButtonItem: - text: "Transit" +.. code-block:: kv - MDSegmentedButtonItem: - text: "Driving" - ''' + MDSegmentedButton: + MDSegmentedButtonItem: - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) + MDSegmentButtonIcon: + icon: "language-python" + MDSegmentedButtonItem: - Example().run() + MDSegmentButtonIcon: + icon: "language-javascript" + + MDSegmentedButtonItem: -By default, segmented buttons support single marking of elements: + MDSegmentButtonIcon: + icon: "language-swift" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-false.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-use-without-text-with-an-icon.gif :align: center -For multiple marking of elements, use the -:attr:`kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton.multiselect` -parameter: +Use only text +------------- .. code-block:: kv MDSegmentedButton: - multiselect: True -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-true.gif - :align: center + MDSegmentedButtonItem: -Control width -------------- + MDSegmentButtonLabel: + text: "Python" + + MDSegmentedButtonItem: + + MDSegmentButtonLabel: + text: "Java-Script" -The width of the panel of segmented buttons will be equal to the width -of the texture of the widest button multiplied by the number of buttons: + MDSegmentedButtonItem: -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-by-default.png + MDSegmentButtonLabel: + text: "Swift" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-use-only-text.gif :align: center -But you can use the `size_hint_x` parameter to specify the relative width: +Multiselect +----------- + +For multiple marking of elements, use the +:attr:`kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton.multiselect` +parameter: .. code-block:: kv MDSegmentedButton: - size_hint_x: .9 + multiselect: True -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-size-hint-x.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-true.gif :align: center -Customization -------------- +Type +---- -You can see below in the documentation from which classes the -:class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton` and -:class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButtonItem` -classes are inherited and use all their attributes such as -`md_bg_color`, `md_bg_color` etc. for additional customization of segments. +Density can be used in denser UIs where space is limited. Density is only +applied to the height. Each step down in density removes 4dp from the height. -Events ------- +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-type.png + :align: center -- on_marked - The method is called when a segment is marked. +.. code-block:: python -- on_unmarked - The method is called when a segment is unmarked. + from kivy.lang import Builder -.. code-block:: kv + from kivymd.uix.label import MDLabel + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.segmentedbutton import ( + MDSegmentedButton, + MDSegmentedButtonItem, + MDSegmentButtonLabel, + ) + from kivymd.app import MDApp - MDSegmentedButton: - on_marked: app.on_marked(*args) + KV = ''' + MDScreen: + md_bg_color: self.theme_cls.backgroundColor -.. code-block:: python + MDBoxLayout: + id: box + orientation: "vertical" + size_hint_x: .7 + adaptive_height: True + spacing: "24dp" + pos_hint: {"center_x": .5, "center_y": .5} + ''' - def on_marked( - self, - segment_button: MDSegmentedButton, - segment_item: MDSegmentedButtonItem, - marked: bool, - ) -> None: - print(segment_button) - print(segment_item) - print(marked) + + class Example(MDApp): + def on_start(self): + for segment_type in ["large", "normal", "medium", "small"]: + self.root.ids.box.add_widget( + MDBoxLayout( + MDLabel( + text=f"Type '{segment_type}'", + adaptive_height=True, + bold=True, + pos_hint={"center_y": 0.5}, + halign="center", + ), + MDSegmentedButton( + MDSegmentedButtonItem( + MDSegmentButtonLabel( + text="Songs", + ), + ), + MDSegmentedButtonItem( + MDSegmentButtonLabel( + text="Albums", + ), + ), + MDSegmentedButtonItem( + MDSegmentButtonLabel( + text="Podcasts", + ), + ), + type=segment_type, + ), + orientation="vertical", + spacing="12dp", + adaptive_height=True, + ) + ) + + def build(self): + return Builder.load_string(KV) + + + Example().run() A practical example ------------------- @@ -157,37 +235,30 @@ def on_marked( from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.segmentedbutton import MDSegmentedButton, MDSegmentedButtonItem from kivymd.utils import asynckivy KV = ''' adaptive_height: True - md_bg_color: "#343930" radius: 16 - TwoLineAvatarListItem: - id: item - divider: None - _no_ripple_effect: True - text: root.name - secondary_text: root.path_to_file - theme_text_color: "Custom" - text_color: "#8A8D79" - secondary_theme_text_color: self.theme_text_color - secondary_text_color: self.text_color - on_size: - self.ids._left_container.size = (item.height, item.height) - self.ids._left_container.x = dp(6) - self._txt_right_pad = item.height + dp(12) - - ImageLeftWidget: + MDListItem: + radius: 16 + theme_bg_color: "Custom" + md_bg_color: self.theme_cls.secondaryContainerColor + + MDListItemLeadingAvatar: source: root.album - radius: root.radius + + MDListItemHeadlineText: + text: root.name + + MDListItemSupportingText: + text: root.path_to_file MDScreen: - md_bg_color: "#151514" + md_bg_color: self.theme_cls.backgroundColor MDBoxLayout: orientation: "vertical" @@ -197,25 +268,31 @@ def on_marked( MDLabel: adaptive_height: True text: "Your downloads" - font_style: "H5" - theme_text_color: "Custom" - text_color: "#8A8D79" + theme_font_style: "Custom" + font_style: "Display" + role: "small" MDSegmentedButton: size_hint_x: 1 - selected_color: "#303A29" - line_color: "#343930" - on_marked: app.on_marked(*args) MDSegmentedButtonItem: - text: "Songs" - active: True + on_active: app.generate_card() + + MDSegmentButtonLabel: + text: "Songs" + active: True MDSegmentedButtonItem: - text: "Albums" + on_active: app.generate_card() + + MDSegmentButtonLabel: + text: "Albums" MDSegmentedButtonItem: - text: "Podcasts" + on_active: app.generate_card() + + MDSegmentButtonLabel: + text: "Podcasts" RecycleView: id: card_list @@ -242,16 +319,9 @@ class UserCard(MDBoxLayout): class Example(MDApp): def build(self): self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Olive" return Builder.load_string(KV) - def on_marked( - self, - segment_button: MDSegmentedButton, - segment_item: MDSegmentedButtonItem, - marked: bool, - ) -> None: - self.generate_card() - def generate_card(self): async def generate_card(): for i in range(10): @@ -273,68 +343,126 @@ async def generate_card(): .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-practical-example.gif :align: center + +API break +========= + +1.2.0 version +------------- + +.. code-block:: kv + + MDSegmentedButton: + on_marked: func(*args) + + MDSegmentedButtonItem: + icon: ... + text: ... + +2.0.0 version +------------- + +.. code-block:: kv + + MDSegmentedButton: + + MDSegmentedButtonItem: + on_active: func(*args) + + MDSegmentButtonIcon: + icon: ... + + MDSegmentButtonLabel: + text: ... + """ from __future__ import annotations -__all__ = ("MDSegmentedButton", "MDSegmentedButtonItem") +__all__ = ( + "MDSegmentedButton", + "MDSegmentedButtonItem", + "MDSegmentButtonLabel", + "MDSegmentButtonIcon", +) import os from kivy.animation import Animation from kivy.clock import Clock from kivy.lang import Builder -from kivy.metrics import dp +from kivy.metrics import dp, sp from kivy.properties import ( BooleanProperty, ColorProperty, ListProperty, NumericProperty, StringProperty, - VariableListProperty, + OptionProperty, + ObjectProperty, ) from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.relativelayout import RelativeLayout +from kivymd.theming import ThemableBehavior from kivymd import uix_path -from kivymd.uix.behaviors import RectangularRippleBehavior, ScaleBehavior +from kivymd.uix.behaviors import ( + RectangularRippleBehavior, + DeclarativeBehavior, + BackgroundColorBehavior, +) +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.floatlayout import MDFloatLayout -from kivymd.uix.label import MDIcon +from kivymd.uix.label import MDIcon, MDLabel with open( os.path.join(uix_path, "segmentedbutton", "segmentedbutton.kv"), encoding="utf-8", ) as kv_file: - Builder.load_string(kv_file.read()) + Builder.load_string(kv_file.read(), filename="MDSegmentedButton") class MDSegmentedButtonItem( - RectangularRippleBehavior, ButtonBehavior, MDFloatLayout + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + RectangularRippleBehavior, + ButtonBehavior, + StateLayerBehavior, + RelativeLayout, ): """ Segment button item. - For more information, see in the - :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + For more information see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and - :class:`~kivymd.uix.boxlayout.MDBoxLayout` + :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior` and + :class:`~kivy.uix.relativelayout.RelativeLayout` and class documentation. """ - icon = StringProperty() + selected_color = ColorProperty(None) """ - Icon segment. + Color of the marked segment. + + .. versionadded:: 2.0.0 - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. + :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - text = StringProperty() + md_bg_color_disabled = ColorProperty(None) """ - Text segment. + The background color in (r, g, b, a) or string format of the list item when + the list item is disabled. - :attr:`text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ active = BooleanProperty(False) @@ -345,49 +473,100 @@ class documentation. and defaults to `False`. """ - disabled_color = ColorProperty(None) - """ - Is active segment. + _icon = ObjectProperty() # MDSegmentButtonIcon object + _label = ObjectProperty() # MDSegmentButtonLabel object + _segmented_button = ObjectProperty() # MDSegmentedButton object + _line_color = ColorProperty(None) - :attr:`active` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ + def add_widget(self, widget, *args, **kwargs): + def add_selected_icon(container: MDSegmentedButtonItemContainer): + selected_icon = MDSegmentButtonSelectedIcon( + _segmented_button=self._segmented_button, _item=self + ) + container.add_widget(selected_icon, index=1) + + if isinstance( + widget, + (MDSegmentButtonLabel, MDSegmentButtonIcon), + ): + if isinstance(widget, MDSegmentButtonLabel): + self._label = widget + elif isinstance(widget, MDSegmentButtonIcon): + self._icon = widget + Clock.schedule_once( + lambda x: self._segmented_button._set_size_hint_min_x(widget) + ) + self.ids.container.add_widget(widget) + elif isinstance(widget, MDSegmentedButtonItemContainer): + Clock.schedule_once(lambda x: add_selected_icon(widget)) + return super().add_widget(widget) + + def on_line_color(self, instance, value) -> None: + """Fired when the values of :attr:`line_color` change.""" - _no_ripple_effect = BooleanProperty(True) - _current_icon = "" - _current_md_bg_color = None + if not self.disabled and self.theme_line_color == "Custom": + self._line_color = value - def on_disabled(self, instance, value: bool) -> None: - def on_disabled(*args): - if value: - if not self._current_md_bg_color: - self._current_md_bg_color = self.md_bg_color - self.md_bg_color = ( - self.theme_cls.disabled_hint_text_color - if not self.disabled_color - else self.disabled_color + def on_active(self, instance, value) -> None: + """ + Fired when the :attr:`active` value changes. + Animates the marker icon for the element. + """ + + def set_active(*args): + t = ( + self._segmented_button.opening_icon_transition + if value + else self._segmented_button.hiding_icon_transition + ) + d = ( + self._segmented_button.opening_icon_duration + if value + else self._segmented_button.hiding_icon_duration + ) + + if self._icon and self._segmented_button: + if self._label: + Animation(font_size=0 if value else sp(18), t=t, d=d).start( + self._icon + ) + + selected_icon = self._get_selected_icon_from_container() + if selected_icon: + Animation(font_size=sp(18) if value else 0, t=t, d=d).start( + selected_icon ) - else: - if self._current_md_bg_color: - self.md_bg_color = self._current_md_bg_color - self._current_md_bg_color = None - Clock.schedule_once(on_disabled) + Clock.schedule_once(set_active, 0.5) + + def on_disabled(self, instance, value) -> None: + """Fired when the :attr:`disabled` value changes.""" + + selected_icon = None - def on_icon(self, instance, icon_name: str): - if icon_name != "check": - self._current_icon = icon_name + if self._icon and self._segmented_button: + selected_icon = self._get_selected_icon_from_item() + elif not self._icon and self._segmented_button: + selected_icon = self._get_selected_icon_from_container() + if selected_icon: + selected_icon.state_layer_color = self.theme_cls.transparentColor -# TODO: -# Add the feature to use both text and icons in segments - -# https://m3.material.io/components/segmented-buttons/guidelines#26abac1c-c6bd-44c1-a969-8c910c880b98 -# Icons: optional check icon to indicate selected state - -# https://m3.material.io/components/segmented-buttons/overview#7b80f313-7d3a-4865-b26c-1f7ec98ba694 -# Hovered: add a color for the hovered segment - -# https://m3.material.io/components/segmented-buttons/specs#d730b3ba-c59e-4ef8-b652-20979fe20b67 -# Density: Each step down in density removes 4dp from the height - -# https://m3.material.io/components/segmented-buttons/specs#2d5cab36-1deb-40bd-9e37-bc2bb1657009 + def _get_selected_icon_from_container(self): + selected_icon = None + for item in self.ids.container.children: + if isinstance(item, MDSegmentButtonSelectedIcon): + selected_icon = item + break + return selected_icon + + def _get_selected_icon_from_item(self): + selected_icon = None + for item in self.children: + if isinstance(item, MDSegmentButtonSelectedIcon): + selected_icon = item + break + return selected_icon class MDSegmentedButton(MDBoxLayout): @@ -396,20 +575,6 @@ class MDSegmentedButton(MDBoxLayout): For more information, see in the :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. - - :Events: - `on_marked` - The method is called when a segment is marked. - `on_unmarked` - The method is called when a segment is unmarked. - """ - - radius = VariableListProperty([20], length=4) - """ - Panel radius. - - :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` - and defaults to `[20, 20, 20, 20]`. """ multiselect = BooleanProperty(False) @@ -428,12 +593,12 @@ class MDSegmentedButton(MDBoxLayout): and defaults to `'linear'`. """ - hiding_icon_duration = NumericProperty(0.05) + hiding_icon_duration = NumericProperty(0.1) """ Duration of hiding the current icon. :attr:`hiding_icon_duration` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.05`. + and defaults to `1`. """ opening_icon_transition = StringProperty("linear") @@ -444,210 +609,162 @@ class MDSegmentedButton(MDBoxLayout): and defaults to `'linear'`. """ - opening_icon_duration = NumericProperty(0.05) + opening_icon_duration = NumericProperty(0.1) """ The duration of opening a new icon of the "marked" type. :attr:`opening_icon_duration` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.05`. + and defaults to `0.1`. """ - selected_items = ListProperty() + selected_segments = ListProperty() """ The list of :class:`~MDSegmentedButtonItem` objects that are currently marked. - :attr:`selected_items` is a :class:`~kivy.properties.ListProperty` + :attr:`selected_segments` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ - selected_color = ColorProperty(None) + type = OptionProperty( + "large", options=["large", "normal", "medium", "small"] + ) """ - Color of the marked segment. + Density can be used in denser UIs where space is limited. + Density is only applied to the height. Each step down in density removes + '4dp' from the height. - :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` + .. versionadded:: 2.0.0 + + Available options are: 'large', 'normal', 'medium', 'small'. + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'large'`. + """ + + selected_icon_color = ColorProperty(None) + """ + Color in (r, g, b, a) or string format of the icon of the marked segment. + + .. versionadded:: 2.0.0 + + :attr:`selected_icon_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_event_type("on_marked") - self.register_event_type("on_unmarked") - Clock.schedule_once(self.mark_segment) - Clock.schedule_once(self.adjust_segment_radius) - Clock.schedule_once(self.adjust_segment_panel_width, 2) + def get_marked_items(self) -> list: + """Returns a list of active item objects.""" - def mark_segment(self, *args) -> None: - """Programmatically marks a segment.""" + return [item for item in self.ids.container.children if item.active] - for widget in self.children: - if widget.active: - widget.active = False - widget.dispatch("on_release") + def get_items(self) -> list: + """Returns a list of item objects.""" - if not self.multiselect: - break + return [item for item in self.ids.container.children] def adjust_segment_radius(self, *args) -> None: """Rounds off the first and last elements.""" - if self.children[0].radius == [0, 0, 0, 0]: - self.children[0].radius = (0, self.height / 2, self.height / 2, 0) - if self.children[-1].radius == [0, 0, 0, 0]: - self.children[-1].radius = (self.height / 2, 0, 0, self.height / 2) + if self.ids.container.children[0].radius == [0, 0, 0, 0]: + self.ids.container.children[0].radius = ( + 0, + self.height / 2, + self.height / 2, + 0, + ) + if self.ids.container.children[-1].radius == [0, 0, 0, 0]: + self.ids.container.children[-1].radius = ( + self.height / 2, + 0, + 0, + self.height / 2, + ) - def adjust_segment_panel_width(self, *args) -> None: - """ - Sets the width of all segments and the width of the panel - by the widest segment. - """ + def mark_item(self, segment_item: MDSegmentedButtonItem) -> None: + """Fired when a segment element is clicked (`on_release` event).""" - if not self.size_hint_x: - width_list = [ - widget.ids.label_text.texture_size[0] - + (dp(72) if widget.icon else dp(48)) - for widget in self.children - ] - max_width = max(width_list) - self.width = max_width * len(width_list) - else: - max_width = self.width / len(self.children) + if not segment_item.disabled: + if not segment_item.active and not self.multiselect: + segment_item.active = True + elif self.multiselect: + segment_item.active = not segment_item.active - for widget in self.children: - widget.width = max_width + if not self.multiselect: + for widget in self.ids.container.children: + if segment_item is not widget: + widget.active = False - self.opacity = 1 + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDSegmentedButtonItem): + widget._segmented_button = self + widget.bind(on_release=self.mark_item) + self.ids.container.add_widget(widget) + Clock.schedule_once(self.adjust_segment_radius) + elif isinstance(widget, MDSegmentedButtonContainer): + return super().add_widget(widget) - for widget in self.children: - if widget.active: - widget.dispatch("on_release") + def _set_size_hint_min_x( + self, widget: MDSegmentButtonLabel | MDSegmentButtonIcon + ): + self.ids.container.size_hint_min_x += widget.texture_size[0] + dp(36) - def shift_segment_text(self, segment_item: MDSegmentedButtonItem) -> None: - """ - Shifts the segment text to the right, thus freeing up space - for the icon (when the segment is marked). - """ - Animation( - x=( - segment_item.ids.label_text.x - + ( - dp(16) - if not segment_item.icon and not segment_item.active - else 0 - ) - ) - if not segment_item.active - else ( - segment_item.ids.label_text.x - - ( - dp(16) - if not segment_item.icon and segment_item.active - else 0 - ) - ), - d=0.2, - ).start(segment_item.ids.label_text) +class MDSegmentedButtonContainer(BoxLayout): + """ + Implements a container for placing :class:`~MDSegmentedButtonItem` + elements. - def show_icon_marked_segment( - self, segment_item: MDSegmentedButtonItem - ) -> None: - """ - Sets the icon for the marked segment and changes the icon scale - to the normal scale. - """ + .. versionadded:: 2.0.0 - segment_item.ids.scale_icon.icon = "check" - if segment_item.ids.scale_icon.icon == "check" and segment_item.active: - segment_item.ids.scale_icon.icon = segment_item._current_icon - - Animation( - scale_value_x=1, - scale_value_y=1, - d=self.opening_icon_duration, - t=self.opening_icon_transition, - ).start(segment_item.ids.scale_icon) - - self.shift_segment_text(segment_item) - self.set_selected_segment_list(segment_item) - self.set_bg_marked_segment(segment_item) - - def hide_icon_marked_segment( - self, segment_item: MDSegmentedButtonItem - ) -> None: - """Changes the scale of the icon of the marked segment to zero.""" - - anim = Animation( - scale_value_x=0, - scale_value_y=0, - d=self.hiding_icon_duration, - t=self.hiding_icon_transition, - ) - anim.bind( - on_complete=lambda x, y: self.show_icon_marked_segment(segment_item) - ) - anim.start(segment_item.ids.scale_icon) - - def restore_bg_segment(self, segment_item) -> None: - Animation(md_bg_color=self.md_bg_color, d=0.2).start(segment_item) - - def set_bg_marked_segment(self, segment_item) -> None: - if segment_item.active: - Animation( - md_bg_color=self.selected_color - if self.selected_color - else self.theme_cls.primary_color, - d=0.2, - ).start(segment_item) - - def set_selected_segment_list(self, segment_item) -> None: - segment_item.active = not segment_item.active - - if segment_item.active: - self.selected_items.append(segment_item) - self.dispatch("on_marked", segment_item, segment_item.active) - else: - if segment_item in self.selected_items: - self.selected_items.remove(segment_item) - self.dispatch("on_unmarked", segment_item, segment_item.active) + For more information, see in the + :class:`~kivy.uix.boxlayout.BoxLayout` class documentation. + """ - def mark_item(self, segment_item: MDSegmentedButtonItem) -> None: - if segment_item.active and not self.multiselect: - return - if not self.multiselect and self.selected_items: - self.uncheck_item() - else: - if segment_item.active: - self.restore_bg_segment(segment_item) - self.hide_icon_marked_segment(segment_item) +class MDSegmentedButtonItemContainer(BoxLayout): + """ + Implements a container for placing :class:`~MDSegmentButtonLabel` + and :class:`~MDSegmentButtonLabel` elements. - def uncheck_item(self) -> None: - for item in self.children: - if item.active: - self.hide_icon_marked_segment(item) - self.restore_bg_segment(item) - break + .. versionadded:: 2.0.0 - def add_widget(self, widget, *args, **kwargs): - if isinstance(widget, MDSegmentedButtonItem): - widget.bind(on_release=self.mark_item) - return super().add_widget(widget) + For more information, see in the + :class:`~kivy.uix.boxlayout.BoxLayout` class documentation. + """ + + +class MDSegmentButtonSelectedIcon(MDIcon): + """ + Implements the selected icon with scaling behavior + for :class:`~MDSegmentedButtonItem` class. + + .. versionadded:: 2.0.0 - def on_size(self, instance_segment_button, size: list) -> None: - """Called when the root screen is resized.""" + For more information, see in the + :class:`~kivymd.uix.label.label.MDIcon` class documentation. + """ + + _segmented_button = ObjectProperty() # MDSegmentedButton object + _item = ObjectProperty() # MDSegmentedButtonItem object + + +class MDSegmentButtonIcon(MDIcon): + """ + Implements a icon for :class:`~MDSegmentedButtonItem` class. - if self.size_hint_x: - max_width = size[0] / len(self.children) - for widget in self.children: - widget.width = max_width + .. versionadded:: 2.0.0 + + For more information, see in the + :class:`~kivymd.uix.label.label.MDIcon` class documentation. + """ - def on_marked(self, *args): - """The method is called when a segment is marked.""" - def on_unmarked(self, *args): - """The method is called when a segment is unmarked.""" +class MDSegmentButtonLabel(MDLabel): + """ + Implements a label for :class:`~MDSegmentedButtonItem` class. + .. versionadded:: 2.0.0 -class SegmentButtonIcon(MDIcon, ScaleBehavior): - """Implements an icon with scaling behavior.""" + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ diff --git a/kivymd/uix/selection/__init__.py b/kivymd/uix/selection/__init__.py deleted file mode 100644 index 9be34e218..000000000 --- a/kivymd/uix/selection/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .selection import MDSelectionList # NOQA F401 diff --git a/kivymd/uix/selection/selection.kv b/kivymd/uix/selection/selection.kv deleted file mode 100644 index 7e3dead60..000000000 --- a/kivymd/uix/selection/selection.kv +++ /dev/null @@ -1,17 +0,0 @@ - - theme_text_color: "Custom" - text_color: self.icon_check_color - - canvas.before: - PushMatrix - Scale: - x: root.scale - y: root.scale - z: root.scale - origin: self.center - canvas.after: - PopMatrix - - - - md_bg_color: root.overlay_color if root.selected else (0, 0, 0, 0) diff --git a/kivymd/uix/selection/selection.py b/kivymd/uix/selection/selection.py deleted file mode 100644 index 1ee867650..000000000 --- a/kivymd/uix/selection/selection.py +++ /dev/null @@ -1,672 +0,0 @@ -""" -Components/Selection -==================== - -.. seealso:: - - `Material Design spec, Banner `_ - -.. rubric:: Selection refers to how users indicate specific items they intend to take action on. - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-previous.png - :align: center - -Entering selection mode ------------------------ - -To select an item and enter selection mode, long press the item: - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/enter-selection-mode.gif - :align: center - -Exiting selection mode ----------------------- - -To exit selection mode, tap each selected item until they’re all deselected: - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/exit-selection-mode.gif - :align: center - -Larger selections ------------------ - -.. note:: This feature is missing yet. - -Events ------- - -.. code-block:: python - - def on_selected(self, instance_selection_list, instance_selection_item): - '''Called when a list item is selected.''' - - def on_unselected(self, instance_selection_list, instance_selection_item): - '''Called when a list item is unselected.''' - -Example with TwoLineAvatarListItem ----------------------------------- - -.. code-block:: python - - from kivy.animation import Animation - from kivy.lang import Builder - from kivy.utils import get_color_from_hex - - from kivymd.app import MDApp - from kivymd.uix.list import TwoLineAvatarListItem - - KV = ''' - - text: "Two-line item with avatar" - secondary_text: "Secondary text here" - _no_ripple_effect: True - - ImageLeftWidget: - source: "data/logo/kivy-icon-256.png" - - - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - id: toolbar - title: "Inbox" - left_action_items: [["menu"]] - right_action_items: [["magnify"], ["dots-vertical"]] - md_bg_color: 0, 0, 0, 1 - - MDBoxLayout: - padding: "24dp", "8dp", 0, "8dp" - adaptive_size: True - - MDLabel: - text: "Today" - adaptive_size: True - - ScrollView: - - MDSelectionList: - id: selection_list - spacing: "12dp" - overlay_color: app.overlay_color[:-1] + [.2] - icon_bg_color: app.overlay_color - on_selected: app.on_selected(*args) - on_unselected: app.on_unselected(*args) - on_selected_mode: app.set_selection_mode(*args) - ''' - - - class MyItem(TwoLineAvatarListItem): - pass - - - class Example(MDApp): - overlay_color = get_color_from_hex("#6042e4") - - def build(self): - return Builder.load_string(KV) - - def on_start(self): - for i in range(10): - self.root.ids.selection_list.add_widget(MyItem()) - - def set_selection_mode(self, instance_selection_list, mode): - if mode: - md_bg_color = self.overlay_color - left_action_items = [ - [ - "close", - lambda x: self.root.ids.selection_list.unselected_all(), - ] - ] - right_action_items = [["trash-can"], ["dots-vertical"]] - else: - md_bg_color = (0, 0, 0, 1) - left_action_items = [["menu"]] - right_action_items = [["magnify"], ["dots-vertical"]] - self.root.ids.toolbar.title = "Inbox" - - Animation(md_bg_color=md_bg_color, d=0.2).start(self.root.ids.toolbar) - self.root.ids.toolbar.left_action_items = left_action_items - self.root.ids.toolbar.right_action_items = right_action_items - - def on_selected(self, instance_selection_list, instance_selection_item): - self.root.ids.toolbar.title = str( - len(instance_selection_list.get_selected_list_items()) - ) - - def on_unselected(self, instance_selection_list, instance_selection_item): - if instance_selection_list.get_selected_list_items(): - self.root.ids.toolbar.title = str( - len(instance_selection_list.get_selected_list_items()) - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-example-with-listItem.gif - :align: center - -Example with FitImage ---------------------- - -.. code-block:: python - - from kivy.animation import Animation - from kivy.lang import Builder - from kivy.properties import ColorProperty - - from kivymd.app import MDApp - from kivymd.uix.fitimage import FitImage - - KV = ''' - MDBoxLayout: - orientation: "vertical" - md_bg_color: app.theme_cls.bg_light - - MDTopAppBar: - id: toolbar - title: "Inbox" - left_action_items: [["menu"]] - right_action_items: [["magnify"], ["dots-vertical"]] - md_bg_color: app.theme_cls.bg_light - specific_text_color: 0, 0, 0, 1 - - MDBoxLayout: - padding: "24dp", "8dp", 0, "8dp" - adaptive_size: True - - MDLabel: - text: "Today" - adaptive_size: True - - ScrollView: - - MDSelectionList: - id: selection_list - padding: "24dp", 0, "24dp", "24dp" - cols: 3 - spacing: "12dp" - overlay_color: app.overlay_color[:-1] + [.2] - icon_bg_color: app.overlay_color - progress_round_color: app.progress_round_color - on_selected: app.on_selected(*args) - on_unselected: app.on_unselected(*args) - on_selected_mode: app.set_selection_mode(*args) - ''' - - - class Example(MDApp): - overlay_color = ColorProperty("#6042e4") - progress_round_color = "#ef514b" - - def build(self): - return Builder.load_string(KV) - - def on_start(self): - for i in range(10): - self.root.ids.selection_list.add_widget( - FitImage( - source="image.png", - size_hint_y=None, - height="240dp", - ) - ) - - def set_selection_mode(self, instance_selection_list, mode): - if mode: - md_bg_color = self.overlay_color - left_action_items = [ - [ - "close", - lambda x: self.root.ids.selection_list.unselected_all(), - ] - ] - right_action_items = [["trash-can"], ["dots-vertical"]] - else: - md_bg_color = (1, 1, 1, 1) - left_action_items = [["menu"]] - right_action_items = [["magnify"], ["dots-vertical"]] - self.root.ids.toolbar.title = "Inbox" - - Animation(md_bg_color=md_bg_color, d=0.2).start(self.root.ids.toolbar) - self.root.ids.toolbar.left_action_items = left_action_items - self.root.ids.toolbar.right_action_items = right_action_items - - def on_selected(self, instance_selection_list, instance_selection_item): - self.root.ids.toolbar.title = str( - len(instance_selection_list.get_selected_list_items()) - ) - - def on_unselected(self, instance_selection_list, instance_selection_item): - if instance_selection_list.get_selected_list_items(): - self.root.ids.toolbar.title = str( - len(instance_selection_list.get_selected_list_items()) - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-example-with-fitimage.gif - :align: center -""" - -__all__ = ("MDSelectionList",) - -import os -from typing import Union - -from kivy.animation import Animation -from kivy.clock import Clock -from kivy.graphics.context_instructions import Color -from kivy.graphics.vertex_instructions import ( - Ellipse, - RoundedRectangle, - SmoothLine, -) -from kivy.lang import Builder -from kivy.metrics import dp -from kivy.properties import ( - BooleanProperty, - ColorProperty, - ListProperty, - NumericProperty, - ObjectProperty, - StringProperty, -) - -from kivymd import uix_path -from kivymd.uix.behaviors import TouchBehavior -from kivymd.uix.button import MDIconButton -from kivymd.uix.list import MDList -from kivymd.uix.relativelayout import MDRelativeLayout - -with open( - os.path.join(uix_path, "selection", "selection.kv"), encoding="utf-8" -) as kv_file: - Builder.load_string(kv_file.read()) - - -class SelectionIconCheck(MDIconButton): - """Implements the icon for the checked item.""" - - scale = NumericProperty(0) - icon_check_color = ColorProperty([0, 0, 0, 1]) - - -class SelectionItem(MDRelativeLayout, TouchBehavior): - selected = BooleanProperty(False) - """ - Whether or not an item is checked. - - :attr:`selected` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - owner = ObjectProperty() - """ - Instance of :class:`~kivymd.uix.selection.MDSelectionList` class. - - :attr:`owner` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - instance_item = ObjectProperty() - """ - User item. Must be a Kivy or KivyMD widget. - - :attr:`instance_item` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - instance_icon = ObjectProperty() - """ - Instance of :class:`~kivymd.uix.selection.SelectionIconCheck` class. - - :attr:`instance_icon` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - overlay_color = ColorProperty([0, 0, 0, 0.2]) - """See :attr:`~MDSelectionList.overlay_color`.""" - - progress_round_size = NumericProperty(dp(46)) - """See :attr:`~MDSelectionList.progress_round_size`.""" - - progress_round_color = ColorProperty(None) - """See :attr:`~MDSelectionList.progress_round_color`.""" - - _progress_round = NumericProperty(0) - _progress_line_end = NumericProperty(0) - _progress_animation = BooleanProperty(False) - _touch_long = BooleanProperty(False) - _instance_progress_inner_circle_color = ObjectProperty() - _instance_progress_inner_circle_ellipse = ObjectProperty() - _instance_progress_inner_outer_color = ObjectProperty() - _instance_progress_inner_outer_line = ObjectProperty() - _instance_overlay_color = ObjectProperty() - _instance_overlay_rounded_rec = ObjectProperty() - - def __init__(self, **kwargs): - super().__init__(**kwargs) - Clock.schedule_once(self.set_progress_round) - - def set_progress_round(self, interval: Union[int, float]) -> None: - with self.canvas.after: - self._instance_progress_inner_circle_color = Color( - rgba=(0, 0, 0, 0) - ) - self._instance_progress_inner_circle_ellipse = Ellipse( - size=self.get_progress_round_size(), - pos=self.get_progress_round_pos(), - ) - self.bind( - pos=self.update_progress_inner_circle_ellipse, - size=self.update_progress_inner_circle_ellipse, - ) - # FIXME: Radius value is not displayed. - self._instance_overlay_color = Color(rgba=(0, 0, 0, 0)) - self._instance_overlay_rounded_rec = RoundedRectangle( - size=self.size, - pos=self.pos, - radius=self.instance_item.radius - if hasattr(self.instance_item, "radius") - else [ - 0, - ], - ) - self.bind( - pos=self.update_overlay_rounded_rec, - size=self.update_overlay_rounded_rec, - ) - self._instance_progress_inner_outer_color = Color(rgba=(0, 0, 0, 0)) - self._instance_progress_inner_outer_line = SmoothLine( - width=dp(4), - circle=[ - self.center_x, - self.center_y, - self.progress_round_size * 0.58, - 0, - 0, - ], - ) - - def do_selected_item(self, *args) -> None: - Animation(scale=1, d=0.2).start(self.instance_icon) - self.selected = True - self._progress_animation = False - self._instance_overlay_color.rgba = self.get_overlay_color() - self.owner.dispatch("on_selected", self) - - def do_unselected_item(self) -> None: - Animation(scale=0, d=0.2).start(self.instance_icon) - self.selected = False - self._instance_overlay_color.rgba = self.get_overlay_color() - self.owner.dispatch("on_unselected", self) - - def do_animation_progress_line( - self, animation: Animation, instance_selection_item, value: float - ) -> None: - self._instance_progress_inner_outer_line.circle = ( - self.center_x, - self.center_y, - self.progress_round_size * 0.58, - 0, - 360 * value, - ) - - def update_overlay_rounded_rec(self, *args) -> None: - self._instance_overlay_rounded_rec.size = self.size - self._instance_overlay_rounded_rec.pos = self.pos - - def update_progress_inner_circle_ellipse(self, *args) -> None: - self._instance_progress_inner_circle_ellipse.size = ( - self.get_progress_round_size() - ) - self._instance_progress_inner_circle_ellipse.pos = ( - self.get_progress_round_pos() - ) - - def reset_progress_animation(self) -> None: - Animation.cancel_all(self) - self._progress_animation = False - self._instance_progress_inner_circle_color.rgba = (0, 0, 0, 0) - self._instance_progress_inner_outer_color.rgba = (0, 0, 0, 0) - self._instance_progress_inner_outer_line.circle = [ - self.center_x, - self.center_y, - self.progress_round_size * 0.58, - 0, - 0, - ] - self._progress_line_end = 0 - - def get_overlay_color(self) -> list: - return self.overlay_color if self.selected else (0, 0, 0, 0) - - def get_progress_round_pos(self) -> tuple: - return ( - (self.pos[0] + self.width / 2) - self.progress_round_size / 2, - self.center_y - self.progress_round_size / 2, - ) - - def get_progress_round_size(self) -> tuple: - return self.progress_round_size, self.progress_round_size - - def get_progress_round_color(self) -> tuple: - return ( - self.theme_cls.primary_color - if not self.progress_round_color - else self.progress_round_color - ) - - def get_progress_line_color(self) -> tuple: - return ( - self.theme_cls.primary_color[:-1] + [0.5] - if not self.progress_round_color - else self.progress_round_color[:-1] + [0.5] - ) - - def on_long_touch(self, *args) -> None: - if not self.owner.get_selected(): - self._touch_long = True - self._progress_animation = True - - def on_touch_up(self, touch): - if self.collide_point(*touch.pos): - if self._touch_long: - self._touch_long = False - return super().on_touch_up(touch) - - def on_touch_down(self, touch): - if self.collide_point(*touch.pos): - if self.selected: - self.do_unselected_item() - else: - if self.owner.selected_mode: - self.do_selected_item() - return super().on_touch_down(touch) - - def on__touch_long(self, instance_selection_tem, touch_value: bool) -> None: - if not touch_value: - self.reset_progress_animation() - - def on__progress_animation( - self, instance_selection_tem, touch_value: bool - ) -> None: - if touch_value: - anim = Animation(_progress_line_end=360, d=1, t="in_out_quad") - anim.bind( - on_progress=self.do_animation_progress_line, - on_complete=self.do_selected_item, - ) - anim.start(self) - self._instance_progress_inner_outer_color.rgba = ( - self.get_progress_line_color() - ) - self._instance_progress_inner_circle_color.rgba = ( - self.get_progress_round_color() - ) - else: - self.reset_progress_animation() - - -class MDSelectionList(MDList): - """ - Selection list class. - - For more information, see in the - :class:`~kivymd.uix.list.MDList` classes documentation. - - :Events: - `on_selected` - Called when a list item is selected. - `on_unselected` - Called when a list item is unselected. - """ - - selected_mode = BooleanProperty(False) - """ - List item selection mode. If `True` when clicking on a list item, it will - be selected. - - :attr:`selected_mode` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - icon = StringProperty("check") - """ - Name of the icon with which the selected list item will be marked. - - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `'check'`. - """ - - icon_pos = ListProperty() - """ - The position of the icon that will mark the selected list item. - - :attr:`icon_pos` is an :class:`~kivy.properties.ListProperty` - and defaults to `[]`. - """ - - icon_bg_color = ColorProperty([1, 1, 1, 1]) - """ - Background color in (r, g, b, a) or string format of the icon that will - mark the selected list item. - - :attr:`icon_bg_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[1, 1, 1, 1]`. - """ - - icon_check_color = ColorProperty([0, 0, 0, 1]) - """ - Color in (r, g, b, a) or string format of the icon that will mark the - selected list item. - - :attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[1, 1, 1, 1]`. - """ - - overlay_color = ColorProperty([0, 0, 0, 0.2]) - """ - The overlay color in (r, g, b, a) or string format of the selected list item. - - :attr:`overlay_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0.2]]`. - """ - - progress_round_size = NumericProperty(dp(46)) - """ - Size of the spinner for switching of `selected_mode` mode. - - :attr:`progress_round_size` is an :class:`~kivy.properties.NumericProperty` - and defaults to `dp(46)`. - """ - - progress_round_color = ColorProperty(None) - """ - Color in (r, g, b, a) or string format of the spinner for switching of - `selected_mode` mode. - - :attr:`progress_round_color` is an :class:`~kivy.properties.NumericProperty` - and defaults to `None`. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.register_event_type("on_selected") - self.register_event_type("on_unselected") - - def add_widget(self, widget, index=0, canvas=None): - selection_icon = SelectionIconCheck( - icon=self.icon, - md_bg_color=self.icon_bg_color, - icon_check_color=self.icon_check_color, - ) - container = SelectionItem( - size_hint=(1, None), - height=widget.height, - instance_item=widget, - instance_icon=selection_icon, - overlay_color=self.overlay_color, - progress_round_size=self.progress_round_size, - progress_round_color=self.progress_round_color, - owner=self, - ) - container.add_widget(widget) - - if not self.icon_pos: - pos = ( - dp(12), - container.height / 2 - selection_icon.height / 2, - ) - else: - pos = self.icon_pos - selection_icon.pos = pos - container.add_widget(selection_icon) - return super().add_widget(container, index, canvas) - - def get_selected(self) -> bool: - """Returns ``True`` if at least one item in the list is checked.""" - - selected = False - for item in self.children: - if item.selected: - selected = True - break - return selected - - def get_selected_list_items(self) -> list: - """ - Returns a list of marked objects: - - [, ...] - """ - - selected_list_items = [] - for item in self.children: - if item.selected: - selected_list_items.append(item) - return selected_list_items - - def unselected_all(self) -> None: - for item in self.children: - item.do_unselected_item() - self.selected_mode = False - - def selected_all(self) -> None: - for item in self.children: - item.do_selected_item() - self.selected_mode = True - - def on_selected(self, *args): - """Called when a list item is selected.""" - - if not self.selected_mode: - self.selected_mode = True - - def on_unselected(self, *args): - """Called when a list item is unselected.""" - - self.selected_mode = self.get_selected() diff --git a/kivymd/uix/selectioncontrol/selectioncontrol.kv b/kivymd/uix/selectioncontrol/selectioncontrol.kv index 3d3b015fe..e25b81d2b 100644 --- a/kivymd/uix/selectioncontrol/selectioncontrol.kv +++ b/kivymd/uix/selectioncontrol/selectioncontrol.kv @@ -1,12 +1,26 @@ -#:import get_color_from_hex kivy.utils.get_color_from_hex -#:import colors kivymd.color_definitions.colors - - + _current_color: + ( \ + ( \ + self.theme_cls.primaryColor \ + if not self.color_active else self.color_active \ + ) \ + if self.active else \ + ( \ + self.theme_cls.onSurfaceVariantColor \ + if not self.color_inactive else self.color_inactive \ + ) \ + ) \ + if not self.disabled else \ + ( \ + self.theme_cls.onSurfaceColor[:-1] \ + + [self.checkbox_opacity_value_disabled_container] \ + if not self.color_disabled else self.color_disabled \ + ) + canvas: - Clear Color: - rgba: self.color + rgba: self._current_color Rectangle: texture: self.texture size: self.texture_size @@ -14,9 +28,22 @@ int(self.center_x - self.texture_size[0] / 2.), \ int(self.center_y - self.texture_size[1] / 2.) - color: self._current_color + # FIXME: Move to `kivymd/uix/behaviors/state_layer_behavior.py` + canvas.after: + # Clear + Color + rgba: self.state_layer_color + RoundedRectangle: + size: self.width + dp(20), self.height + dp(20) + pos: self.x - self.width / 2 + dp(2), self.y - self.height / 2 + dp(2) + radius: [(self.width + dp(20)) / 2, ] + + theme_text_color: "Custom" + text_color: self._current_color halign: "center" valign: "middle" + size_hint: None, None + size: "22dp", "22dp" @@ -26,115 +53,103 @@ ThumbIcon: id: icon font_size: "16sp" - theme_text_color: "Custom" pos_hint: {"center_x": .5, "center_y": .5} + theme_text_color: "Custom" + text_color: + ( \ + ( \ + root.parent.icon_active_color \ + if root.parent.icon_active_color \ + else self.theme_cls.onPrimaryContainerColor \ + ) \ + if root.parent.icon_active and root.parent.active else \ + ( \ + root.parent.icon_inactive_color \ + if root.parent.icon_inactive_color \ + else self.theme_cls.surfaceContainerHighestColor \ + ) \ + ) \ + if not root.parent.disabled else \ + ( \ + self.theme_cls.onSurfaceColor[:-1] \ + + [root.parent.switch_opacity_value_disabled_icon] \ + if root.parent.icon_active else \ + self.theme_cls.surfaceContainerHighestColor[:-1] \ + + [root.parent.switch_opacity_value_disabled_icon] \ + ) - canvas.before: - Color: - rgba: - ( \ - self.track_color_disabled \ - if self.track_color_disabled else \ - self.theme_cls.disabled_hint_text_color) \ - if self.disabled else \ - ( \ - ( \ - self.track_color_active \ - if self.track_color_active else \ - self.theme_cls.primary_color[:-1] + [.5]) \ - if self.active else \ - (self.track_color_inactive \ - if self.track_color_inactive else \ - self.theme_cls.disabled_hint_text_color) \ - ) - RoundedRectangle: - size: - (self.width + dp(14), dp(28)) \ - if root.widget_style == "ios" else \ - ( \ - (self.width - dp(8), dp(16)) \ - if app.theme_cls.material_style == "M2" else \ - (self.width + dp(16), dp(32)) \ - ) - pos: - (self.x - dp(2), self.center_y - dp(14)) \ - if root.widget_style == "ios" else \ - (self.x + dp(8), self.center_y - dp(8)) - radius: - [dp(14)] \ - if root.widget_style == "ios" else \ - [dp(7) if app.theme_cls.material_style == "M2" else dp(16)] - Color: - rgba: - ( \ - self.theme_cls.disabled_hint_text_color \ - if not root.active else (0, 0, 0, 0) \ - ) \ - if root.widget_style == "ios" \ - or app.theme_cls.material_style == "M3" else \ - (0, 0, 0, 0) - SmoothLine: - width: - 1 \ - if root.widget_style == "ios" \ - or app.theme_cls.material_style == "M2" else \ - 1.4 - rounded_rectangle: - ( \ - self.x - dp(2), self.center_y - dp(14), self.width + dp(14), \ - dp(28), dp(14), dp(14), dp(14), dp(14), dp(28) \ - ) \ - if root.widget_style == "ios" else \ - ( \ - (1, 1, 1, 1, 1, 1, 1, 1, 1) \ - if app.theme_cls.material_style == "M2" else \ - ( \ - self.x + dp(8), self.center_y - dp(8), self.width + dp(16), \ - dp(32), dp(16), dp(16), dp(16), dp(16), dp(32) \ - ) - ) + size_hint: None, None + size: dp(52), dp(32) + radius: [self.height / 2, ] + md_bg_color: + ( \ + self.track_color_disabled \ + if self.track_color_disabled else \ + ( \ + self.theme_cls.onSurfaceColor[:-1] \ + + [self.switch_opacity_value_disabled_container] \ + if self.active else self.theme_cls.surfaceContainerHighestColor[:-1] \ + + [self.switch_opacity_value_disabled_container] \ + ) \ + ) \ + if self.disabled else \ + ( \ + ( \ + self.track_color_active \ + if self.track_color_active else \ + self.theme_cls.primaryColor \ + ) \ + if self.active else \ + (self.track_color_inactive \ + if self.track_color_inactive else \ + self.theme_cls.surfaceContainerHighestColor \ + ) \ + ) + line_color: + ( \ + ( \ + self.theme_cls.outlineColor if not self.active else self.md_bg_color + ) \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] \ + + [self.switch_opacity_value_disabled_line] \ + ) \ + if self.theme_line_color == "Primary" else \ + self._line_color if not self.disabled else \ + ( \ + self.line_color_disabled \ + if self.line_color_disabled else \ + self._line_color \ + ) Thumb: id: thumb size_hint: None, None size: dp(24), dp(24) - elevation: - (2.5 if root.active else 1) \ - if app.theme_cls.material_style != "M3" else \ - 0 - pos: - (root.pos[0] + root._thumb_pos[0], root.pos[1] + root._thumb_pos[1]) \ - if root.widget_style == "ios" \ - or app.theme_cls.material_style == "M2" else \ - ( \ - root.pos[0] + self.width + root._thumb_pos[0], \ - root.pos[1] + (root.height / 2 - self.height / 2) + root._thumb_pos[1] \ - ) - _no_ripple_effect: - True \ - if app.theme_cls.material_style == "M3" \ - and root.widget_style != "ios" else \ - False + _no_ripple_effect: not root.ripple_effect md_bg_color: ( \ root.thumb_color_disabled \ if root.thumb_color_disabled else \ - get_color_from_hex(colors["Gray"]["800"]) \ + ( \ + root.theme_cls.surfaceColor \ + if root.active else root.theme_cls.onSurfaceColor[:-1] \ + + [root.switch_thumb_opacity_value_disabled_container] \ + ) \ ) \ if root.disabled else \ ( \ (root.thumb_color_active \ if root.thumb_color_active else \ - root.theme_cls.primary_color \ + root.theme_cls.onPrimaryColor \ ) \ if root.active else \ ( \ root.thumb_color_inactive \ if root.thumb_color_inactive else \ - get_color_from_hex(colors["Gray"]["50"] \ - ) \ + self.theme_cls.outlineColor \ ) \ ) on_touch_down: @@ -143,3 +158,7 @@ on_touch_up: if self.collide_point(*args[1].pos) and not root.disabled: \ setattr(root, "active", not root.active) + pos: + root.pos[0] + (self.width / 2) + root._thumb_pos[0] \ + + dp(6 if root.icon_inactive else 0), \ + root.pos[1] + (root.height / 2 - self.height / 2) + root._thumb_pos[1] \ No newline at end of file diff --git a/kivymd/uix/selectioncontrol/selectioncontrol.py b/kivymd/uix/selectioncontrol/selectioncontrol.py index 6bcefe679..1effec528 100755 --- a/kivymd/uix/selectioncontrol/selectioncontrol.py +++ b/kivymd/uix/selectioncontrol/selectioncontrol.py @@ -152,34 +152,24 @@ def build(self): adaptive_height: True MDCheckbox: - size_hint: None, None - size: "48dp", "48dp" group: root.group MDLabel: text: root.text adaptive_height: True - theme_text_color: "Custom" - text_color: "#B2B6AE" + padding_x: "12dp" pos_hint: {"center_y": .5} MDBoxLayout: orientation: "vertical" - md_bg_color: "#141612" - - MDTopAppBar: - md_bg_color: "#21271F" - specific_text_color: "#B2B6AE" - elevation: 0 - title: "Meal options" - left_action_items: [["arrow-left", lambda x: x]] - anchor_title: "left" + md_bg_color: self.theme_cls.backgroundColor MDBoxLayout: orientation: "vertical" adaptive_height: True padding: "12dp", "36dp", 0, 0 + spacing: "12dp" CheckItem: text: "Recieve emails" @@ -189,6 +179,7 @@ def build(self): orientation: "vertical" adaptive_height: True padding: "24dp", 0, 0, 0 + spacing: "12dp" CheckItem: text: "Daily" @@ -213,7 +204,6 @@ class CheckItem(MDBoxLayout): class Example(MDApp): def build(self): - self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "Teal" return Builder.load_string(KV) @@ -270,7 +260,7 @@ def build(self): from kivy.animation import Animation from kivy.clock import Clock from kivy.lang import Builder -from kivy.metrics import dp, sp +from kivy.metrics import dp from kivy.properties import ( BooleanProperty, ColorProperty, @@ -278,18 +268,12 @@ def build(self): StringProperty, ) from kivy.uix.behaviors import ToggleButtonBehavior -from kivy.uix.floatlayout import FloatLayout from kivymd import uix_path -from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import ( - CircularRippleBehavior, - CommonElevationBehavior, - ScaleBehavior, -) +from kivymd.uix.behaviors import CircularRippleBehavior, ScaleBehavior +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.label import MDIcon -from kivymd.utils import asynckivy with open( os.path.join(uix_path, "selectioncontrol", "selectioncontrol.kv"), @@ -299,13 +283,18 @@ def build(self): class MDCheckbox( - CircularRippleBehavior, ScaleBehavior, ToggleButtonBehavior, MDIcon + CircularRippleBehavior, + ScaleBehavior, + ToggleButtonBehavior, + MDIcon, ): """ Checkbox class. For more information, see in the + :class:`~kivymd.uix.behaviors.StateLayerBehavior` and :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.ScaleBehavior` and :class:`~kivy.uix.behaviors.ToggleButtonBehavior` and :class:`~kivymd.uix.label.MDIcon` classes documentation. @@ -394,26 +383,30 @@ class MDCheckbox( and defaults to `None`. """ - disabled_color = ColorProperty(None) + color_disabled = ColorProperty(None) """ Color in (r, g, b, a) or string format when the checkbox is in the disabled state. - .. code-block:: kv + .. versionadded:: 2.0.0 + Use :attr:`color_disabled` instead. - MDCheckbox: - disabled_color: "lightgrey" - disabled: True - active: True + :attr:`color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-disabled-color.png - :align: center + # Deprecated property. + + disabled_color = ColorProperty(None) + """ + Color in (r, g, b, a) or string format when the checkbox is in the disabled state. + + .. deprecated:: 2.0.0 + Use :attr:`color_disabled` instead. :attr:`disabled_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - # Deprecated property. - selected_color = ColorProperty(None, deprecated=True) """ Color in (r, g, b, a) or string format when the checkbox is in the active state. @@ -446,10 +439,6 @@ def __init__(self, **kwargs): scale_value_x=1, scale_value_y=1, duration=0.1, t="out_quad" ) super().__init__(**kwargs) - self.color_active = self.theme_cls.primary_color - self.color_inactive = self.theme_cls.secondary_text_color - self.disabled_color = self.theme_cls.divider_color - self._current_color = self.color_inactive self.check_anim_out.bind( on_complete=lambda *x: self.check_anim_in.start(self) ) @@ -459,38 +448,12 @@ def __init__(self, **kwargs): radio_icon_normal=self.update_icon, radio_icon_down=self.update_icon, group=self.update_icon, - color_active=self.update_color, - color_inactive=self.update_color, - disabled_color=self.update_color, - disabled=self.update_color, - state=self.update_color, - ) - self.theme_cls.bind( - theme_style=self.update_primary_color, - primary_color=self.update_primary_color, ) self.update_icon() - self.update_color() - - def update_primary_color(self, instance, value) -> None: - """ - Called when the values of - :attr:`kivymd.theming.ThemableBehavior.theme_cls.theme_style` and - :attr:`kivymd.theming.ThemableBehavior.theme_cls.primary_color` - change. - """ - - if value in ("Dark", "Light"): - if not self.disabled: - self.color = self.theme_cls.primary_color - else: - self.color = self.disabled_color - else: - self.color_active = value def update_icon(self, *args) -> None: """ - Called when the values of + Fired when the values of :attr:`checkbox_icon_normal` and :attr:`checkbox_icon_down` and :attr:`radio_icon_normal` and @@ -513,26 +476,22 @@ def update_icon(self, *args) -> None: else self.checkbox_icon_normal ) - def update_color(self, *args) -> None: - """ - Called when the values of - :attr:`color_active` and - :attr:`color_inactive` and - :attr:`disabled_color` and - :attr:`disabled` and - :attr:`state` - change. - """ + def set_root_active(self) -> None: + root_checkbox = self.get_widgets("root") + if root_checkbox: + MDCheckbox.__allow_root_checkbox_active = False + root_checkbox[0].active = True in [ + child.active for child in self.get_widgets("child") + ] + MDCheckbox.__allow_root_checkbox_active = True - if self.disabled: - self._current_color = self.disabled_color - elif self.state == "down": - self._current_color = self.color_active - else: - self._current_color = self.color_inactive + def set_child_active(self, active: bool): + for child in self.get_widgets("child"): + child.active = active + MDCheckbox.__allow_child_checkboxes_active = True def on_state(self, *args) -> None: - """Called when the values of :attr:`state` change.""" + """Fired when the values of :attr:`state` change.""" if self.state == "down": self.check_anim_in.cancel(self) @@ -549,7 +508,7 @@ def on_state(self, *args) -> None: self.active = False def on_active(self, *args) -> None: - """Called when the values of :attr:`active` change.""" + """Fired when the values of :attr:`active` change.""" self.state = "down" if self.active else "normal" @@ -563,20 +522,6 @@ def on_active(self, *args) -> None: if MDCheckbox.__allow_child_checkboxes_active: self.set_root_active() - def set_root_active(self) -> None: - root_checkbox = self.get_widgets("root") - if root_checkbox: - MDCheckbox.__allow_root_checkbox_active = False - root_checkbox[0].active = True in [ - child.active for child in self.get_widgets("child") - ] - MDCheckbox.__allow_root_checkbox_active = True - - def set_child_active(self, active: bool): - for child in self.get_widgets("child"): - child.active = active - MDCheckbox.__allow_child_checkboxes_active = True - def on_touch_down(self, touch): if self.collide_point(touch.x, touch.y): if self.group and self.group == "root": @@ -597,7 +542,7 @@ class ThumbIcon(MDIcon): """ -class Thumb(CommonElevationBehavior, CircularRippleBehavior, MDFloatLayout): +class Thumb(CircularRippleBehavior, MDFloatLayout): """Implements a thumb for the :class:`~MDSwitch` widget.""" def _set_ellipse(self, instance, value): @@ -614,13 +559,34 @@ def _set_ellipse(self, instance, value): ) -class MDSwitch(ThemableBehavior, FloatLayout): +class MDSwitch(StateLayerBehavior, MDFloatLayout): """ Switch class. For more information, see in the - :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivy.uix.floatlayout.FloatLayout` classes documentation. + :class:`~kivymd.uix.behaviors.StateLayerBehavior` and + :class:`~kivymd.uix.floatlayout.MDFloatLayout` + classes documentation. + """ + + md_bg_color_disabled = ColorProperty(None) + """ + The background color in (r, g, b, a) or string format of the switch when + the switch is disabled. + + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + ripple_effect = BooleanProperty(True) + """ + Allows or does not allow the ripple effect when activating/deactivating + the switch. + + .. versionadded:: 2.0.0 + + :attr:`ripple_effect` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. """ active = BooleanProperty(False) @@ -818,20 +784,27 @@ class MDSwitch(ThemableBehavior, FloatLayout): and default to `None`. """ + line_color_disabled = ColorProperty(None) + """ + The color of the outline in the disabled state + + .. versionadded:: 2.0.0 + + :attr:`line_color_disabled` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + _thumb_pos = ListProperty([0, 0]) + _line_color = ColorProperty(None) def __init__(self, **kwargs): super().__init__(**kwargs) self.bind(icon_active=self.set_icon, icon_inactive=self.set_icon) - self.size_hint = (None, None) - self.size = (dp(36), dp(48)) - Clock.schedule_once(self._check_style) - Clock.schedule_once(lambda x: self._update_thumb_pos(animation=False)) Clock.schedule_once(lambda x: self.on_active(self, self.active)) def set_icon(self, instance_switch, icon_value: str) -> None: """ - Called when the values of + Fired when the values of :attr:`icon_active` and :attr:`icon_inactive` change. """ @@ -841,43 +814,29 @@ def set_icon(*args): Clock.schedule_once(set_icon, 0.2) + def on_line_color(self, instance, value) -> None: + """Fired when the values of :attr:`line_color` change.""" + + if not self.disabled: + self._line_color = value + def on_active(self, instance_switch, active_value: bool) -> None: - """Called when the values of :attr:`active` change.""" - - if self.theme_cls.material_style == "M3" and self.widget_style != "ios": - size = ( - ( - (dp(16), dp(16)) - if not self.icon_inactive - else (dp(24), dp(24)) - ) - if not active_value - else (dp(24), dp(24)) - ) - icon = "blank" - color = (0, 0, 0, 0) - - if self.icon_active and active_value: - icon = self.icon_active - color = ( - self.icon_active_color - if self.icon_active_color - else self.theme_cls.text_color - ) - elif self.icon_inactive and not active_value: - icon = self.icon_inactive - color = ( - self.icon_inactive_color - if self.icon_inactive_color - else self.theme_cls.text_color - ) - - Animation(size=size, t="out_quad", d=0.2).start(self.ids.thumb) - Animation(color=color, t="out_quad", d=0.2).start( - self.ids.thumb.ids.icon - ) - self.set_icon(self, icon) + """Fired when the values of :attr:`active` change.""" + + size = ( + ((dp(16), dp(16)) if not self.icon_inactive else (dp(24), dp(24))) + if not active_value + else (dp(24), dp(24)) + ) + icon = "blank" + + if self.icon_active and active_value: + icon = self.icon_active + elif self.icon_inactive and not active_value: + icon = self.icon_inactive + Animation(size=size, t="out_quad", d=0.2).start(self.ids.thumb) + self.set_icon(self, icon) self._update_thumb_pos() # FIXME: If you move the cursor from the switch during the @@ -885,63 +844,28 @@ def on_active(self, instance_switch, active_value: bool) -> None: # the previous size does not work. The following code fixes this. def on_thumb_down(self) -> None: """ - Called at the on_touch_down event of the :class:`~Thumb` object. + Fired at the on_touch_down event of the :class:`~Thumb` object. Indicates the state of the switch "on/off" by an animation of increasing the size of the thumb. """ - if self.widget_style != "ios" and self.theme_cls.material_style == "M3": - if self.active: - size = (dp(28), dp(28)) - pos = ( - self.ids.thumb.pos[0] - dp(2), - self.ids.thumb.pos[1] - dp(1.8), - ) - else: - size = (dp(26), dp(26)) - pos = ( - ( - self.ids.thumb.pos[0] - dp(5), - self.ids.thumb.pos[1] - dp(5), - ) - if not self.icon_inactive - else ( - self.ids.thumb.pos[0] + dp(1), - self.ids.thumb.pos[1] - dp(1), - ) - ) - Animation(size=size, pos=pos, t="out_quad", d=0.2).start( - self.ids.thumb - ) + if self.active: + size = (dp(28), dp(28)) + else: + size = (dp(24), dp(24)) + + Animation(size=size, t="out_quad", d=0.2).start(self.ids.thumb) def _update_thumb_pos(self, *args, animation=True): if self.active: _thumb_pos = ( - self.width - - ( - dp(14) - if self.widget_style == "ios" - or self.theme_cls.material_style == "M2" - else dp(28) - ), - self.height / 2 - - ( - dp(12) - if self.widget_style == "ios" - or self.theme_cls.material_style == "M2" - else dp(16) - ), + self.width - dp(46 if self.icon_inactive else 40), + self.height / 2 - dp(16), ) else: _thumb_pos = ( 0 if not self.icon_inactive else dp(-14), - self.height / 2 - - ( - dp(12) - if self.widget_style == "ios" - or self.theme_cls.material_style == "M2" - else dp(16) - ), + self.height / 2 - dp(16), ) Animation.cancel_all(self, "_thumb_pos") @@ -951,7 +875,3 @@ def _update_thumb_pos(self, *args, animation=True): ) else: self._thumb_pos = _thumb_pos - - def _check_style(self, *args): - if self.widget_style == "ios" or self.theme_cls.material_style == "M2": - self.set_icon(self, "") diff --git a/kivymd/uix/slider/__init__.py b/kivymd/uix/slider/__init__.py index a4edb11df..0d97a4770 100644 --- a/kivymd/uix/slider/__init__.py +++ b/kivymd/uix/slider/__init__.py @@ -1 +1 @@ -from .slider import MDSlider # NOQA F401 +from .slider import MDSlider, MDSliderHandle, MDSliderValueLabel # NOQA F401 diff --git a/kivymd/uix/slider/slider.kv b/kivymd/uix/slider/slider.kv index 53323811b..3eb317954 100644 --- a/kivymd/uix/slider/slider.kv +++ b/kivymd/uix/slider/slider.kv @@ -1,169 +1,216 @@ -#:import Thumb kivymd.uix.selectioncontrol.Thumb -#:import get_color_from_hex kivy.utils.get_color_from_hex -#:import colors kivymd.color_definitions.colors - - - - - canvas: Clear + # Inactive track. Color: rgba: ( \ - self.track_color_disabled \ - if self.track_color_disabled else \ - self.theme_cls.disabled_hint_text_color) \ - if self.disabled else \ + self.theme_cls.surfaceVariantColor \ + if not self.track_inactive_color else \ + self.track_inactive_color \ + ) \ + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + [.12] + Rectangle: + size: ( \ + self.width - self.padding * 2, \ + self.track_inactive_width \ + ) \ + if self.orientation == "horizontal" else \ ( \ - self.track_color_active \ - if self.track_color_active else \ + self.track_inactive_width, \ + self.height - self.padding * 2 \ + ) + pos: ( \ - get_color_from_hex(colors["Gray"]["400"]) \ - if app.theme_cls.theme_style == "Light" else \ - (1, 1, 1, .3) \ - ) \ + self.x + self.padding, \ + self.center_y - self.track_inactive_width \ ) \ - if self.active else \ + if self.orientation == "horizontal" else \ ( \ - self.track_color_inactive \ - if self.track_color_inactive else \ + self.center_x - self.track_inactive_width, \ + self.y + self.padding \ + ) + + # Active track. + Color: + rgba: ( \ - self.theme_cls.disabled_hint_text_color \ - if app.theme_cls.theme_style == "Light" else \ - get_color_from_hex(colors["Gray"]["800"]) \ + self.theme_cls.primaryColor \ + if not self.track_active_color else \ + self.track_active_color \ ) \ - ) \ - ) + if not self.disabled else \ + self.theme_cls.onSurfaceColor[:-1] + [.38] Rectangle: size: - (self.width - self.padding * 2 - self._offset[0], dp(4)) if \ - self.orientation == "horizontal" \ - else (dp(4),self.height - self.padding*2 - self._offset[1]) + ( \ + (self.width - self.padding * 2) * self.value_normalized, \ + self.track_active_width \ + ) \ + if self.orientation == "horizontal" else \ + ( \ + self.track_active_width, \ + (self.height - self.padding * 2) * self.value_normalized \ + ) pos: - (self.x + self.padding + self._offset[0], self.center_y - dp(4)) \ + (self.x + self.padding, self.center_y - self.track_active_width) \ if self.orientation == "horizontal" else \ - (self.center_x - dp(4), self.y + self.padding + self._offset[1]) + (self.center_x - self.track_active_width, self.y + self.padding) - # If 0 draw circle + # Points of the inactive track when a step is used. Color: rgba: - (0, 0, 0, 0) if not self._is_off else \ - ( \ - self.track_color_disabled \ - if self.disabled and self.track_color_disabled else \ ( \ - self.theme_cls.disabled_hint_text_color \ - if app.theme_cls.theme_style == "Light" else \ - get_color_from_hex(colors["Gray"]["800"]) \ + self.theme_cls.onPrimaryColor \ + if not self.track_active_step_point_color else \ + self.track_active_step_point_color \ ) \ - ) - Line: - width: 2 - circle: - (self.x + self.padding + dp(3), self.center_y - dp(2), 8 \ - if self.active else 6 ) if self.orientation == "horizontal" \ - else (self.center_x - dp(2), self.y + self.padding + dp(3), 8 \ - if self.active else 6) + if not self.disabled else \ + self.theme_cls.onPrimaryColor[:-1] + [.38] + Point: + points: self._inactive_points + pointsize: self.step_point_size + # Points of the active track when a step is used. Color: rgba: - (0, 0, 0, 0) if self._is_off \ - else \ - ( \ ( \ - self.color if self.color else \ - app.theme_cls.primary_color \ + self.theme_cls.primaryColor \ + if not self.track_inactive_step_point_color else \ + self.track_inactive_step_point_color \ ) \ if not self.disabled else \ - ( \ - self.track_color_disabled \ - if self.track_color_disabled else \ - ( \ - (0, 0, 0, .26) \ - if app.theme_cls.theme_style == "Light" else (1, 1, 1, .3) \ - ) \ - ) \ - ) - Rectangle: - size: - ((self.width - self.padding * 2) * self.value_normalized, sp(4)) \ - if root.orientation == "horizontal" else \ - (sp(4), (self.height - self.padding * 2) * self.value_normalized) - pos: - (self.x + self.padding, self.center_y - dp(4)) \ - if self.orientation == "horizontal" else \ - (self.center_x - dp(4), self.y + self.padding) + self.theme_cls.onSurfaceColor[:-1] + [.38] + Point: + points: self._active_points + pointsize: self.step_point_size + + size_hint_y: None + height: + handle_container.children[0].height \ + if handle_container.children else 0 - Thumb: - id: thumb + # Label container. + BoxLayout: + id: value_container size_hint: None, None - size: - (dp(12), dp(12)) if root.disabled else ((dp(24), dp(24)) \ - if root.active else \ - (dp(16), dp(16))) + size: self.minimum_size + pos: + handle_container.center_x - self.width / 2, \ + root._value_container_y + + # Handle container. + BoxLayout: + id: handle_container + size_hint: None, None + size: self.minimum_size pos: - (root.value_pos[0] - dp(8), root.center_y - thumb.height / 2 - dp(2)) \ - if root.orientation == "horizontal" \ - else (root.center_x - thumb.width / 2 - dp(2), \ - root.value_pos[1] - dp(8)) - md_bg_color: - (0, 0, 0, 0) if root._is_off else \ - ( \ - ( \ - root.thumb_color_disabled \ - if root.thumb_color_disabled else \ - get_color_from_hex(colors["Gray"]["800"]) \ - ) \ - if root.disabled else \ ( \ - (root.thumb_color_active \ - if root.thumb_color_active else \ - root.theme_cls.primary_color \ + root.value_pos[0] - dp(0), \ + root.center_y - self.height / 2 - root.track_active_width / 2 \ ) \ - if root.active else \ + if root.orientation == "horizontal" else \ ( \ - root.thumb_color_inactive \ - if root.thumb_color_inactive else \ - root.theme_cls.primary_color \ - ) \ - ) \ + root.center_x - self.width / 2 - root.track_active_width / 2, \ + root.value_pos[1] - root.track_active_width / 2 \ ) - elevation: 0 if root._is_off else (3 if root.active else 1) - HintBoxContainer: - id: hint_box - size_hint: None, None - md_bg_color: root.hint_bg_color if root.hint_bg_color else [0, 0, 0, 0] - elevation: 1.5 - opacity: 1 if root.active else 0 - radius: root.hint_radius - padding: "6dp", "6dp", "6dp", "8dp" - shadow_color: - ([0, 0, 0, 0.6] if root.hint_bg_color else [0, 0, 0, 0]) \ - if root.active else \ - [0, 0, 0, 0] - size: - lbl_value.width + self.padding[0] * 2, \ - lbl_value.height + self.padding[0] - pos: - (root.value_pos[0] - dp(9), root.center_y - hint_box.height / 2 + dp(30)) \ - if root.orientation == "horizontal" else \ - (root.center_x - hint_box.width / 2 + dp(30), root.value_pos[1] - dp(8)) - - MDLabel: - id: lbl_value - font_style: "Caption" - halign: "center" - theme_text_color: "Custom" - -text_size: None, None - adaptive_size: True - pos_hint: {"center_x": .5, "center_y": .5} - text_color: - app.theme_cls.primary_color \ - if not root.hint_text_color else root.hint_text_color - text: - str(root.value) \ - if isinstance(root.step, float) else str(int(root.value)) + + + _state_layer: state_layer + size_hint: None, None + radius: self.radius + + canvas: + Color: + rgba: + ( \ + self.theme_cls.primaryColor \ + if self.theme_bg_color == "Primary" else \ + self.md_bg_color \ + ) \ + if not self.disabled else self.theme_cls.onSurfaceColor + RoundedRectangle: + radius: self.radius + size: self.size + pos: self.pos + + MDSliderHandleStateLayer: + id: state_layer + + canvas.before: + Color: + rgba: + ( \ + app.theme_cls.primaryColor \ + if not root.state_layer_color else \ + root.state_layer_color \ + )[:-1] + [0.38] + RoundedRectangle: + radius: [root.state_layer_size[0] / 2, ] + size: root.state_layer_size + pos: + root.x - root.state_layer_size[0] / 4, \ + root.y - root.state_layer_size[1] / 4 + + + + canvas: + Color: + rgba: app.theme_cls.primaryColor + RoundedRectangle: + radius: [self.size[0] / 2, ] + pos: self.pos + size: self.size + + Triangle: + points: + [ \ + self.x + 18, self.y - 6, \ + self.x + 32, self.y + 8, \ + self.x + 2, self.y + 8 \ + ] + + # Label texture. + Color: + group: "md-slider-label-value-color" + Rectangle: + group: "md-slider-label-value-rect" + texture: + self._slider._value_label.texture \ + if self._slider and self._slider._value_label else \ + None + pos: + ( \ + ( \ + self._slider.value_pos[0] - (self._slider._value_label.texture_size[0] / 2) + dp(10), \ + self._slider.center_y + dp(28) \ + ) \ + if self._slider.orientation == "horizontal" else \ + ( \ + self._slider.center_x - self.width / 2, \ + self._slider.value_pos[1] - dp(0) \ + ) \ + ) \ + if self._slider and self._slider._value_label else (0, 0) + size: + self._slider._value_label.texture_size \ + if self._slider and self._slider._value_label else (0, 0) + + size_hint: None, None + # opacity: 0 + size: + self._slider._value_label.size \ + if self._slider and self._slider._value_label else \ + (0, 0) + + + + font_style: "Label" + role: "medium" + size_hint: None, None + theme_text_color: "Custom" + text_color: self.theme_cls.onPrimaryColor + halign: "center" diff --git a/kivymd/uix/slider/slider.py b/kivymd/uix/slider/slider.py index f0824ae3e..602186a72 100644 --- a/kivymd/uix/slider/slider.py +++ b/kivymd/uix/slider/slider.py @@ -4,29 +4,81 @@ .. seealso:: - `Material Design spec, Sliders `_ + `Material Design spec, Sliders `_ .. rubric:: Sliders allow users to make selections from a range of values. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider.png :align: center + +- Sliders should present the full range of choices that are available +- Two types: continuous and discrete +- The slider should immediately reflect any input made by a user + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliders-types.png + :align: center + +1. Continuous slider +2. Discrete slider + +Usage +----- + +.. code-block:: python + + MDSlider( + MDSliderHandle( + ), + MDSliderValueLabel( + ), + step=10, + value=50, + ) + +.. code-block:: kv + + MDSlider: + step: 10 + value: 50 + + MDSliderHandle: + + MDSliderValueLabel: + + +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider-anatomy.png + :align: center """ -__all__ = ("MDSlider",) +__all__ = ("MDSlider", "MDSliderHandle", "MDSliderValueLabel") import os +from kivy.animation import Animation from kivy.clock import Clock from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ( - BooleanProperty, - ColorProperty, ListProperty, VariableListProperty, + StringProperty, + NumericProperty, + ObjectProperty, + ColorProperty, ) from kivy.uix.slider import Slider +from kivy.uix.widget import Widget +from kivymd.uix.label import MDLabel +from kivymd.uix.behaviors import ( + ScaleBehavior, + DeclarativeBehavior, + BackgroundColorBehavior, +) +from kivymd.uix.behaviors.focus_behavior import FocusBehavior from kivymd import uix_path from kivymd.theming import ThemableBehavior @@ -36,282 +88,488 @@ Builder.load_string(kv_file.read()) -class MDSlider(ThemableBehavior, Slider): - """ - Class for creating a Slider widget. See in the - :class:`~kivy.uix.slider.Slider` class documentation. +class MDSlider(DeclarativeBehavior, ThemableBehavior, Slider): """ + Slider class. - active = BooleanProperty(False) + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.slider.Slider` + classes documentation. """ - If the slider is clicked. - :attr:`active` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. + track_active_width = NumericProperty(dp(4)) """ + Width of the active track. + + .. versionadded:: 2.0.0 - color = ColorProperty(None) + :attr:`track_active_width` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(4)`. """ - Color slider in (r, g, b, a) or string format. - .. code-block:: kv + track_inactive_width = NumericProperty(dp(4)) + """ + Width of the inactive track. - MDSlider - color: "red" + .. versionadded:: 2.0.0 - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-color.png - :align: center + :attr:`track_inactive_width` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(4)`. + """ - :attr:`color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. + step_point_size = NumericProperty(dp(1)) """ + Step point size. - hint = BooleanProperty(True) + .. versionadded:: 2.0.0 + + :attr:`step_point_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(1)`. """ - If True, then the current value is displayed above the slider. - .. code-block:: kv + track_active_color = ColorProperty(None) + """ + Color of the active track. - MDSlider - hint: True + .. versionadded:: 2.0.0 - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-hint.png - :align: center + .. versionchanged:: 2.0.0 + Rename from `track_color_active` to `track_active_color` - :attr:`hint` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. + :attr:`track_active_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - hint_bg_color = ColorProperty(None) + track_active_step_point_color = ColorProperty(None) """ - Hint rectangle color in (r, g, b, a) or string format. - - .. code-block:: kv + Color of step points on active track. - MDSlider - hint: True - hint_bg_color: "red" + .. versionadded:: 2.0.0 - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-hint-bg-color.png - :align: center + :attr:`track_active_step_point_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ - :attr:`hint_bg_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + track_inactive_step_point_color = ColorProperty(None) """ + Color of step points on inactive track. + + .. versionadded:: 2.0.0 - hint_text_color = ColorProperty(None) + :attr:`track_inactive_step_point_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - Hint text color in in (r, g, b, a) or string format. - .. code-block:: kv + track_inactive_color = ColorProperty(None) + """ + Color of the inactive track. - MDSlider - hint: True - hint_bg_color: "red" - hint_text_color: "white" + .. versionadded:: 2.0.0 - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-hint-text-color.png - :align: center + .. versionchanged:: 2.0.0 + Rename from `track_color_inactive` to `track_inactive_color` - :attr:`hint_text_color` is an :class:`~kivy.properties.ColorProperty` + :attr:`track_active_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - hint_radius = VariableListProperty([dp(4), dp(4), dp(4), dp(4)]) + value_container_show_anim_duration = NumericProperty(0.2) """ - Hint radius. + Duration of the animation opening of the label value. - .. code-block:: kv + .. versionadded:: 2.0.0 - MDSlider - hint: True - hint_bg_color: "red" - hint_text_color: "white" - hint_radius: [6, 0, 6, 0] + :attr:`value_container_show_anim_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + value_container_hide_anim_duration = NumericProperty(0.2) + """ + Duration of closing the animation of the label value. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-hint-radius.png - :align: center + .. versionadded:: 2.0.0 - :attr:`hint_radius` is an :class:`~kivy.properties.VariableListProperty` - and defaults to `[dp(4), dp(4), dp(4), dp(4)]`. + :attr:`value_container_hide_anim_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. """ - thumb_color_active = ColorProperty(None) + value_container_show_anim_transition = StringProperty("out_circ") """ - The color in (r, g, b, a) or string format of the thumb when the slider is active. + The type of the opening animation of the label value. - .. versionadded:: 1.0.0 + .. versionadded:: 2.0.0 - .. code-block:: kv + :attr:`value_container_show_anim_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `'out_circ'`. + """ - MDSlider - thumb_color_active: "red" + value_container_hide_anim_transition = StringProperty("out_circ") + """ + The type of the closing animation of the label value. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-thumb-color-active.png - :align: center + .. versionadded:: 2.0.0 - :attr:`thumb_color_active` is an :class:`~kivy.properties.ColorProperty` - and default to `None`. + :attr:`value_container_hide_anim_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `'out_circ'`. """ - thumb_color_inactive = ColorProperty(None) + handle_anim_transition = StringProperty("out_circ") """ - The color in (r, g, b, a) or string format of the thumb when the slider is inactive. + Handle animation type. - .. versionadded:: 1.0.0 + .. versionadded:: 2.0.0 - .. code-block:: kv + :attr:`handle_anim_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `'out_circ'`. + """ - MDSlider - thumb_color_inactive: "red" + handle_anim_duration = NumericProperty(0.2) + """ + Handle animation duration. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-thumb-color-inactive.png - :align: center + .. versionadded:: 2.0.0 - :attr:`thumb_color_inactive` is an :class:`~kivy.properties.ColorProperty` - and default to `None`. + :attr:`handle_anim_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. """ - thumb_color_disabled = ColorProperty(None) - """ - The color in (r, g, b, a) or string format of the thumb when the slider is - in the disabled state. + _value_label_container_size = ListProperty([0, 0]) # value label texture + _value_label = ObjectProperty() # value label texture + _value_container = ObjectProperty() # MDSliderValueContainer object + _value_container_y = NumericProperty(0) # MDSliderValueContainer object + _handle = ObjectProperty() # MDSliderHandle object + # List of points displayed on the slider when using the `step` for th + # active/inactive tracks. + _active_points = ListProperty() + _inactive_points = ListProperty() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + Clock.schedule_once(self._update_state_layer_pos, 0.5) + Clock.schedule_once(self.on_size) + + def add_widget(self, widget, index=0, canvas=None): + def set_value_container_y(*args): + self._value_container_y = self.ids.handle_container.y + + if isinstance(widget, MDSliderValueLabel): + self._value_label = widget + self._value_container = MDSliderValueContainer(_slider=self) + self.ids.value_container.add_widget(self._value_container) + Clock.schedule_once(set_value_container_y) + elif isinstance(widget, MDSliderHandle): + widget._slider = self + self._handle = widget + self.ids.handle_container.add_widget(widget) + else: + return super().add_widget(widget) + + def update_points(self, instance, step) -> None: + """Draws the step points on the slider.""" + + def update_points(*args): + y = self.center_y - self.track_active_width / 2 + slider_length = self.width - (self.padding * 2) + slider_max_value = int(self.max) + multiplier = slider_length / slider_max_value + active_track_width = ( + self.width - self.padding * 2 + ) * self.value_normalized + + for i in range(0, slider_max_value + 1, step): + x = i * multiplier + + if x < active_track_width: + points = self._inactive_points + else: + points = self._active_points + + points.append( + self.x + + x + + self.padding + + ( + (self.ids.handle_container.width / 2) + if i != self.max and i + else 0 + ) + ) + points.append(y) + + Clock.schedule_once(update_points) + + def on_size(self, *args) -> None: + """Fired when the widget is resized.""" + + self._update_points() + + def on_touch_down(self, touch): + if self.disabled or not self.collide_point(*touch.pos): + return + if touch.is_mouse_scrolling: + if "down" in touch.button or "left" in touch.button: + if self.step: + self.value = min(self.max, self.value + self.step) + else: + self.value = min( + self.max, self.value + (self.max - self.min) / 20 + ) + if "up" in touch.button or "right" in touch.button: + if self.step: + self.value = max(self.min, self.value - self.step) + else: + self.value = max( + self.min, self.value - (self.max - self.min) / 20 + ) + elif self.sensitivity == "handle": + if self.children[0].collide_point(*touch.pos): + touch.grab(self) + else: + touch.grab(self) + Clock.schedule_once(self._update_state_layer_pos) + Animation(value_pos=touch.pos, d=0.2).start(self) + + return True + + def on_value_pos(self, *args) -> None: + """ + Fired when the `value_pos` value changes. + Sets a new value for the value label texture. + """ - .. versionadded:: 1.0.0 + self._update_points() - .. code-block:: kv + if self._value_label and self._value_container: + # FIXME: I do not know how else I can update the texture. + self._value_label.text = "" + self._value_label.text = f"{int(self.value)}" + self._value_label.texture_update() + label_value_rect = self._value_container.canvas.get_group( + "md-slider-label-value-rect" + )[0] + label_value_rect.texture = None + label_value_rect.texture = self._value_label.texture + label_value_rect.size = self._value_label.texture_size - MDSlider - value: 55 - disabled: True - thumb_color_disabled: "red" + def on_touch_up(self, touch): + if touch.grab_current == self: + if self._handle: + self._handle.on_leave() + return True + + def on_touch_move(self, touch): + if self.collide_point(touch.x, touch.y): + if self._handle: + self._update_state_layer_pos() + if self._handle and not self._handle._active: + self._handle.on_enter() + return super().on_touch_move(touch) + + def on_handle_enter(self) -> None: + """Scales the container of the label value.""" + + if self._handle and self._value_label: + Animation( + scale_value_x=1, + scale_value_y=1, + t=self.value_container_show_anim_transition, + d=self.value_container_show_anim_duration, + ).start(self._value_container) + Animation( + _value_container_y=self.ids.handle_container.y + dp(32), + t=self.value_container_show_anim_transition, + d=self.value_container_show_anim_duration, + ).start(self) + + def on_handle_leave(self) -> None: + """Scales the container of the label value.""" + + if self._handle and self._value_label: + Animation( + scale_value_x=0, + scale_value_y=0, + # opacity=1, + d=self.value_container_hide_anim_duration, + t=self.value_container_hide_anim_transition, + ).start(self._value_container) + Animation( + _value_container_y=self._value_container_y - dp(24), + t=self.value_container_hide_anim_transition, + d=self.value_container_hide_anim_duration, + ).start(self) + + def _update_points(self, *args) -> None: + if self.step: + self._active_points = [] + self._inactive_points = [] + self.update_points(self, self.step) + + def _update_state_layer_pos(self, *args): + if self._handle: + self._handle.ids.state_layer.scale_value_center = ( + self.ids.handle_container.center + ) + + +class MDSliderHandle( + ThemableBehavior, BackgroundColorBehavior, FocusBehavior, Widget +): + """ + Handle class. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-thumb-color-disabled.png - :align: center + .. versionadded:: 2.0.0 - :attr:`thumb_color_disabled` is an :class:`~kivy.properties.ColorProperty` - and default to `None`. + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.focus_behavior.FocusBehavior` and + :class:`~kivy.uix.widget.Widget` + classes documentation. """ - track_color_active = ColorProperty(None) + radius = VariableListProperty([dp(10)], length=4) """ - The color in (r, g, b, a) or string format of the track when the slider is active. + Handle radius. - .. versionadded:: 1.0.0 + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(10), dp(10), dp(10), dp(10)]`. + """ - .. code-block:: kv + size = ListProperty([dp(20), dp(20)]) + """ + Handle size. - MDSlider - track_color_active: "red" + :attr:`size` is an :class:`~kivy.properties.ListProperty` + and defaults to `[dp(20), dp(20)]`. + """ - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-track-color-active.png - :align: center + state_layer_size = ListProperty([dp(40), dp(40)]) + """ + Handle state layer size. - :attr:`track_color_active` is an :class:`~kivy.properties.ColorProperty` - and default to `None`. + :attr:`state_layer_size` is an :class:`~kivy.properties.ListProperty` + and defaults to `[dp(40), dp(40)]`. """ - track_color_inactive = ColorProperty(None) + state_layer_color = ColorProperty(None) """ - The color in (r, g, b, a) or string format of the track when the slider is inactive. + Handle state layer color. - .. versionadded:: 1.0.0 + :attr:`state_layer_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ - .. code-block:: kv + _slider = ObjectProperty() # MDSlider object + _active = False # is the layer currently displayed + _state_layer = ObjectProperty() # MDSliderStateLayer object - MDSlider - track_color_inactive: "red" + def on_enter(self) -> None: + """ + Fired when mouse enter the bbox of the widget. + Animates the display of the slider handle layer. + """ - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-track-color-inactive.png - :align: center + if self._slider: + if self._state_layer and not self._slider.disabled: + self._active = True + anim = Animation(scale_value_x=1, scale_value_y=1, d=0.2) + anim.bind(on_complete=self._slider._update_state_layer_pos) + anim.start(self._state_layer) + if not self._slider.disabled: + self._slider.on_handle_enter() - :attr:`track_color_inactive` is an :class:`~kivy.properties.ColorProperty` - and default to `None`. - """ + def on_leave(self) -> None: + """ + Fired when the mouse goes outside the widget border. + Animates the hiding of the slider handle layer. + """ - track_color_disabled = ColorProperty(None) - """ - The color in (r, g, b, a) or string format of the track when the slider is - in the disabled state. + if self._slider: + if self._state_layer and not self._slider.disabled: + self._active = False + anim = Animation(scale_value_x=0, scale_value_y=0, d=0.2) + anim.bind(on_complete=self._slider._update_state_layer_pos) + anim.start(self._state_layer) + if not self._slider.disabled: + self._slider.on_handle_leave() - .. versionadded:: 1.0.0 - .. code-block:: kv +class MDSliderHandleStateLayer(ScaleBehavior, Widget): + """ + Slider state layer class. - MDSlider - disabled: True - track_color_disabled: "red" + .. versionadded:: 2.0.0 - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slide-track-color-disabled.png - :align: center + For more information, see in the + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and + :class:`~kivy.uix.widget.Widget` + classes documentation. + """ - :attr:`track_color_disabled` is an :class:`~kivy.properties.ColorProperty` - and default to `None`. + scale_value_x = NumericProperty(0) """ + X-axis value. - show_off = BooleanProperty(True) + :attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. """ - Show the `'off'` ring when set to minimum value. - :attr:`show_off` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. + scale_value_y = NumericProperty(0) """ + Y-axis value. - _thumb_pos = ListProperty([0, 0]) - # Internal state of ring. - _is_off = BooleanProperty(False) - # Internal adjustment to reposition sliders for ring. - _offset = ListProperty((0, 0)) + :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - Clock.schedule_once(self.set_thumb_icon) - def set_thumb_icon(self, *args) -> None: - self.ids.thumb.ids.icon.icon = "blank" +class MDSliderValueLabel(MDLabel): + """ + Implements the value label. - def on_hint(self, instance, value) -> None: - def on_hint(*args): - if not value: - self.remove_widget(self.ids.hint_box) + For more information, see in the :class:`~kivymd.uix.label.label.MDLabel` + class documentation. - # Schedule using for declarative style. - # Otherwise get AttributeError exception. - Clock.schedule_once(on_hint) + .. versionadded:: 2.0.0 + """ - def on_value_normalized(self, *args) -> None: - """ - When the ``value == min`` set it to `'off'` state and make slider - a ring. - """ + size = ListProperty([dp(36), dp(36)]) + """ + Container size for the label value. - self._update_is_off() + :attr:`handle_anim_transition` is an :class:`~kivy.properties.ListProperty` + and defaults to `[dp(36), dp(36)]`. + """ - def on_show_off(self, *args) -> None: - self._update_is_off() - def on__is_off(self, *args) -> None: - self._update_offset() +class MDSliderValueContainer(ScaleBehavior, Widget): + """ + Implements the container for value label. - def on_active(self, *args) -> None: - self._update_offset() + For more information, see in the + :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and + :class:`~kivy.uix.widget.Widget` + classes documentation. - def on_touch_down(self, touch): - if super().on_touch_down(touch): - self.active = True + .. versionadded:: 2.0.0 + """ - def on_touch_up(self, touch): - if super().on_touch_up(touch): - self.active = False + scale_value_x = NumericProperty(0) + """ + X-axis value. - def _update_offset(self): - """ - Offset is used to shift the sliders so the background color - shows through the off circle. - """ + :attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + scale_value_y = NumericProperty(0) + """ + Y-axis value. - d = 2 if self.active else 0 - self._offset = (dp(11 + d), dp(11 + d)) if self._is_off else (0, 0) + :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ - def _update_is_off(self): - self._is_off = self.show_off and (self.value_normalized == 0) + _slider = ObjectProperty() # MDSlider object diff --git a/kivymd/uix/snackbar/__init__.py b/kivymd/uix/snackbar/__init__.py index a6fd8ec82..c83a2cf2d 100644 --- a/kivymd/uix/snackbar/__init__.py +++ b/kivymd/uix/snackbar/__init__.py @@ -1,6 +1,9 @@ from .snackbar import ( # NOQA F401 MDSnackbar, + MDSnackbarText, + MDSnackbarSupportingText, MDSnackbarActionButton, + MDSnackbarActionButtonText, + MDSnackbarButtonContainer, MDSnackbarCloseButton, - Snackbar, ) diff --git a/kivymd/uix/snackbar/snackbar.kv b/kivymd/uix/snackbar/snackbar.kv index 4334bbf30..7ed7d7cf8 100644 --- a/kivymd/uix/snackbar/snackbar.kv +++ b/kivymd/uix/snackbar/snackbar.kv @@ -1,28 +1,78 @@ -#:import SNACK_BAR_ELEVATION kivymd.material_resources.SNACK_BAR_ELEVATION -#:import SNACK_BAR_OFFSET kivymd.material_resources.SNACK_BAR_OFFSET - - padding: 0, 0, "8dp", 0 + theme_bg_color: "Custom" + theme_elevation_level: "Custom" + theme_elevation_level: "Custom" + style: "elevated" + shadow_radius: self.radius + elevation_level: 3 size_hint_y: None height: self.minimum_height - md_bg_color: "#323232" - elevation: SNACK_BAR_ELEVATION - shadow_offset: SNACK_BAR_OFFSET + orientation: "vertical" + md_bg_color: + self.theme_cls.inverseSurfaceColor \ + if not self.background_color else \ + self.background_color - SnackbarLabelContainer: + BoxLayout: id: label_container padding: "16dp", "15dp", 0, "15dp" orientation: "vertical" - adaptive_height: True - pos_hint: {"center_y": .5} + size_hint_y: None + height: self.minimum_height + pos_hint: {"top": 1} spacing: "4dp" - SnackbarActionButtonContainer: - id: action_container + BoxLayout: + id: button_container size_hint_x: None + width: self.minimum_width - SnackbarCloseButtonContainer: - id: close_container - size_hint_x: None - width: "38dp" + + + size_hint_y: None + height: self.minimum_height + + + + adaptive_size: True + font_style: "Label" + role: "large" + markup: True + text_color: + self.theme_cls.inversePrimaryColor \ + if self.theme_text_color == "Primary" else \ + self.text_color + + + + adaptive_height: True + font_style: "Body" + role: "medium" + markup: True + text_color: + self.theme_cls.inverseOnSurfaceColor \ + if self.theme_text_color == "Primary" else \ + self.text_color + + + style: "text" + pos_hint: {"right": 1} + + + + text_color: + self.theme_cls.inverseOnSurfaceColor \ + if self.theme_icon_color == "Primary" else \ + self.icon_color + + + + adaptive_size: True + font_style: "Label" + role: "large" + markup: True + color: + self.theme_cls.inversePrimaryColor \ + if self.theme_text_color == "Primary" else \ + self.text_color diff --git a/kivymd/uix/snackbar/snackbar.py b/kivymd/uix/snackbar/snackbar.py index fdb7ebe45..8b6902779 100755 --- a/kivymd/uix/snackbar/snackbar.py +++ b/kivymd/uix/snackbar/snackbar.py @@ -12,227 +12,178 @@ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar.png :align: center +- Snackbars shouldn’t interrupt the user’s experience +- Usually appear at the bottom of the UI +- Can disappear on their own or remain on screen until the user takes action + Usage ----- .. code-block:: python MDSnackbar( - MDLabel( - text="First string", - theme_text_color="Custom", - text_color="#393231", + MDSnackbarText( + text="Text", ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, ).open() -Example -------- - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.label import MDLabel - from kivymd.uix.snackbar import MDSnackbar - - - KV = ''' - MDScreen: - - MDRaisedButton: - text: "Create simple snackbar" - on_release: app.open_snackbar() - pos_hint: {"center_x": .5, "center_y": .5} - ''' +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-anatomy-detail.png + :align: center +1. Container +2. Supporting text +3. Action (optional) +4. Icon (optional close affordance) - class Example(MDApp): - def open_snackbar(self): - MDSnackbar( - MDLabel( - text="First string", - ), - ).open() +Anatomy +------- - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-anatomy.png + :align: center +Configurations +============== - Example().run() +1. Single line +-------------- -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-simple.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-configurations-single-line.png :align: center -Control width and pos ---------------------- - .. code-block:: python MDSnackbar( - MDLabel( - text="First string", + MDSnackbarText( + text="Single-line snackbar", ), - pos=(dp(24), dp(56)), + y=dp(24), + pos_hint={"center_x": 0.5}, size_hint_x=0.5, ).open() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-widith-and-pos.gif - :align: center -On mobile, use up to two lines of text to communicate the snackbar message: +2. Single-line snackbar with action +----------------------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-configurations-single-line-with-action.png + :align: center .. code-block:: python MDSnackbar( - MDLabel( - text="First string", - theme_text_color="Custom", - text_color="#393231", + MDSnackbarSupportingText( + text="Single-line snackbar with action", ), - MDLabel( - text="Second string", - theme_text_color="Custom", - text_color="#393231", + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText( + text="Action button" + ), + ), + pos_hint={"center_y": 0.5} ), y=dp(24), + orientation="horizontal", pos_hint={"center_x": 0.5}, size_hint_x=0.5, - md_bg_color="#E8D8D7", ).open() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-two-line.gif - :align: center - -Usage action button -------------------- +3. Single-line snackbar with action and close buttons +----------------------------------------------------- -A snackbar can contain a single action. "Dismiss" or "cancel" actions are -optional: +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-configurations-single-line-with-action-and-close-buttons.png + :align: center .. code-block:: python MDSnackbar( - MDLabel( - text="First string", - theme_text_color="Custom", - text_color="#393231", + MDSnackbarSupportingText( + text="Single-line snackbar with action and close buttons", ), - MDSnackbarActionButton( - text="Done", - theme_text_color="Custom", - text_color="#8E353C", + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText( + text="Action button" + ), + ), + MDSnackbarCloseButton( + icon="close", + ), + pos_hint={"center_y": 0.5} ), y=dp(24), + orientation="horizontal", pos_hint={"center_x": 0.5}, size_hint_x=0.5, - md_bg_color="#E8D8D7", ).open() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-action-button.gif - :align: center - -Callback action button ----------------------- +4. Two-line snackbar with action and close buttons +-------------------------------------------------- -.. code-block:: python - - def snackbar_action_button_callback(self, *args): - print("Snackbar callback action button") - - def open_snackbar(self): - self.snackbar = MDSnackbar( - MDLabel( - text="First string", - theme_text_color="Custom", - text_color="#393231", - ), - MDSnackbarActionButton( - text="Done", - theme_text_color="Custom", - text_color="#8E353C", - _no_ripple_effect=True, - on_release=self.snackbar_action_button_callback, - ), - y=dp(24), - pos_hint={"center_x": 0.5}, - size_hint_x=0.5, - md_bg_color="#E8D8D7", - ) - self.snackbar.open() - -If an action is long, it can be displayed on a third line: +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-configurations-two-line-with-action-and-close-buttons.png + :align: center .. code-block:: python MDSnackbar( - MDLabel( - text="If an action is long, it can be displayed", - theme_text_color="Custom", - text_color="#393231", + MDSnackbarText( + text="Single-line snackbar", ), - MDLabel( - text="on a third line.", - theme_text_color="Custom", - text_color="#393231", + MDSnackbarSupportingText( + text="with action and close buttons", ), - MDLabel( - text=" ", - ), - MDSnackbarActionButton( - text="Action button", - theme_text_color="Custom", - text_color="#8E353C", - y=dp(8), - _no_ripple_effect=True, + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText( + text="Action button" + ), + ), + MDSnackbarCloseButton( + icon="close", + ), + pos_hint={"center_y": 0.5} ), y=dp(24), + orientation="horizontal", pos_hint={"center_x": 0.5}, size_hint_x=0.5, - md_bg_color="#E8D8D7", ).open() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-action-button-on-thrid-line.gif - :align: center +5. Two-line snackbar with action and close buttons at the bottom +---------------------------------------------------------------- -Icon (optional close affordance): +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snakbar-configurations-two-line-with-action-and-close-buttons-bottom.png + :align: center .. code-block:: python - def snackbar_close(self, *args): - self.snackbar.dismiss() - - def open_snackbar(self): - self.snackbar = MDSnackbar( - MDLabel( - text="Icon (optional close affordance)", - theme_text_color="Custom", - text_color="#393231", - ), + MDSnackbar( + MDSnackbarText( + text="Single-line snackbar with action", + ), + MDSnackbarSupportingText( + text="and close buttons at the bottom", + padding=[0, 0, 0, dp(56)], + ), + MDSnackbarButtonContainer( + Widget(), MDSnackbarActionButton( - text="Action button", - theme_text_color="Custom", - text_color="#8E353C", - _no_ripple_effect=True, + MDSnackbarActionButtonText( + text="Action button" + ), ), MDSnackbarCloseButton( icon="close", - theme_text_color="Custom", - text_color="#8E353C", - _no_ripple_effect=True, - on_release=self.snackbar_close, ), - y=dp(24), - pos_hint={"center_x": 0.5}, - size_hint_x=0.5, - md_bg_color="#E8D8D7", - ) - self.snackbar.open() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-optional-close-affordance.gif - :align: center + ), + y=dp(124), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + padding=[0, 0, "8dp", "8dp"], + ).open() API break ========= @@ -279,37 +230,61 @@ def open_snackbar(self): size_hint_x=0.5, md_bg_color="#E8D8D7", ).open() + +2.2.0 version +------------- + +.. code-block:: python + + MDSnackbar( + MDSnackbarSupportingText( + text="Single-line snackbar with action", + ), + MDSnackbarButtonContainer( + MDSnackbarActionButton( + MDSnackbarActionButtonText( + text="Action button" + ), + ), + pos_hint={"center_y": 0.5} + ), + y=dp(24), + orientation="horizontal", + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + background_color=self.theme_cls.onPrimaryContainerColor, + ).open() """ __all__ = ( "MDSnackbar", + "MDSnackbarText", + "MDSnackbarSupportingText", + "MDSnackbarButtonContainer", "MDSnackbarActionButton", + "MDSnackbarActionButtonText", "MDSnackbarCloseButton", ) import os -from kivy import Logger -from kivy.animation import Animation 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 ( BooleanProperty, - ColorProperty, ListProperty, - NumericProperty, - OptionProperty, - StringProperty, + NumericProperty, ColorProperty, ) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.widget import Widget from kivymd import uix_path -from kivymd.uix.behaviors import MotionShackBehavior -from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.button import MDFlatButton, MDIconButton +from kivymd.uix.behaviors import MotionShackBehavior, DeclarativeBehavior +from kivymd.uix.button import MDButton, MDIconButton, MDButtonText from kivymd.uix.card import MDCard from kivymd.uix.label import MDLabel -from kivymd.uix.relativelayout import MDRelativeLayout with open( os.path.join(uix_path, "snackbar", "snackbar.kv"), encoding="utf-8" @@ -317,16 +292,25 @@ def open_snackbar(self): Builder.load_string(kv_file.read()) -class SnackbarLabelContainer(MDBoxLayout): - """Container for placing snackbar text.""" +class MDSnackbarButtonContainer(DeclarativeBehavior, BoxLayout): + """ + The class implements a container for placing snackbar buttons. + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation. + """ -class SnackbarActionButtonContainer(MDRelativeLayout): - """Container for placing snackbar action button.""" + def add_widget(self, widget, *args, **kwargs): + def set_container_width(w): + self.parent.width += w.width + if isinstance( + widget, (MDSnackbarActionButton, MDSnackbarCloseButton, Widget) + ): + Clock.schedule_once(lambda x: set_container_width(widget), 0.2) -class SnackbarCloseButtonContainer(MDRelativeLayout): - """Container for placing snackbar close button.""" + return super().add_widget(widget) class MDSnackbarCloseButton(MDIconButton): @@ -334,28 +318,30 @@ class MDSnackbarCloseButton(MDIconButton): Snackbar closed button class. For more information, see in the - :class:`~kivymd.uix.button.MDIconButton` class documentation. + :class:`~kivymd.uix.button.button.MDIconButton` class documentation. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.y and not self.pos_hint: - self.pos_hint = {"center_y": 0.5} + +class MDSnackbarActionButtonText(MDButtonText): + """ + The class implements the text for the :class:`~MDSnackbarActionButton` + class. + + .. versionchanged:: 2.2.0 + + For more information, see in the + :class:`~kivymd.uix.button.button.MDButtonText` class documentation. + """ -class MDSnackbarActionButton(MDFlatButton): +class MDSnackbarActionButton(MDButton): """ Snackbar action button class. For more information, see in the - :class:`~kivymd.uix.button.MDFlatButton` class documentation. + :class:`~kivymd.uix.button.button.MDButton` class documentation. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.y and not self.pos_hint: - self.pos_hint = {"center_y": 0.5} - class MDSnackbar(MotionShackBehavior, MDCard): """ @@ -365,15 +351,15 @@ class MDSnackbar(MotionShackBehavior, MDCard): Rename `BaseSnackbar` to `MDSnackbar` class. For more information, see in the - :class:`~kivymd.uix.card.MDCard` and - :class:`~kivymd.uix.behaviors.StencilBehavior` + :class:`~kivymd.uix.behaviors.motion_behavior.MotionShackBehavior` and + :class:`~kivymd.uix.card.card.MDCard` and class documentation. :Events: :attr:`on_open` - Called when a snackbar opened. + Fired when a snackbar opened. :attr:`on_dismiss` - Called when a snackbar closes. + Fired when a snackbar closes. """ duration = NumericProperty(3) @@ -392,70 +378,22 @@ class documentation. and defaults to `True`. """ - radius = ListProperty([5, 5, 5, 5]) + radius = ListProperty([dp(4), dp(4), dp(4), dp(4)]) """ Snackbar radius. :attr:`radius` is a :class:`~kivy.properties.ListProperty` - and defaults to `[5, 5, 5, 5]` + and defaults to `[dp(4), dp(4), dp(4), dp(4)]` """ - bg_color = ColorProperty(None, deprecated=True) + background_color = ColorProperty(None) """ - Snackbar background color in (r, g, b, a) or string format. + The background color in (r, g, b, a) or string format of the snackbar. - .. deprecated:: 1.2.0 - Use 'md_bg_color` instead. - - :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` + :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - buttons = ListProperty(deprecated=True) - """ - Snackbar buttons. - - .. deprecated:: 1.2.0 - - :attr:`buttons` is a :class:`~kivy.properties.ListProperty` - and defaults to `[]` - """ - - snackbar_animation_dir = OptionProperty( - "Bottom", - options=["Top", "Bottom", "Left", "Right"], - deprecated=True, - ) - """ - Snackbar animation direction. - Available options are: `'Top'`, `'Bottom'`, `'Left'`, `'Right'`. - - .. deprecated:: 1.2.0 - - :attr:`snackbar_animation_dir` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'Bottom'`. - """ - - snackbar_x = NumericProperty(0, deprecated=True) - """ - The snackbar x position in the screen - - .. deprecated:: 1.2.0 - - :attr:`snackbar_x` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0`. - """ - - snackbar_y = NumericProperty(0, deprecated=True) - """ - The snackbar x position in the screen - - .. deprecated:: 1.2.0 - - :attr:`snackbar_y` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0`. - """ - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.register_event_type("on_open") @@ -470,80 +408,40 @@ def dismiss(self, *args) -> None: def open(self) -> None: """Show the snackbar.""" - for widget in Window.parent.children: - if widget.__class__ is MDSnackbar: - return - - Window.parent.add_widget(self) + Window.add_widget(self) super().on_open() def add_widget(self, widget, *args, **kwargs): - def check_color(color): - if not widget.text_color: - widget.theme_text_color = "Custom" - widget.text_color = color - - if isinstance(widget, MDSnackbarCloseButton): - widget.icon_size = "20sp" - check_color("white") - self.ids.close_container.add_widget(widget) - if len(self.ids.close_container.children) >= 2: - Logger.warning( - "KivyMD: " - "Do not use more than one button to close the snackbar. " - "This is contrary to the material design rules " - "of version 3" - ) - if isinstance(widget, MDSnackbarActionButton): - self.ids.action_container.add_widget(widget) - check_color(self.theme_cls.primary_color) - if len(self.ids.action_container.children) >= 2: - Logger.warning( - "KivyMD: " - "Do not use more than one action button. " - "This is contrary to the material design rules " - "of version 3" - ) - if isinstance(widget, MDLabel): - widget.adaptive_height = True - widget.pos_hint = {"center_y": 0.5} - check_color("white") + if isinstance(widget, (MDSnackbarText, MDSnackbarSupportingText)): self.ids.label_container.add_widget(widget) - if len(self.ids.label_container.children) >= 4: - Logger.warning( - "KivyMD: " - "Do not use more than three lines in the snackbar. " - "This is contrary to the material design rules " - "of version 3" - ) - elif isinstance( - widget, - ( - SnackbarLabelContainer, - SnackbarActionButtonContainer, - SnackbarCloseButtonContainer, - ), - ): + elif isinstance(widget, MDSnackbarButtonContainer): + self.ids.button_container.size_hint_x = ( + 1 if self.orientation == "vertical" else None + ) + self.ids.button_container.add_widget(widget) + else: return super().add_widget(widget) def on_open(self, *args) -> None: - """Called when a snackbar opened.""" + """Fired when a snackbar opened.""" def on_dismiss(self, *args) -> None: - """Called when a snackbar closed.""" + """Fired when a snackbar closed.""" -class Snackbar(MDSnackbar): +class MDSnackbarText(MDLabel): """ - .. deprecated:: 1.2.0 - Use :class:`~kivymd.uix.snackbar.MDSnackbar` - class instead. + The class implements the text. + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - Logger.warning( - "KivyMD: " - "The `Snackbar` class has been deprecated. " - "Use the `MDSnackbar` class instead." - ) + +class MDSnackbarSupportingText(MDLabel): + """ + The class implements the supporting text. + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. + """ diff --git a/kivymd/uix/stacklayout.py b/kivymd/uix/stacklayout.py index c5028ca5b..224008702 100644 --- a/kivymd/uix/stacklayout.py +++ b/kivymd/uix/stacklayout.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size @@ -28,7 +28,7 @@ MDStackLayout: adaptive_height: True - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor Available options are: ---------------------- @@ -89,13 +89,24 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior class MDStackLayout( - DeclarativeBehavior, ThemableBehavior, StackLayout, MDAdaptiveWidget + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + StackLayout, + MDAdaptiveWidget, ): """ - Stack layout class. For more information, see in the - :class:`~kivy.uix.stacklayout.StackLayout` class documentation. + Stack layout class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivy.uix.stacklayout.StackLayout` and + :class:`~kivymd.uix.MDAdaptiveWidget` + classes documentation. """ diff --git a/kivymd/uix/tab/__init__.py b/kivymd/uix/tab/__init__.py index c5111982c..4454a781f 100644 --- a/kivymd/uix/tab/__init__.py +++ b/kivymd/uix/tab/__init__.py @@ -1 +1,6 @@ -from .tab import MDTabs, MDTabsBase, MDTabsLabel # NOQA F401 +from .tab import ( + MDTabs, + MDTabsItem, + MDTabsItemIcon, + MDTabsItemLabel, +) # NOQA F401 diff --git a/kivymd/uix/tab/tab.kv b/kivymd/uix/tab/tab.kv index f219bc1e7..0b1abd589 100644 --- a/kivymd/uix/tab/tab.kv +++ b/kivymd/uix/tab/tab.kv @@ -1,128 +1,47 @@ -#:import DampedScrollEffect kivy.effects.dampedscroll.DampedScrollEffect - - - - _set_start_tab: False - size_hint: None, 1 - halign: "center" - valign: "center" - group: "tabs" - font: root.font_name - allow_no_selection: False - markup: True - on_width: - if not self._set_start_tab: \ - self.tab_bar.parent._update_indicator( \ - self.tab_bar.parent.carousel.current_slide.tab_label); \ - self._set_start_tab = True - on_tab_bar: - self.text_size = (None, None) \ - if self.tab_bar.parent.allow_stretch else (self.width, None) - on_ref_press: - self.tab_bar.parent.dispatch( \ - "on_ref_press", - self, \ - self.tab, \ - self.tab_bar, \ - self.tab_bar.parent.carousel) - color: - ( \ - self.text_color_active \ - if self.text_color_active else self.specific_secondary_text_color \ - ) \ - if self.state == "down" else \ - ( \ - self.text_color_normal \ - if self.text_color_normal else self.theme_cls.text_color \ - ) - + + size_hint_y: None - - size_hint: 1, 1 - do_scroll_y: False - bar_color: 0, 0, 0, 0 - bar_inactive_color: 0, 0, 0, 0 - bar_width: 0 - effect_cls: DampedScrollEffect + canvas.before: + Color: + rgba: + self.md_bg_color \ + if self.md_bg_color and self.theme_bg_color == "Custom" else \ + self.theme_cls.surfaceColor + Rectangle: + pos: self.pos + size: self.size + MDTabsScrollView: + id: tab_scroll + bar_width: 0 - - carousel: carousel - tab_bar: tab_bar - anchor_y: "top" - background_palette: "Primary" + GridLayout: + id: container + rows: 1 + size_hint: None, None + width: self.minimum_width + height: root.height - _line_x: 0 - _line_width: 0 - _line_height: 0 - _line_radius: 0 - on_size: - root._update_padding(layout) + + pos_hint: {"center_x": .5} + theme_icon_color: "Custom" + icon_color: self.theme_cls.onSurfaceVariantColor - MDTabsMain: - padding: 0, tab_bar.height, 0, 0 - MDTabsCarousel: - id: carousel - lock_swiping: root.lock_swiping - ignore_perpendicular_swipes: True - anim_move_duration: root.anim_duration - on_index: root.on_carousel_index(*args) - on__offset: tab_bar.android_animation(*args) + + adaptive_size: True + pos_hint: {"center_x": .5} + padding_x: "36dp" + theme_text_color: "Custom" + text_color: self.theme_cls.onSurfaceVariantColor - MDTabsBar: - id: tab_bar - padding: root.tab_padding - carousel: carousel - scrollview: scrollview - layout: layout - size_hint: 1, None - elevation: root.elevation - radius: root.radius - shadow_offset: root.shadow_offset - shadow_color: root.shadow_color - shadow_softness: root.shadow_softness - height: root.tab_bar_height - md_bg_color: - self.theme_cls.primary_color \ - if not root.background_color else \ - root.background_color - MDTabsScrollView: - id: scrollview - do_scroll_x: False if layout.width <= self.width else True + + orientation: "vertical" + size_hint: None, None + height: self.minimum_height + spacing: "4dp" + padding: 0, "8dp", 0, "8dp" - MDGridLayout: - id: layout - rows: 1 - size_hint_y: 1 - adaptive_width: True - on_size: root._update_padding(layout) - canvas.before: - Color: - rgba: root.underline_color - Line: - width: dp(2) - rectangle: [0, 0, layout.width, dp(2)] - Color: - rgba: - root.theme_cls.accent_color \ - if not root.indicator_color else \ - root.indicator_color - RoundedRectangle: - group: "Indicator_line" - pos: self.pos - size: 0, root.tab_indicator_height - radius: [0,] - Line: - width: dp(2) - rounded_rectangle: - [ \ - root._line_x, \ - self.pos[1], \ - root._line_width, \ - root._line_height, \ - root._line_radius \ - ] diff --git a/kivymd/uix/tab/tab.py b/kivymd/uix/tab/tab.py index 4e97befd1..7af769291 100755 --- a/kivymd/uix/tab/tab.py +++ b/kivymd/uix/tab/tab.py @@ -920,343 +920,44 @@ def switch_tab_by_name(self, *args): :align: center """ -__all__ = ("MDTabs", "MDTabsBase") +from __future__ import annotations + +__all__ = ("MDTabs", "MDTabsItem", "MDTabsItemIcon", "MDTabsItemLabel") import os -from typing import Union +from kivy.utils import boundary +from kivy.animation import Animation from kivy.clock import Clock -from kivy.graphics.texture import Texture from kivy.lang import Builder -from kivy.logger import Logger from kivy.metrics import dp from kivy.properties import ( - AliasProperty, - BooleanProperty, - BoundedNumericProperty, - ColorProperty, - ListProperty, - NumericProperty, ObjectProperty, + BooleanProperty, OptionProperty, - StringProperty, + ColorProperty, + AliasProperty, ) -from kivy.uix.anchorlayout import AnchorLayout -from kivy.uix.behaviors import ToggleButtonBehavior +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout from kivy.uix.scrollview import ScrollView -from kivy.utils import boundary from kivymd import uix_path -from kivymd.font_definitions import fonts, theme_font_styles -from kivymd.icon_definitions import md_icons -from kivymd.theming import ThemableBehavior, ThemeManager +from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import ( DeclarativeBehavior, RectangularRippleBehavior, - SpecificBackgroundColorBehavior, + CommonElevationBehavior, ) -from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.card import MDCard -from kivymd.uix.carousel import MDCarousel -from kivymd.uix.label import MDLabel +from kivymd.uix.behaviors.focus_behavior import FocusBehavior +from kivymd.uix.label import MDLabel, MDIcon with open(os.path.join(uix_path, "tab", "tab.kv"), encoding="utf-8") as kv_file: Builder.load_string(kv_file.read()) -class MDTabsException(Exception): - pass - - -class MDTabsLabel(ToggleButtonBehavior, RectangularRippleBehavior, MDLabel): - """This class it represent the label of each tab.""" - - text_color_normal = ColorProperty(None) - text_color_active = ColorProperty(None) - tab = ObjectProperty() - tab_bar = ObjectProperty() - font_name = StringProperty("Roboto") - - def __init__(self, **kwargs): - self.split_str = " ,-" - super().__init__(**kwargs) - self.max_lines = 2 - self.size_hint_x = None - self.size_hint_min_x = dp(90) - self.min_space = dp(98) - self.bind( - text=self._update_text_size, - ) - - def on_release(self) -> None: - try: - self.tab_bar.parent.dispatch( - "on_tab_switch", self.tab, self, self.text - ) - # If the label is selected load the relative tab from carousel. - if self.state == "down": - self.tab_bar.parent.carousel.load_slide(self.tab) - except KeyError: - pass - - def on_texture(self, instance_tabs_label, texture: Texture) -> None: - # Just save the minimum width of the label based of the content. - if texture: - max_width = dp(360) - min_width = dp(90) - if texture.width > max_width: - self.width = max_width - self.text_size = (max_width, None) - elif texture.width < min_width: - self.width = min_width - else: - self.width = texture.width - - def _update_text_size(self, *args): - if not self.tab_bar: - return - if self.tab_bar.parent.allow_stretch is True: - self.text_size = (None, None) - else: - self.width = self.tab_bar.parent.fixed_tab_label_width - self.text_size = (self.width, None) - Clock.schedule_once(self.tab_bar._label_request_indicator_update, 0) - - -class MDTabsBase: - """ - This class allow you to create a tab. - You must create a new class that inherits from MDTabsBase. - In this way you have total control over the views of your tabbed panel. - """ - - icon = StringProperty() - """ - This property will set the Tab's Label Icon. - - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"]) - """ - This property sets the mode in wich the tab's title and icon are shown. - - :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'Lead'`. - """ - - title = StringProperty() - """ - This property will set the Name of the tab. - - .. note:: - As a side note. - - All tabs have set `markup = True`. - Thanks to this, you can use the kivy markup language to set a colorful - and fully customizable tabs titles. - - .. warning:: - The material design requires that every title label is written in - capital letters, because of this, the `string.upper()` will be applied - to it's contents. - - :attr:`title` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - title_is_capital = BooleanProperty(False) - """ - This value controls wether if the title property should be converted to - capital letters. - - :attr:`title_is_capital` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. - """ - - tab_label_text = StringProperty() - """ - This property is the actual title's Label of the tab. - use the property :attr:`icon` and :attr:`title` to set this property - correctly. - - This property is kept public for specific and backward compatibility - purposes. - - :attr:`tab_label_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - tab_label = ObjectProperty() - """ - It is the label object reference of the tab. - - :attr:`tab_label` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - def _get_label_font_style(self): - if self.tab_label: - return self.tab_label.font_style - - def _set_label_font_style(self, value): - if self.tab_label: - if value in theme_font_styles: - self.tab_label.font_style = value - else: - raise ValueError( - "tab_label_font_style:\n\t" - "font_style not found in theme_font_styles\n\t" - f"font_style = {value}" - ) - else: - Clock.schedule_once(lambda x: self._set_label_font_style(value)) - return True - - tab_label_font_style = AliasProperty( - _get_label_font_style, - _set_label_font_style, - cache=True, - ) - """ - :attr:`tab_label_font_style` is an :class:`~kivy.properties.AliasProperty` - that behavies similar to an :class:`~kivy.properties.OptionProperty`. - - This property's behavior allows the developer to use any new label style - registered to the app. - - This property will affect the Tab's Title Label widget. - """ - - def __init__(self, *args, **kwargs): - self.tab_label = MDTabsLabel(tab=self) - super().__init__(*args, **kwargs) - self.bind( - icon=self._update_text, - title=self._update_text, - title_icon_mode=self._update_text, - tab_label_text=self.update_label_text, - title_is_capital=self.update_label_text, - ) - Clock.schedule_once( - self._update_text - ) # this will ensure the text is correct - - def _update_text(self, *args): - # Ensures that the title is in capital letters. - if self.title and self.title_is_capital is True: - if self.title != self.title.upper(): - self.title = self.title.upper() - # Avoids event recursion. - return - # Add the icon. - if self.icon and self.icon in md_icons: - self.tab_label_text = f"[size=24sp][font={fonts[-1]['fn_regular']}]{md_icons[self.icon]}[/size][/font]" - if self.title: - self.tab_label_text = ( - self.tab_label_text - + (" " if self.title_icon_mode == "Lead" else "\n") - + self.title - ) - # Add the title. - else: - if self.icon: - Logger.error( - f"{self}: [UID] = [{self.uid}]:\n\t" - f"Icon '{self.icon}' not found in md_icons" - ) - if self.title: - self.tab_label_text = self.title - else: - if not self.tab_label_text: - raise ValueError( - f"{self}: [UID] = [{self.uid}]:\n\t" - "No valid Icon was found.\n\t" - "No valid Title was found.\n\t" - f"Icon\t= '{self.icon}'\n\t" - f"Title\t= '{self.title}'\n\t" - ) - - self.tab_label.padding = dp(16), 0 - self.update_label_text(None, self.tab_label_text) - - def update_label_text(self, instance_user_tab, text_tab: str) -> None: - self.tab_label.text = text_tab - - -class MDTabsMain(MDBoxLayout): - """ - This class is just a boxlayout that contain the carousel. - It allows you to have control over the carousel. - """ - - -class MDTabsCarousel(MDCarousel): - lock_swiping = BooleanProperty(False) - """ - If True - disable switching tabs by swipe. - - :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - def on_touch_move(self, touch): - if self.lock_swiping: # lock a swiping - return - if not self.touch_mode_change: - if self.ignore_perpendicular_swipes and self.direction in ( - "top", - "bottom", - ): - if abs(touch.oy - touch.y) < self.scroll_distance: - if abs(touch.ox - touch.x) > self.scroll_distance: - self._change_touch_mode() - self.touch_mode_change = True - elif self.ignore_perpendicular_swipes and self.direction in ( - "right", - "left", - ): - if abs(touch.ox - touch.x) < self.scroll_distance: - if abs(touch.oy - touch.y) > self.scroll_distance: - self._change_touch_mode() - self.touch_mode_change = True - - if self._get_uid("cavoid") in touch.ud: - return - if self._touch is not touch: - super().on_touch_move(touch) - return self._get_uid() in touch.ud - if touch.grab_current is not self: - return True - - ud = touch.ud[self._get_uid()] - direction = self.direction[0] - - if ud["mode"] == "unknown": - if direction in "rl": - distance = abs(touch.ox - touch.x) - else: - distance = abs(touch.oy - touch.y) - if distance > self.scroll_distance: - ev = self._change_touch_mode_ev - if ev is not None: - ev.cancel() - ud["mode"] = "scroll" - else: - if direction in "rl": - self._offset += touch.dx - if direction in "tb": - self._offset += touch.dy - return True - - class MDTabsScrollView(ScrollView): - """This class hacked version to fix scroll_x manual setting.""" - - def goto( - self, scroll_x: Union[float, None], scroll_y: Union[float, None] - ) -> None: + def goto(self, scroll_x: float | None, scroll_y: float | None) -> None: """Update event value along with scroll_*.""" def _update(e, x): @@ -1264,7 +965,8 @@ def _update(e, x): e.value = (e.max + e.min) * x if not (scroll_x is None): - self.scroll_x = scroll_x + Animation(scroll_x=scroll_x, d=0.2).start(self) + # self.scroll_x = scroll_x _update(self.effect_x, scroll_x) if not (scroll_y is None): @@ -1272,709 +974,224 @@ def _update(e, x): _update(self.effect_y, scroll_y) -class MDTabsBar(MDCard): - """ - This class is just a boxlayout that contains the scroll view for tabs. - It is also responsible for resizing the tab shortcut when necessary. +class MDTabsItemLabel(MDLabel): """ + Implements an label for the :class:`~MDTabsItem` class. + + For more information, see in the + :class:`~kivymd.uix.label.label.MDLabel` class documentation. - target = ObjectProperty(None, allownone=True) + .. versionchanged:: 2.0.0 """ - It is the carousel reference of the next tab / slide. - When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the - target tab / slide of the carousel. - :attr:`target` is an :class:`~kivy.properties.ObjectProperty` - and default to `None`. + _active = BooleanProperty(False) + + +class MDTabsItemIcon(MDIcon): """ + Implements an icon for the :class:`~MDTabsItem` class. - def get_rect_instruction(self): - canvas_instructions = self.layout.canvas.before.get_group( - "Indicator_line" - ) - return canvas_instructions[0] + For more information, see in the + :class:`~kivymd.uix.label.label.MDIcon` class documentation. - indicator = AliasProperty(get_rect_instruction, cache=True) + .. versionchanged:: 2.0.0 """ - It is the :class:`~kivy.graphics.vertex_instructions.RoundedRectangle` - instruction reference of the tab indicator. - :attr:`indicator` is an :class:`~kivy.properties.AliasProperty`. + +class MDTabsItem( + DeclarativeBehavior, + ThemableBehavior, + RectangularRippleBehavior, + FocusBehavior, + ButtonBehavior, + BoxLayout, +): """ + Implements a menu item with an icon and text. + + .. versionchanged:: 2.0.0 - def get_last_scroll_x(self): - return self.scrollview.scroll_x + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.behaviors.ripple_behavior.RectangularRippleBehavior` and + :class:`~kivymd.uix.behaviors.behaviors.focus_behavior.FocusBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. + """ - last_scroll_x = AliasProperty( - get_last_scroll_x, bind=("target",), cache=True - ) + active = BooleanProperty(False) """ - Is the carousel reference of the next tab/slide. - When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the - target tab/slide of the carousel. + Is the tab active. - :attr:`last_scroll_x` is an :class:`~kivy.properties.AliasProperty`. + :attr:`active` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def update_indicator( - self, x: Union[float, int], w: Union[float, int], radius=None - ) -> None: - # Update position and size of the indicator. - if self.parent.tab_indicator_type == "line-round": - self.parent._line_x = x - self.parent._line_width = w - self.parent._line_height = self.parent.tab_indicator_height - self.parent._line_radius = self.parent.tab_indicator_height / 2 - elif self.parent.tab_indicator_type == "line-rect": - self.parent._line_x = x - self.parent._line_width = w - self.parent._line_height = self.parent.tab_indicator_height - else: - self.indicator.pos = (x, 0) - self.indicator.size = (w, self.parent.tab_indicator_height) - if radius: - self.indicator.radius = radius + _tabs = ObjectProperty() + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, (MDTabsItemLabel, MDTabsItemIcon)): + if len(self.children) <= 1: + Clock.schedule_once(lambda x: self._set_width(widget)) + + def on_release(self): + print(self._tabs.ids.container.width) + # self._tabs.ids.tab_scroll.goto(x, None) - def tab_bar_autoscroll(self, instance_tab_label: MDTabsLabel, step: float): # Automatic scroll animation of the tab bar. - bound_left = self.center_x - self.x - bound_right = self.layout.width - bound_left - dt = instance_tab_label.center_x - bound_left - sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0) - lsx = self.last_scroll_x # ast scroll x of the tab bar + bound_left = self._tabs.center_x - self._tabs.x + bound_right = self._tabs.ids.container.width - bound_left + dt = self.center_x - bound_left + sx, sy = self._tabs.ids.tab_scroll.convert_distance_to_scroll(dt, 0) + lsx = self._tabs.ids.tab_scroll.scroll_x # ast scroll x of the tab bar scroll_is_late = lsx < sx # determine scroll direction - dst = abs(lsx - sx) * step # distance to run + dst = abs(lsx - sx) * 0.5 # distance to run if not dst: return - if scroll_is_late and instance_tab_label.center_x > bound_left: + if scroll_is_late and self.center_x > bound_left: x = lsx + dst - elif not scroll_is_late and instance_tab_label.center_x < bound_right: + elif not scroll_is_late and self.center_x < bound_right: x = lsx - dst else: return x = boundary(x, 0.0, 1.0) - self.scrollview.goto(x, None) - - def android_animation( - self, instance_carousel: MDTabsCarousel, offset: Union[float, int] - ): - # Try to reproduce the android animation effect. - if offset != 0 and abs(offset) < instance_carousel.width: - forward = offset < 0 - offset = abs(offset) - step = offset / float(instance_carousel.width) - indicator_animation = self.parent.tab_indicator_anim - - skip_slide = ( - instance_carousel.slides[instance_carousel._skip_slide] - if instance_carousel._skip_slide is not None - else None - ) - next_slide = ( - instance_carousel.next_slide - if forward - else instance_carousel.previous_slide - ) - self.target = skip_slide if skip_slide else next_slide - - if not self.target: - return - - a = instance_carousel.current_slide.tab_label - b = self.target.tab_label - self.tab_bar_autoscroll(b, step) - - # Avoids the animation if `indicator_animation` is True. - if indicator_animation is False: - return - gap_x = abs((a.x) - (b.x)) - gap_w = (b.width) - (a.width) - if forward: - x_step = a.x + (gap_x * step) - else: - x_step = a.x - gap_x * step - w_step = a.width + (gap_w * step) - self.update_indicator(x_step, w_step) + self._tabs.ids.tab_scroll.goto(x, None) + + def _set_width(self, widget): + def set_width(*args): + self.width = widget.texture_size[0] + widget.padding_x + 2 - def _label_request_indicator_update(self, *args): - widget = self.carousel.current_slide.tab_label - self.update_indicator(widget.x, widget.width) + if not self._tabs.allow_stretch and isinstance(widget, MDTabsItemLabel): + Clock.schedule_once(set_width) + + super().add_widget(widget) class MDTabs( DeclarativeBehavior, ThemableBehavior, - SpecificBackgroundColorBehavior, - AnchorLayout, + CommonElevationBehavior, + BoxLayout, ): """ Tabs class. You can use this class to create your own tabbed panel. For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and - :class:`~kivy.uix.anchorlayout.AnchorLayout` + :class:`~kivymd.uix.behaviors.elevation.CommonElevationBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation. :Events: `on_tab_switch` Called when switching tabs. - `on_slide_progress` - Called while the slide is scrolling. - `on_ref_press` - The method will be called when the ``on_ref_press`` event - occurs when you, for example, use markup text for tabs. - """ - - tab_bar_height = NumericProperty("48dp") """ - Height of the tab bar. - :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty` - and defaults to `'48dp'`. + md_bg_color = ColorProperty(None) """ + The background color of the widget. - tab_padding = ListProperty([0, 0, 0, 0]) - """ - Padding of the tab bar. - - :attr:`tab_padding` is an :class:`~kivy.properties.ListProperty` - and defaults to `[0, 0, 0, 0]`. - """ - - tab_indicator_anim = BooleanProperty(False) - """ - Tab indicator animation. If you want use animation set it to ``True``. - - :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - tab_indicator_height = NumericProperty("2dp") - """ - Height of the tab indicator. - - :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty` - and defaults to `'2dp'`. - """ - - tab_indicator_type = OptionProperty( - "line", options=["line", "fill", "round", "line-round", "line-rect"] - ) - """ - Type of tab indicator. Available options are: `'line'`, `'fill'`, - `'round'`, `'line-rect'` and `'line-round'`. - - :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'line'`. - """ - - tab_hint_x = BooleanProperty(False) - """ - This option affects the size of each child. if it's `True`, the size of - each tab will be ignored and will use the size available by the container. - - :attr:`tab_hint_x` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - anim_duration = NumericProperty(0.2) - """ - Duration of the slide animation. - - :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - anim_threshold = BoundedNumericProperty( - 0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0 - ) - """ - Animation threshold allow you to change the tab indicator animation effect. - - :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty` - and defaults to `0.8`. - """ - - allow_stretch = BooleanProperty(True) - """ - If `True`, the tab will update dynamically (if :attr:`tab_hint_x` is `True`) - to it's content width, and wrap any text if the widget is wider than `"360dp"`. - - If `False`, the tab won't update to it's maximum texture width. - this means that the `fixed_tab_label_width` will be used as the label - width. this will wrap any text inside to fit the fixed value. - - :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. - """ - - fixed_tab_label_width = NumericProperty("140dp") - """ - If :attr:`allow_stretch` is `False`, the class will set this value as the - width to all the tabs title label. - - :attr:`fixed_tab_label_width` is an :class:`~kivy.properties.NumericProperty` - and defaults to `140dp`. - """ - - background_color = ColorProperty(None) - """ - Background color of tabs in (r, g, b, a) or string format. - - :attr:`background_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - underline_color = ColorProperty([0, 0, 0, 0]) - """ - Underline color of tabs in (r, g, b, a) or string format. - - :attr:`underline_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. - """ - - text_color_normal = ColorProperty(None) - """ - 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 `None`. - """ - - text_color_active = ColorProperty(None) - """ - 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` + :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - shadow_softness = NumericProperty(12) - """ - See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.shadow_softness` - attribute. - - .. versionadded:: 1.1.0 - - :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty` - and defaults to `12`. - """ - - shadow_color = ColorProperty([0, 0, 0, 0.6]) + type = OptionProperty("primary", options=["primary", "secondary"]) """ - See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.shadow_color` - attribute. + Panel type. + Available options are: `'primary'`, `'secondary'`. - .. versionadded:: 1.1.0 + .. versionchanged:: 2.0.0 - :attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0.6]`. + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'primary'`. """ - shadow_offset = ListProperty((0, 0)) + label_only = BooleanProperty(False) """ - See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.shadow_offset` - attribute. + Tabs with a label only or with an icon and a label. - .. versionadded:: 1.1.0 - - :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty` - and defaults to `[0, 0]`. - """ + .. versionchanged:: 2.0.0 - elevation = NumericProperty(0) - """ - See :attr:`kivymd.uix.behaviors.CommonElevationBehavior.elevation` - attribute. - - :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` - and defaults to `0`. - """ - - indicator_color = ColorProperty(None) - """ - Color indicator in (r, g, b, a) or string format. - - :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - lock_swiping = BooleanProperty(False) - """ - If True - disable switching tabs by swipe. - - :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` + :attr:`label_only` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ - font_name = StringProperty("Roboto") - """ - Font name for tab text. - - :attr:`font_name` is an :class:`~kivy.properties.StringProperty` - and defaults to `'Roboto'`. + allow_stretch = BooleanProperty(False) """ + Whether to stretch tabs to the width of the panel. - ripple_duration = NumericProperty(2) - """ - Ripple duration when long touching to tab. - - :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty` - and defaults to `2`. - """ - - no_ripple_effect = BooleanProperty(True) - """ - Whether to use the ripple effect when tapping on a tab. - - :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. - """ - - title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"]) - """ - This property sets the mode in wich the tab's title and icon are shown. - - :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'Lead'`. - """ - - force_title_icon_mode = BooleanProperty(True) - """ - If this property is se to `True`, it will force the class to update every - tab inside the scroll view to the current `title_icon_mode` - - :attr:`force_title_icon_mode` is an :class:`~kivy.properties.BooleanProperty` + :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.register_event_type("on_tab_switch") - self.register_event_type("on_ref_press") - self.register_event_type("on_slide_progress") - Clock.schedule_once(self._carousel_bind, 1) - self.theme_cls.bind( - primary_palette=self.update_icon_color, - theme_style=self.update_icon_color, - ) - self.bind( - force_title_icon_mode=self._parse_icon_mode, - title_icon_mode=self._parse_icon_mode, - ) - self.bind(tab_hint_x=self._update_tab_hint_x) - - def update_icon_color( - self, - instance_theme_manager: ThemeManager, - name_theme_style_name_palette: str, - ) -> None: - """ - Called when the app's color scheme or style has changed - (dark theme/light theme). - """ - - for tab_label in self.get_tab_list(): - if not self.text_color_normal: - tab_label.text_color_normal = self.theme_cls.text_color - if not self.text_color_active: - tab_label.text_color_active = self.specific_secondary_text_color - - def switch_tab(self, name_tab: Union[MDTabsLabel, str], search_by="text"): - """ - This method switch between tabs - name_tab can be either a String or a :class:`~MDTabsBase`. - - `search_by` will look up through the properties of every tab. - - If the value doesnt match, it will raise a ValueError. - - Search_by options: - text : will search by the raw text of the label (`tab_label_text`) - icon : will search by the `icon` property - title : will search by the `title` property - """ - - if isinstance(name_tab, str): - if search_by == "title": - for tab_instance in self.tab_bar.parent.carousel.slides: - if tab_instance.title_is_capital is True: - _name_tab = name_tab.upper() - else: - _name_tab = name_tab - if tab_instance.title == _name_tab: - self.carousel.load_slide(tab_instance) - return - # Search by icon. - elif search_by == "icon": - for tab_instance in self.tab_bar.parent.carousel.slides: - if tab_instance.icon == name_tab: - self.carousel.load_slide(tab_instance) - return - # Search by title. - else: - for tab_instance in self.tab_bar.parent.carousel.slides: - if tab_instance.tab_label_text == name_tab: - self.carousel.load_slide(tab_instance) - return - raise ValueError( - "switch_tab:\n\t" - "name_tab not found in the tab list\n\t" - f"search_by = {repr(search_by)} \n\t" - f"name_tab = {repr(name_tab)} \n\t" - ) + Clock.schedule_once(self._check_panel_height) + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDTabsItem): + widget._tabs = self + widget.bind(on_release=self.set_active_item) + self.ids.container.add_widget(widget) else: - self.carousel.load_slide(name_tab.tab) - - def get_tab_list(self) -> list: - """Returns a list of :class:`~MDTabsLabel` objects.""" - - return self.tab_bar.layout.children[::-1] - - def get_slides(self) -> list: - """Returns a list of user tab objects.""" - - return self.carousel.slides - - def get_current_tab(self): - """ - Returns current tab object. - - .. versionadded:: 1.0.0 - """ - - return self.carousel.current_slide - - def add_widget(self, widget, index=0, canvas=None): - # You can add only subclass of MDTabsBase. - if not isinstance(widget, (MDTabsBase, MDTabsMain, MDTabsBar)): - raise ValueError( - f"MDTabs[{self.uid}].add_widget:\n\t" - "The widget provided is not a subclass of MDTabsBase." - ) - if len(self.children) >= 2: - try: - # FIXME: Can't set the value of the `no_ripple_effect` - # and `ripple_duration` properties for widget.tab_label. - widget.tab_label._no_ripple_effect = self.no_ripple_effect - widget.tab_label.ripple_duration_in_slow = self.ripple_duration - widget.tab_label.group = str(self) - widget.tab_label.tab_bar = self.tab_bar - widget.tab_label.font_name = self.font_name - widget.tab_label.text_color_normal = ( - self.text_color_normal - if self.text_color_normal - else self.specific_secondary_text_color - ) - widget.tab_label.text_color_active = ( - self.text_color_active - if self.text_color_active - else self.specific_text_color - ) - self.bind( - allow_stretch=widget.tab_label._update_text_size, - fixed_tab_label_width=widget.tab_label._update_text_size, - font_name=widget.tab_label.setter("font_name"), - text_color_active=widget.tab_label.setter( - "text_color_active" - ), - text_color_normal=widget.tab_label.setter( - "text_color_normal" - ), - ) - Clock.schedule_once(widget.tab_label._update_text_size, 0) - self.tab_bar.layout.add_widget(widget.tab_label) - self.carousel.add_widget(widget) - if self.force_title_icon_mode is True: - widget.title_icon_mode = self.title_icon_mode - Clock.schedule_once( - self.tab_bar._label_request_indicator_update, 0 - ) - return - except AttributeError: - pass - if isinstance(widget, (MDTabsMain, MDTabsBar)): return super().add_widget(widget) - def remove_widget(self, widget): - # You can remove only subclass of MDTabsLabel or MDTabsBase. - if not issubclass(widget.__class__, (MDTabsLabel, MDTabsBase)): - raise MDTabsException( - "MDTabs can remove only subclass of MDTabsLabel or MDTabsBase" - ) - # If the widget is an instance of MDTabsBase, then the widget is - # set as the widget's tab_label object. - if issubclass(widget.__class__, MDTabsBase): - slide = widget - title_label = widget.tab_label - else: - # We already got the label, so we set the slide reference. - slide = widget.tab - title_label = widget - # Set memory. - # Search object next tab. - # Clean all bindings to allow the widget to be collected. - self.unbind( - allow_stretch=title_label._update_text_size, - fixed_tab_label_width=title_label._update_text_size, - font_name=title_label.setter("font_name"), - text_color_active=title_label.setter("text_color_active"), - text_color_normal=title_label.setter("text_color_normal"), - ) - self.carousel.remove_widget(slide) - self.tab_bar.layout.remove_widget(title_label) - # Clean the references. - slide = None - title_label = None - widget = None - - def on_slide_progress(self, *args) -> None: - """ - This event is deployed every available frame while the tab is scrolling. - """ - - def on_carousel_index(self, instance_tabs_carousel, index: int) -> None: - """ - Called when the Tab index have changed. - - This event is deployed by the built in carousel of the class. - """ - - # When the index of the carousel change, update tab indicator, - # select the current tab and reset threshold data. - if instance_tabs_carousel.current_slide: - current_tab_label = instance_tabs_carousel.current_slide.tab_label - if current_tab_label.state == "normal": - # current_tab_label._do_press() - current_tab_label.dispatch("on_release") - current_tab_label._release_group(self) - current_tab_label.state = "down" - - if self.tab_indicator_type == "round": - self.tab_indicator_height = self.tab_bar_height - if index == 0: - radius = [ - 0, - self.tab_bar_height / 2, - self.tab_bar_height / 2, - 0, - ] - self.tab_bar.update_indicator( - current_tab_label.x, current_tab_label.width, radius - ) - elif index == len(self.get_tab_list()) - 1: - radius = [ - self.tab_bar_height / 2, - 0, - 0, - self.tab_bar_height / 2, - ] - self.tab_bar.update_indicator( - current_tab_label.x, current_tab_label.width, radius - ) - else: - radius = [ - self.tab_bar_height / 2, - ] - self.tab_bar.update_indicator( - current_tab_label.x, current_tab_label.width, radius - ) - elif ( - self.tab_indicator_type == "fill" - or self.tab_indicator_type == "line-round" - or self.tab_indicator_type == "line-rect" - ): - self.tab_indicator_height = self.tab_bar_height - self.tab_bar.update_indicator( - current_tab_label.x, current_tab_label.width - ) + def set_active_item(self, item: MDTabsItem) -> None: + """Sets the active tab item.""" + + for widget in self.ids.container.children: + if item is widget: + widget.active = not widget.active + + for widget_item in item.children: + if isinstance(widget_item, MDTabsItemLabel): + widget_item._active = widget.active + Animation( + text_color=self.theme_cls.primaryColor + if widget.active + else self.theme_cls.onSurfaceVariantColor, + d=0.2, + ).start(widget_item) + if isinstance(widget_item, MDTabsItemIcon): + widget_item._active = widget.active + Animation( + icon_color=self.theme_cls.primaryColor + if widget.active + else self.theme_cls.onSurfaceVariantColor, + d=0.2, + ).start(widget_item) else: - self.tab_bar.update_indicator( - current_tab_label.x, current_tab_label.width - ) - - def on_ref_press(self, *args) -> None: - """ - This event will be launched every time the user press a markup enabled - label with a link or reference inside. - """ - - def on_tab_switch(self, *args) -> None: - """This event is launched every time the current tab is changed.""" - - def on_size(self, instance_tab, size: list) -> None: - """Called when the application screen is resized.""" - - if self.carousel.current_slide: - self._update_indicator(self.carousel.current_slide.tab_label) - - def _update_tab_hint_x(self, *args): - if not self.ids.layout.children: - return - if self.tab_hint_x is True: - self.fixed_tab_label_width = self.width // len( - self.ids.layout.children - ) - self.allow_stretch = False + widget.active = False + for widget_item in widget.children: + widget_item._active = widget.active + if isinstance(widget_item, MDTabsItemLabel): + Animation( + text_color=self.theme_cls.onSurfaceVariantColor, + d=0.2, + ).start(widget_item) + if isinstance(widget_item, MDTabsItemIcon): + Animation( + icon_color=self.theme_cls.onSurfaceVariantColor, + d=0.2, + ).start(widget_item) + + def on_size(self, instance, size) -> None: + """Fired when the application screen size changes.""" + + width, height = size + number_tabs = len(self.ids.container.children) + + if self.allow_stretch: + for tab in self.ids.container.children: + tab.width = width / number_tabs + + def _check_panel_height(self, *args): + if self.label_only: + self.height = dp(48) else: - self.allow_stretch = True - - def _parse_icon_mode(self, *args): - if self.force_title_icon_mode is True: - for slide in self.carousel.slides: - slide.title_icon_mode = self.title_icon_mode - if self.title_icon_mode == "Top": - self.tab_bar_height = dp(72) - else: - self.tab_bar_height = dp(48) - - def _carousel_bind(self, interval): - self.carousel.bind(on_slide_progress=self._on_slide_progress) - - def _on_slide_progress(self, *args): - self.dispatch("on_slide_progress", args) - - def _update_indicator(self, current_tab_label): - def update_indicator(interval): - self.tab_bar.update_indicator( - current_tab_label.x, current_tab_label.width - ) - - if not current_tab_label: - current_tab_label = self.tab_bar.layout.children[-1] - Clock.schedule_once(update_indicator) - - def _update_padding(self, layout, *args): - if self.tab_hint_x is True: - layout.padding = [0, 0] - Clock.schedule_once(self._update_tab_hint_x) - return True - padding = [0, 0] - # FIXME: It's not entirely clear why the `padding = [dp (52), 0]` - # instruction is needed? This creates an extra 52px left padding and - # looks like a bug. This instruction was added by the contributors in - # previous commits and I have not yet figured out why this was done. - # This is more efficient than to use sum([layout.children]). - # width = layout.width - (layout.padding[0] * 2) - # Forces the padding of the tab_bar when the tab_bar is scrollable. - # if width > self.width: - # padding = [dp(52), 0] - # Set the new padding. - layout.padding = padding - # Update the indicator. - if self.carousel.current_slide: - self._update_indicator(self.carousel.current_slide.tab_label) - Clock.schedule_once( - lambda x: setattr( - self.carousel.current_slide.tab_label, "state", "down" - ), - -1, - ) - return True + self.height = dp(64) diff --git a/kivymd/uix/taptargetview.py b/kivymd/uix/taptargetview.py deleted file mode 100644 index 735de0d08..000000000 --- a/kivymd/uix/taptargetview.py +++ /dev/null @@ -1,857 +0,0 @@ -""" -Components/TapTargetView -======================== - -.. seealso:: - - `TapTargetView, GitHub `_ - - `TapTargetView, Material archive `_ - -.. rubric:: Provide value and improve engagement by introducing users to new - features and functionality at relevant moments. - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-previous.gif - :align: center - -Usage ------ - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.taptargetview import MDTapTargetView - - KV = ''' - Screen: - - MDFloatingActionButton: - id: button - icon: "plus" - pos: 10, 10 - on_release: app.tap_target_start() - ''' - - - class TapTargetViewDemo(MDApp): - def build(self): - screen = Builder.load_string(KV) - self.tap_target_view = MDTapTargetView( - widget=screen.ids.button, - title_text="This is an add button", - description_text="This is a description of the button", - widget_position="left_bottom", - ) - - return screen - - def tap_target_start(self): - if self.tap_target_view.state == "close": - self.tap_target_view.start() - else: - self.tap_target_view.stop() - - - TapTargetViewDemo().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-usage.gif - :align: center - -Widget position ---------------- - -Sets the position of the widget relative to the floating circle. - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="right", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-right.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="left", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-left.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="top", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-top.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="bottom", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-bottom.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="left_top", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-left_top.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="right_top", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-right_top.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="left_bottom", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-left_bottom.png - :align: center - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="right_bottom", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-right_bottom.png - :align: center - -If you use ``the widget_position = "center"`` parameter then you must -definitely specify the :attr:`~MDTapTargetView.title_position`. - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - widget_position="center", - title_position="left_top", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-center.png - :align: center - -Text options ------------- - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - title_text="Title text", - description_text="Description text", - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-text.png - :align: center - - -You can use the following options to control font size, color, and boldness: - -- :attr:`~MDTapTargetView.title_text_size` -- :attr:`~MDTapTargetView.title_text_color` -- :attr:`~MDTapTargetView.title_text_bold` -- :attr:`~MDTapTargetView.description_text_size` -- :attr:`~MDTapTargetView.description_text_color` -- :attr:`~MDTapTargetView.description_text_bold` - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - title_text="Title text", - title_text_size="36sp", - description_text="Description text", - description_text_color=[1, 0, 0, 1] - ) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-text-option.png - :align: center - -But you can also use markup to set these values. - -.. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - title_text="[size=36]Title text[/size]", - description_text="[color=#ff0000ff]Description text[/color]", - ) - -Events control --------------- - -.. code-block:: python - - self.tap_target_view.bind(on_open=self.on_open, on_close=self.on_close) - -.. code-block:: python - - def on_open(self, instance_tap_target_view): - '''Called at the time of the start of the widget opening animation.''' - - print("Open", instance_tap_target_view) - - def on_close(self, instance_tap_target_view): - '''Called at the time of the start of the widget closed animation.''' - - print("Close", instance_tap_target_view) - -.. Note:: See other parameters in the :class:`~MDTapTargetView` class. -""" - -from kivy.animation import Animation -from kivy.event import EventDispatcher -from kivy.graphics import Color, Ellipse, Rectangle -from kivy.logger import Logger -from kivy.metrics import dp -from kivy.properties import ( - BooleanProperty, - ListProperty, - NumericProperty, - ObjectProperty, - OptionProperty, - StringProperty, -) -from kivy.uix.label import Label - -from kivymd.theming import ThemableBehavior - - -class MDTapTargetView(ThemableBehavior, EventDispatcher): - """Rough try to mimic the working of Android's TapTargetView. - - :Events: - :attr:`on_open` - Called at the time of the start of the widget opening animation. - :attr:`on_close` - Called at the time of the start of the widget closed animation. - """ - - widget = ObjectProperty() - """ - Widget to add ``TapTargetView`` upon. - - :attr:`widget` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - outer_radius = NumericProperty(dp(200)) - """ - Radius for outer circle. - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-outer-radius.png - :align: center - - :attr:`outer_radius` is an :class:`~kivy.properties.NumericProperty` - and defaults to `dp(200)`. - """ - - outer_circle_color = ListProperty() - """ - Color for the outer circle in ``rgb`` format. - - .. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - outer_circle_color=(1, 0, 0) - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-outer-circle-color.png - :align: center - - :attr:`outer_circle_color` is an :class:`~kivy.properties.ListProperty` - and defaults to ``theme_cls.primary_color``. - """ - - outer_circle_alpha = NumericProperty(0.96) - """ - Alpha value for outer circle. - - :attr:`outer_circle_alpha` is an :class:`~kivy.properties.NumericProperty` - and defaults to `0.96`. - """ - - target_radius = NumericProperty(dp(45)) - """ - Radius for target circle. - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-target-radius.png - :align: center - - :attr:`target_radius` is an :class:`~kivy.properties.NumericProperty` - and defaults to `dp(45)`. - """ - - target_circle_color = ListProperty([1, 1, 1]) - """ - Color for target circle in ``rgb`` format. - - .. code-block:: python - - self.tap_target_view = MDTapTargetView( - ... - target_circle_color=(1, 0, 0) - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-target-circle-color.png - :align: center - - :attr:`target_circle_color` is an :class:`~kivy.properties.ListProperty` - and defaults to `[1, 1, 1]`. - """ - - title_text = StringProperty() - """ - Title to be shown on the view. - - :attr:`title_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - title_text_size = NumericProperty(dp(25)) - """ - Text size for title. - - :attr:`title_text_size` is an :class:`~kivy.properties.NumericProperty` - and defaults to `dp(25)`. - """ - - title_text_color = ListProperty([1, 1, 1, 1]) - """ - Text color for title. - - :attr:`title_text_color` is an :class:`~kivy.properties.ListProperty` - and defaults to `[1, 1, 1, 1]`. - """ - - title_text_bold = BooleanProperty(True) - """ - Whether title should be bold. - - :attr:`title_text_bold` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. - """ - - description_text = StringProperty() - """ - Description to be shown below the title (keep it short). - - :attr:`description_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - description_text_size = NumericProperty(dp(20)) - """ - Text size for description text. - - :attr:`description_text_size` is an :class:`~kivy.properties.NumericProperty` - and defaults to `dp(20)`. - """ - - description_text_color = ListProperty([0.9, 0.9, 0.9, 1]) - """ - Text size for description text. - - :attr:`description_text_color` is an :class:`~kivy.properties.ListProperty` - and defaults to `[0.9, 0.9, 0.9, 1]`. - """ - - description_text_bold = BooleanProperty(False) - """ - Whether description should be bold. - - :attr:`description_text_bold` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - draw_shadow = BooleanProperty(False) - """ - Whether to show shadow. - - :attr:`draw_shadow` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - cancelable = BooleanProperty(False) - """ - Whether clicking outside the outer circle dismisses the view. - - :attr:`cancelable` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - widget_position = OptionProperty( - "left", - options=[ - "left", - "right", - "top", - "bottom", - "left_top", - "right_top", - "left_bottom", - "right_bottom", - "center", - ], - ) - """ - Sets the position of the widget on the :attr:`~outer_circle`. Available options are - `'left`', `'right`', `'top`', `'bottom`', `'left_top`', `'right_top`', - `'left_bottom`', `'right_bottom`', `'center`'. - - :attr:`widget_position` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'left'`. - """ - - title_position = OptionProperty( - "auto", - options=[ - "auto", - "left", - "right", - "top", - "bottom", - "left_top", - "right_top", - "left_bottom", - "right_bottom", - ], - ) - """ - Sets the position of :attr`~title_text` on the outer circle. Only works if - :attr`~widget_position` is set to `'center'`. In all other cases, it - calculates the :attr`~title_position` itself. - Must be set to other than `'auto`' when :attr`~widget_position` is set - to `'center`'. - - Available options are `'auto'`, `'left`', `'right`', `'top`', `'bottom`', - `'left_top`', `'right_top`', `'left_bottom`', `'right_bottom`', `'center`'. - - :attr:`title_position` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'auto'`. - """ - - stop_on_outer_touch = BooleanProperty(False) - """ - Whether clicking on outer circle stops the animation. - - :attr:`stop_on_outer_touch` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - stop_on_target_touch = BooleanProperty(True) - """ - Whether clicking on target circle should stop the animation. - - :attr:`stop_on_target_touch` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. - """ - - state = OptionProperty("close", options=["close", "open"]) - """ - State of :class:`~MDTapTargetView`. - - :attr:`state` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'close'`. - """ - - _outer_radius = NumericProperty(0) - _target_radius = NumericProperty(0) - - __elevation = 0 - - def __init__(self, **kwargs): - self.ripple_max_dist = dp(90) - self.on_outer_radius(self, self.outer_radius) - self.on_target_radius(self, self.target_radius) - self.anim_ripple = None - - self.core_title_text = Label( - markup=True, size_hint=(None, None), bold=self.title_text_bold - ) - self.core_title_text.bind( - texture_size=self.core_title_text.setter("size") - ) - self.core_description_text = Label(markup=True, size_hint=(None, None)) - self.core_description_text.bind( - texture_size=self.core_description_text.setter("size") - ) - - super().__init__(**kwargs) - self.register_event_type("on_outer_touch") - self.register_event_type("on_target_touch") - self.register_event_type("on_outside_click") - self.register_event_type("on_open") - self.register_event_type("on_close") - - if not self.outer_circle_color: - self.outer_circle_color = self.theme_cls.primary_color[:-1] - - def start(self, *args): - """Starts widget opening animation.""" - - self._initialize() - self._animate_outer() - self.state = "open" - self.core_title_text.opacity = 1 - self.core_description_text.opacity = 1 - self.dispatch("on_open") - - elevation = getattr(self.widget, "elevation", None) - if elevation: - self.__elevation = elevation - self.widget.elevation = 0 - - def stop(self, *args): - """Starts widget close animation.""" - - # It needs a better implementation. - if self.anim_ripple is not None: - self.anim_ripple.unbind(on_complete=self._repeat_ripple) - self.core_title_text.opacity = 0 - self.core_description_text.opacity = 0 - anim = Animation( - d=0.15, - t="in_cubic", - **dict( - zip( - ["_outer_radius", "_target_radius", "target_ripple_radius"], - [0, 0, 0], - ) - ), - ) - anim.bind(on_complete=self._after_stop) - anim.start(self.widget) - - def on_open(self, *args): - """Called at the time of the start of the widget opening animation.""" - - def on_close(self, *args): - """Called at the time of the start of the widget closed animation.""" - - def on_draw_shadow(self, instance, value): - Logger.warning( - "The shadow adding method will be implemented in future versions" - ) - - def on_description_text(self, instance, value): - self.core_description_text.text = value - - def on_description_text_size(self, instance, value): - self.core_description_text.font_size = value - - def on_description_text_bold(self, instance, value): - self.core_description_text.bold = value - - def on_title_text(self, instance, value): - self.core_title_text.text = value - - def on_title_text_size(self, instance, value): - self.core_title_text.font_size = value - - def on_title_text_bold(self, instance, value): - self.core_title_text.bold = value - - def on_outer_radius(self, instance, value): - self._outer_radius = self.outer_radius * 2 - - def on_target_radius(self, instance, value): - self._target_radius = self.target_radius * 2 - - def on_target_touch(self): - if self.stop_on_target_touch: - self.stop() - - def on_outer_touch(self): - if self.stop_on_outer_touch: - self.stop() - - def on_outside_click(self): - if self.cancelable: - self.stop() - - def _initialize(self): - setattr(self.widget, "_outer_radius", 0) - setattr(self.widget, "_target_radius", 0) - setattr(self.widget, "target_ripple_radius", 0) - setattr(self.widget, "target_ripple_alpha", 0) - - # Bind some function on widget event when this function is called - # instead of when the class itself is initialized to prevent all - # widgets of all instances to get bind at once and start messing up. - self.widget.bind(on_touch_down=self._some_func) - - def _draw_canvas(self): - _pos = self._ttv_pos() - self.widget.canvas.before.remove_group("ttv_group") - - with self.widget.canvas.before: - # Outer circle. - Color( - *self.outer_circle_color, - self.outer_circle_alpha, - group="ttv_group", - ) - _rad1 = self.widget._outer_radius - Ellipse(size=(_rad1, _rad1), pos=_pos[0], group="ttv_group") - - # Title text. - Color(*self.title_text_color, group="ttv_group") - Rectangle( - size=self.core_title_text.texture.size, - texture=self.core_title_text.texture, - pos=_pos[1], - group="ttv_group", - ) - - # Description text. - Color(*self.description_text_color, group="ttv_group") - Rectangle( - size=self.core_description_text.texture.size, - texture=self.core_description_text.texture, - pos=( - _pos[1][0], - _pos[1][1] - self.core_description_text.size[1] - 5, - ), - group="ttv_group", - ) - - # Target circle. - Color(*self.target_circle_color, group="ttv_group") - _rad2 = self.widget._target_radius - Ellipse( - size=(_rad2, _rad2), - pos=( - self.widget.x - (_rad2 / 2 - self.widget.size[0] / 2), - self.widget.y - (_rad2 / 2 - self.widget.size[0] / 2), - ), - group="ttv_group", - ) - - # Target ripple. - Color( - *self.target_circle_color, - self.widget.target_ripple_alpha, - group="ttv_group", - ) - _rad3 = self.widget.target_ripple_radius - Ellipse( - size=(_rad3, _rad3), - pos=( - self.widget.x - (_rad3 / 2 - self.widget.size[0] / 2), - self.widget.y - (_rad3 / 2 - self.widget.size[0] / 2), - ), - group="ttv_group", - ) - - def _after_stop(self, *args): - self.widget.canvas.before.remove_group("ttv_group") - args[0].stop_all(self.widget) - - elevation = getattr(self.widget, "elevation", None) - if elevation: - self.widget.elevation = self.__elevation - - self.dispatch("on_close") - - # Don't forget to unbind the function or it'll mess - # up with other next bindings. - self.widget.unbind(on_touch_down=self._some_func) - self.state = "close" - - def _fix_elev(self): - with self.widget.canvas.before: - Color(a=self.widget._soft_shadow_a) - Rectangle( - texture=self.widget._soft_shadow_texture, - size=self.widget._soft_shadow_size, - pos=self.widget._soft_shadow_pos, - ) - Color(a=self.widget._hard_shadow_a) - Rectangle( - texture=self.widget._hard_shadow_texture, - size=self.widget._hard_shadow_size, - pos=self.widget._hard_shadow_pos, - ) - Color(a=1) - - def _animate_outer(self): - anim = Animation( - d=0.2, - t="out_cubic", - **dict( - zip( - ["_outer_radius", "_target_radius"], - [self._outer_radius, self._target_radius], - ) - ), - ) - anim.cancel_all(self.widget) - anim.bind(on_progress=lambda x, y, z: self._draw_canvas()) - anim.bind(on_complete=self._animate_ripple) - anim.start(self.widget) - setattr(self.widget, "target_ripple_radius", self._target_radius) - setattr(self.widget, "target_ripple_alpha", 1) - - def _animate_ripple(self, *args): - self.anim_ripple = Animation( - d=1, - t="in_cubic", - target_ripple_radius=self._target_radius + self.ripple_max_dist, - target_ripple_alpha=0, - ) - self.anim_ripple.stop_all(self.widget) - self.anim_ripple.bind(on_progress=lambda x, y, z: self._draw_canvas()) - self.anim_ripple.bind(on_complete=self._repeat_ripple) - self.anim_ripple.start(self.widget) - - def _repeat_ripple(self, *args): - setattr(self.widget, "target_ripple_radius", self._target_radius) - setattr(self.widget, "target_ripple_alpha", 1) - self._animate_ripple() - - def _some_func(self, wid, touch): - """ - This function decides which one to dispatch based on the touch - position. - """ - - if self._check_pos_target(touch.pos): - self.dispatch("on_target_touch") - elif self._check_pos_outer(touch.pos): - self.dispatch("on_outer_touch") - else: - self.dispatch("on_outside_click") - - def _check_pos_outer(self, pos): - """ - Checks if a given `pos` coordinate is within the :attr:`~outer_radius`. - """ - - cx = self.circ_pos[0] + self._outer_radius / 2 - cy = self.circ_pos[1] + self._outer_radius / 2 - r = self._outer_radius / 2 - h, k = pos - - lhs = (cx - h) ** 2 + (cy - k) ** 2 - rhs = r**2 - if lhs <= rhs: - return True - return False - - def _check_pos_target(self, pos): - """ - Checks if a given `pos` coordinate is within the - :attr:`~target_radius`. - """ - - cx = self.widget.pos[0] + self.widget.width / 2 - cy = self.widget.pos[1] + self.widget.height / 2 - r = self._target_radius / 2 - h, k = pos - - lhs = (cx - h) ** 2 + (cy - k) ** 2 - rhs = r**2 - if lhs <= rhs: - return True - return False - - def _ttv_pos(self): - """ - Calculates the `pos` value for outer circle and text - based on the position provided. - - :returns: A tuple containing pos for the circle and text. - """ - - _rad1 = self.widget._outer_radius - _center_x = self.widget.x - (_rad1 / 2 - self.widget.size[0] / 2) - _center_y = self.widget.y - (_rad1 / 2 - self.widget.size[0] / 2) - - if self.widget_position == "left": - circ_pos = (_center_x + _rad1 / 3, _center_y) - title_pos = (_center_x + _rad1 / 1.4, _center_y + _rad1 / 1.4) - elif self.widget_position == "right": - circ_pos = (_center_x - _rad1 / 3, _center_y) - title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 1.4) - elif self.widget_position == "top": - circ_pos = (_center_x, _center_y - _rad1 / 3) - title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4) - elif self.widget_position == "bottom": - circ_pos = (_center_x, _center_y + _rad1 / 3) - title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 1.2) - # Corner ones need to be at a little smaller distance - # than edge ones that's why _rad1/4. - elif self.widget_position == "left_top": - circ_pos = (_center_x + _rad1 / 4, _center_y - _rad1 / 4) - title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 4) - elif self.widget_position == "right_top": - circ_pos = (_center_x - _rad1 / 4, _center_y - _rad1 / 4) - title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 4) - elif self.widget_position == "left_bottom": - circ_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4) - title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.2) - elif self.widget_position == "right_bottom": - circ_pos = (_center_x - _rad1 / 4, _center_y + _rad1 / 4) - title_pos = (_center_x, _center_y + _rad1 / 1.2) - else: - # Center. - circ_pos = (_center_x, _center_y) - - if self.title_position == "auto": - raise ValueError( - "widget_position='center' requires title_position to be set." - ) - elif self.title_position == "left": - title_pos = (_center_x + _rad1 / 10, _center_y + _rad1 / 2) - elif self.title_position == "right": - title_pos = (_center_x + _rad1 / 1.6, _center_y + _rad1 / 2) - elif self.title_position == "top": - title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 1.3) - elif self.title_position == "bottom": - title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 4) - elif self.title_position == "left_top": - title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 1.4) - elif self.title_position == "right_top": - title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.3) - elif self.title_position == "left_bottom": - title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 4) - elif self.title_position == "right_bottom": - title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 3.5) - else: - raise ValueError( - f"'{self.title_position}'" - f"is not a valid value for title_position" - ) - - self.circ_pos = circ_pos - return circ_pos, title_pos diff --git a/kivymd/uix/templates/__init__.py b/kivymd/uix/templates/__init__.py deleted file mode 100644 index 0e627d776..000000000 --- a/kivymd/uix/templates/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Templates -========= - -Base classes for controlling the scale, rotation of the widget, etc. -""" - -from .rotatewidget import RotateWidget -from .scalewidget import ScaleWidget -from .stencilwidget import StencilWidget diff --git a/kivymd/uix/templates/rotatewidget/__init__.py b/kivymd/uix/templates/rotatewidget/__init__.py deleted file mode 100644 index 1fb518fe7..000000000 --- a/kivymd/uix/templates/rotatewidget/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .rotatewidget import RotateWidget diff --git a/kivymd/uix/templates/rotatewidget/rotatewidget.py b/kivymd/uix/templates/rotatewidget/rotatewidget.py deleted file mode 100644 index 9416a5f33..000000000 --- a/kivymd/uix/templates/rotatewidget/rotatewidget.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Templates/RotateWidget -====================== - -.. deprecated:: 1.0.0 - -.. note:: `RotateWidget` class has been deprecated. Please use - `RotateBahavior `_ - class instead. -""" - -__all__ = ("RotateWidget",) - -from kivy import Logger - -from kivymd.uix.behaviors import RotateBehavior - - -class RotateWidget(RotateBehavior): - """ - .. deprecated:: 1.1.0 - Use :class:`~kivymd.uix.behaviors.rotate_behavior.RotateBehavior` - class instead. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - Logger.warning( - "KivyMD: " - "The `RotateWidget` class has been deprecated. " - "Use the `RotateBehavior` class instead." - ) diff --git a/kivymd/uix/templates/scalewidget/__init__.py b/kivymd/uix/templates/scalewidget/__init__.py deleted file mode 100644 index 45d304dde..000000000 --- a/kivymd/uix/templates/scalewidget/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .scalewidget import ScaleWidget diff --git a/kivymd/uix/templates/scalewidget/scalewidget.py b/kivymd/uix/templates/scalewidget/scalewidget.py deleted file mode 100644 index 3fe69850f..000000000 --- a/kivymd/uix/templates/scalewidget/scalewidget.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Templates/ScaleWidget -===================== - -.. deprecated:: 1.1.0 - -Base class for controlling the scale of the widget. - -.. note:: `ScaleWidget` class has been deprecated. Please use - `ScaleBehavior `_ - class instead. -""" - -__all__ = ("ScaleWidget",) - -from kivy import Logger - -from kivymd.uix.behaviors import ScaleBehavior - - -class ScaleWidget(ScaleBehavior): - """ - .. deprecated:: 1.1.0 - Use :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` - class instead. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - Logger.warning( - "KivyMD: " - "The `ScaleWidget` class has been deprecated. " - "Use the `ScaleBehavior` class instead." - ) diff --git a/kivymd/uix/templates/stencilwidget/__init__.py b/kivymd/uix/templates/stencilwidget/__init__.py deleted file mode 100644 index a249f510a..000000000 --- a/kivymd/uix/templates/stencilwidget/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .stencilwidget import StencilWidget diff --git a/kivymd/uix/templates/stencilwidget/stencilwidget.py b/kivymd/uix/templates/stencilwidget/stencilwidget.py deleted file mode 100644 index a23158881..000000000 --- a/kivymd/uix/templates/stencilwidget/stencilwidget.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Templates/StencilWidget -======================= - -.. deprecated:: 1.1.0 - -Base class for controlling the stencil instructions of the widget. - -.. note:: `StencilWidget` class has been deprecated. Please use - `StencilBehavior `_ - class instead. -""" - -__all__ = ("StencilWidget",) - -from kivy import Logger - -from kivymd.uix.behaviors import StencilBehavior - - -class StencilWidget(StencilBehavior): - """ - .. deprecated:: 1.1.0 - Use :class:`~kivymd.uix.behaviors.scale_behavior.StencilBehavior` - class instead. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - Logger.warning( - "KivyMD: " - "The `StencilWidget` class has been deprecated. " - "Use the `StencilBehavior` class instead." - ) diff --git a/kivymd/uix/textfield/__init__.py b/kivymd/uix/textfield/__init__.py index d3786ddf6..cb6b57a48 100644 --- a/kivymd/uix/textfield/__init__.py +++ b/kivymd/uix/textfield/__init__.py @@ -1,2 +1,9 @@ # NOQA F401 -from .textfield import MDTextField, MDTextFieldRect +from .textfield import ( + MDTextField, + MDTextFieldHelperText, + MDTextFieldMaxLengthText, + MDTextFieldHintText, + MDTextFieldLeadingIcon, + MDTextFieldTrailingIcon, +) diff --git a/kivymd/uix/textfield/textfield.kv b/kivymd/uix/textfield/textfield.kv index 1ed7597e6..ef8d9bf95 100644 --- a/kivymd/uix/textfield/textfield.kv +++ b/kivymd/uix/textfield/textfield.kv @@ -1,154 +1,179 @@ +#:import theme_font_styles kivymd.font_definitions.theme_font_styles + + input_filter: self.field_filter do_backspace: self.do_backspace canvas.before: Clear - - # "round" mode. - Color: - group: "round-color" - rgba: self._fill_color if self.mode == "round" else (0, 0, 0, 0) - Ellipse: - angle_start: 180 - angle_end: 360 - pos: self.x - self.height / 2 + dp(18), self.y - size: self.height, self.height - Ellipse: - angle_start: 360 - angle_end: 540 - pos: (self.width - dp(18)) + self.x - self.height / 2.0, self.y - size: self.height, self.height - Rectangle: - pos: self.x + dp(14), self.y - size: self.width - dp(28), self.height - + # Filled mode. Color: + group: "fill-color" rgba: ( \ - (self.line_color_focus if not self.error else self.error_color) \ - if self.focus else ( \ - self.theme_cls.disabled_hint_text_color \ - if not self.line_color_normal else \ - self.line_color_normal) \ + ( \ + self.theme_cls.surfaceVariantColor \ + if self.theme_bg_color == "Primary" else + ( \ + self.fill_color_normal \ + if self.fill_color_normal else \ + self.theme_cls.surfaceVariantColor \ ) \ - if self.mode == "round" else \ - (0, 0, 0, 0) - SmoothLine: - width: dp(1) - rounded_rectangle: - self.x, \ - self.y, \ - self.width, \ - self.height, \ - self.height / 2 - - # "fill" mode. - Color: - group: "fill-color" - rgba: self._fill_color if self.mode == "fill" else (0, 0, 0, 0) + ) \ + if not self.focus else \ + ( \ + self.theme_cls.surfaceVariantColor \ + if self.theme_bg_color == "Primary" else + ( \ + self.fill_color_focus \ + if self.fill_color_focus else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + ) \ + ) \ + if self.mode == "filled" else self.theme_cls.transparentColor RoundedRectangle: pos: self.x, self.y size: self.width, self.height - radius: self.radius + radius: self.radius[0], self.radius[1], 0, 0 - # Static underline texture. + # Active indicator. Color: - group: "static-underline-color" + group: "active-indicator-color" rgba: - (self._line_color_normal \ - if self.line_color_normal else self.theme_cls.divider_color) \ - if self.mode == "line" else (0, 0, 0, 0) + ( \ + ( \ + ( \ + ( \ + self.theme_cls.onSurfaceVariantColor \ + if self.theme_line_color == "Primary" else \ + ( \ + self.line_color_normal \ + if self.line_color_normal else \ + self.theme_cls.onSurfaceVariantColor \ + ) \ + ) \ + if not self.focus else \ + ( \ + self.theme_cls.primaryColor \ + if self.theme_line_color == "Primary" else \ + ( \ + self.line_color_focus \ + if self.line_color_focus else \ + self.theme_cls.primaryColor \ + ) \ + ) \ + ) \ + if not self.error else self._get_error_color() + ) \ + if not self.disabled else self.theme_cls.disabledTextColor \ + ) \ + if self.mode == "filled" else self.theme_cls.transparentColor Line: - points: self.x, self.y + dp(16), self.x + self.width, self.y + dp(16) - width: 1 - dash_length: dp(3) - dash_offset: 2 if self.disabled else 0 + width: self._indicator_height + points: + self.x + dp(1 if self.focus else 0), \ + self.y, \ + self.x - dp(1 if self.focus else 0) + self.width, \ + self.y - # Active underline (on focus) texture. + # Helper text texture. Color: - group: "active-underline-color" - rgba: - self._line_color_focus \ - if self.mode in ("line", "fill") and self.active_line \ - else (0, 0, 0, 0) + group: "helper-text-color" Rectangle: - size: self._underline_width, dp(1) + texture: + self._helper_text_label.texture \ + if self._helper_text_label else \ + None + size: + self._helper_text_label.texture_size \ + if self._helper_text_label else \ + (0, 0) pos: - self.center_x - (self._underline_width / 2), \ - self.y + (dp(16) if self.mode != "fill" else 0) + self.x + (dp(16) if self.mode == "filled" else \ + (0 if self.mode == "filled" else dp(12))), \ + self.y + dp(-18) - # Helper text texture. + # Leading icon texture. Color: - group: "helper-text-color" - rgba: - self.theme_cls.disabled_hint_text_color \ - if self.disabled else \ - self._helper_text_color + group: "leading-icons-color" Rectangle: - texture: self._helper_text_label.texture - size: self._helper_text_label.texture_size + texture: + self._leading_icon.texture if self._leading_icon else None + size: + self._leading_icon.texture_size \ + if self._leading_icon else \ + (0, 0) pos: - self.x + (dp(16) if self.mode == "fill" else \ - (0 if self.mode not in ("round", "rectangle") else dp(12))), \ - self.y + (dp(-18) if self.mode in ("fill", "rectangle", "round") else dp(-2)) + ( \ + ( \ + self.x + \ + ( \ + ( \ + 0 if self.mode != "outlined" else dp(12) \ + ) \ + if self.mode != "filled" else \ + ( \ + dp(4) if not self._leading_icon else dp(16) \ + ) \ + ), \ - # Right/left icon texture. + self.center_y - self._leading_icon.texture_size[1] / 2 \ + ) \ + ) \ + if self._leading_icon else (0, 0) + + # Trailing icon texture. Color: - group: "right-left-icons-color" - rgba: - self.theme_cls.disabled_hint_text_color \ - if self.disabled else \ - (self._icon_right_color if self.icon_right else self._icon_left_color) + group: "trailing-icons-color" Rectangle: texture: - self._icon_right_label.texture if self.icon_right else self._icon_left_label.texture + self._trailing_icon.texture if self._trailing_icon else None size: - (0, 0) if (not self.icon_right and not self.icon_left) else \ - (self._icon_right_label.texture_size if self.icon_right else self._icon_left_label.texture_size) + self._trailing_icon.texture_size \ + if self._trailing_icon else \ + (0, 0) pos: ( \ - (self.width + self.x - (0 if self.mode != "round" else dp(4))) - \ - (self._icon_right_label.texture_size[1]) - dp(8), \ - self.center[1] - self._icon_right_label.texture_size[1] / 2 + ((dp(8) \ - if self.mode != "round" else 0) if self.mode != "fill" else 0) \ - if self.mode != "rectangle" else \ - self.center[1] - self._icon_right_label.texture_size[1] / 2 - dp(4) \ + (self.width + self.x) - \ + (self._trailing_icon.texture_size[1]) - dp(14), \ + self.center_y - self._trailing_icon.texture_size[1] / 2 \ ) \ - if self.icon_right else \ - ( \ - self.x + ((dp((0 if self.mode != "round" else 12)) \ - if self.mode != "rectangle" else dp(12)) \ - if self.mode != "fill" else (dp(4) if not self.icon_left else dp(16))), \ - - self.center[1] - self._icon_left_label.texture_size[1] / 2 + (((dp(4) \ - if self.mode != "round" else 0) if self.mode not in ("rectangle", "fill") \ - else dp(8)) if self.mode != "fill" else 0) \ - if self.mode != "rectangle" else \ - self.center[1] - self._icon_left_label.texture_size[1] / 2 - dp(4) \ - ) + if self._trailing_icon else (0, 0) # Max length texture. Color: group: "max-length-color" - rgba: - self.theme_cls.disabled_hint_text_color \ - if self.disabled else \ - self._max_length_text_color Rectangle: - texture: self._max_length_label.texture - size: self._max_length_label.texture_size + group: "max-length-rect" + texture: + self._max_length_label.texture \ + if self._max_length_label else \ + None + size: + self._max_length_label.texture_size \ + if self._max_length_label else \ + (0, 0) pos: - self.x + self.width - self._max_length_label.texture_size[0] - dp(12), \ - self.y - (dp(2) if self.mode == "line" else dp(18)) + ( \ + (self.x + self.width) \ + - (self._max_length_label.texture_size[0] + dp(16)), \ + self.y - dp(18) \ + ) \ + if self._max_length_label else (0, 0) # Cursor blink. Color: rgba: ( \ - (self.text_color_focus if not self.error else self.error_color) \ + ( \ + self.theme_cls.primaryColor \ + if not self.error else \ + self._get_error_color() \ + ) \ if self.focus \ - else self._text_color_normal \ + else self.theme_cls.primaryColor \ ) \ if self.focus and not self._cursor_blink \ else \ @@ -157,161 +182,225 @@ pos: (int(x) for x in self.cursor_pos) size: 1, -self.line_height - # "rectangle" mode + # Outlined mode. Color: group: "rectangle-color" rgba: ( \ - (self.line_color_focus if not self.error else self.error_color) \ - if self.focus else \ - self.line_color_normal \ - ) \ - if self.mode == "rectangle" else \ - (0, 0, 0, 0) - SmoothLine: - width: dp(1) - rounded_rectangle: - self.x, \ - self.y, \ - self.width, \ - self.height - self._hint_text_label.texture_size[1] // 2, \ - root.radius[0] - - # The background color line of the widget on which the text field - # is placed (for background hint text texture). - Color: - rgba: ( \ ( \ - self.parent.md_bg_color \ - if hasattr(self.parent, "md_bg_color") \ - and self.parent.md_bg_color != [1, 1, 1, 0] else \ - self.theme_cls.bg_normal \ + ( \ + self.theme_cls.primaryColor \ + if self.theme_line_color == "Primary" else \ + self.line_color_focus \ + if self.line_color_focus else \ + self.theme_cls.primaryColor \ ) \ if self.focus else \ ( \ - (0, 0, 0, 0) if not self.text else \ - ( \ - self.parent.md_bg_color \ - if hasattr(self.parent, "md_bg_color") \ - and self.parent.md_bg_color != [1, 1, 1, 0] else \ - self.theme_cls.bg_normal \ + self.theme_cls.outlineColor \ + if self.theme_line_color == "Primary" else \ + self.line_color_normal \ + if self.line_color_normal else \ + self.theme_cls.outlineColor \ ) \ ) \ + if not self.error else self._get_error_color() \ ) \ - if self.mode == "rectangle" else \ - (0, 0, 0, 0) + if not self.disabled else \ + app.theme_cls.onSurfaceColor[:-1] + \ + [self.text_field_opacity_value_disabled_line] + ) \ + if self.mode != "filled" else self.theme_cls.transparentColor + + # Top right corner. + # ------------------------------------------------------─╮ + SmoothLine: + width: self._outline_height + circle: + self.x + self.width - self.radius[1], \ + self.y + self.height - self.radius[1], \ + self.radius[1], \ + 0, \ + 90 + + # Bottom corner. + # -----------------------------------------------------─╯ + SmoothLine: + width: self._outline_height + circle: + self.x + self.width - self.radius[2], \ + self.y + self.radius[2], \ + -self.radius[2], \ + 0, \ + -90 + + # Top left corner. + # ╭─------------------------------------------------------ + SmoothLine: + width: self._outline_height + circle: + self.x + self.radius[0], \ + self.y + self.height - self.radius[0], \ + -self.radius[0], \ + 180, \ + 90 + + # Bottom left corner. + # ╰─----------------------------------------------------- + SmoothLine: + width: self._outline_height + circle: + self.x + self.radius[3], \ + self.y + self.radius[3], \ + -self.radius[3], \ + 0, \ + 90 + + # Left vertical line. + # │ + # │ + # ╰─------------------------------------------------------ + SmoothLine: + width: self._outline_height + points: + self.x, \ + self.y + self.radius[3], \ + self.x, \ + self.y + (self.height - self.radius[0]) + + # Right vertical line. + # │ + # │ + # -----------------------------------------------------─╯ + SmoothLine: + width: self._outline_height + points: + self.x + self.width, \ + self.y + self.radius[2], \ + self.x + self.width, \ + self.y + (self.height - self.radius[1]) + + # Bottom horizontal line. + # ——————————————————————————————————————————————————————─╯ SmoothLine: - width: dp(2) + width: self._outline_height points: - self.x + dp(10), \ - self.top - self._hint_text_label.texture_size[1] // 2, \ - self.x + dp(16) + self._hint_text_label.texture_size[0], \ - self.top - self._hint_text_label.texture_size[1] // 2 + self.x + self.radius[3], \ + self.y, \ + self.x + self.width - self.radius[2], \ + self.y + + # Top (left) part of the line. + # ╭─----------------------------------------------------- + SmoothLine: + width: self._outline_height + points: + self.x + self.radius[0], \ + self.y + self.height, \ + self.x + self._left_x_axis_pos, \ + self.y + self.height + + # Top (right) part of the line. + # ╭─-----------—————————————————————————————————————————─╮ + SmoothLine: + width: self._outline_height + points: + self.x + self._right_x_axis_pos, \ + self.y + self.height, \ + self.x + self.width - self.radius[1], \ + self.y + self.height # Text color. Color: group: "text-color" rgba: - self.theme_cls.disabled_hint_text_color if self.disabled else \ + self.theme_cls.disabled_hint_text_color \ + if self.disabled else \ ( \ - self.text_color_focus if self.focus else self._text_color_normal + self.text_color_focus \ + if self.text_color_focus and self.theme_text_color == "Custom" \ + else self.theme_cls.onSurfaceColor \ ) \ - if not self.error else self.error_color + if self.focus else \ + ( \ + self.text_color_normal \ + if self.text_color_normal and self.theme_text_color == "Custom" \ + else self.theme_cls.onSurfaceVariantColor \ + ) + # Hint texture. canvas.after: - # Hint text texture. Color: group: "hint-text-color" - rgba: - self.theme_cls.disabled_hint_text_color \ - if self.disabled else \ - self._hint_text_color Rectangle: - texture: self._hint_text_label.texture - size: self._hint_text_label.texture_size + group: "hint-text-rectangle" + texture: + self._hint_text_label.texture \ + if self._hint_text_label else \ + None + size: + self._hint_text_label.texture_size \ + if self._hint_text_label else \ + (0, 0) pos: ( \ - self.x + ((dp(16) if not self.icon_left else dp(52)) \ - if self.mode == "fill" else ( \ - ((0 if self.mode != "round" else dp(12)) if self.mode != "rectangle" else dp(12)) \ - if not self.icon_left else \ - (dp(36 if self.mode != "round" else 42) if self.mode != "rectangle" else dp(42)))) \ - if not self.focus and not self.text else \ - self.x + ((dp(16) if self.mode != "round" else dp(36 if not self.icon_left else 42)) \ - if self.mode in ("fill", "rectangle", "round") and \ - self.icon_left else (0 if self.mode != "round" else dp(12))) + self._hint_x + self.x + \ + ( \ + dp(16) \ + if not self._leading_icon else \ + self._leading_icon.texture_size[0] + dp(28) + self._hint_x \ ), \ - self.y + self.height + (((dp(4) if self.mode != "round" else dp(10)) \ - if self.mode != "line" else \ - dp(-6)) if self.mode != "rectangle" else dp(-4)) - self._hint_y - + self.y + self.height \ + + (self._hint_text_label.texture_size[1] / 2) \ + - (self.height / 2) \ + - self._hint_y \ + ) \ + if self._hint_text_label else (0, 0) - font_name: "Roboto" if not self.font_name else self.font_name - foreground_color: self.theme_cls.text_color bold: False + font_name: theme_font_styles[self.font_style][self.role]["font-name"] + font_size: theme_font_styles[self.font_style][self.role]["font-size"] padding: ( \ - ((0 if self.mode != "round" else "12dp") \ - if self.mode != "rectangle" else "12dp") \ - if not self.icon_left else \ - (("36dp" if self.mode != "round" else "42dp") \ - if self.mode != "rectangle" else "42dp") \ - ) \ - if self.mode != "fill" else \ - ("16dp" if not self.icon_left else "52dp"), \ - - "24dp" if self.mode != "round" else "8dp", \ - - ((0 if self.mode != "round" and not self.icon_left else dp(12)) \ - if self.mode != "rectangle" else "12dp") \ - if self.mode != "fill" and not self.icon_right else \ - ( \ - "14dp" \ - if not self.icon_right else \ - self._icon_right_label.texture_size[1] + (dp(20) \ - if self.mode != "round" else dp(24))), \ - - "8dp" if self.mode == "fill" else \ - (("22dp" if self.mode != "round" else "8dp") \ - if self.icon_left and self.mode != "rectangle" else \ - ("16dp" if self.mode in ("fill", "rectangle") else \ - "20dp" if self.mode != "round" else "8dp")) + dp(16) if not self._leading_icon else dp(42) \ + if self.mode != "filled" else \ + (dp(16) if not self._leading_icon else dp(52)), \ + + (self.height / 2.0 - (self.line_height / 2.0) * len(self._lines)) \ + + dp(8 if self.mode == "filled" else 0), \ + + dp(16) \ + if not self._trailing_icon else \ + self._trailing_icon.texture_size[0] + dp(28), \ + + 0 \ + ) multiline: False size_hint_y: None - height: self.minimum_height + height: dp(56) - + + size_hint: None, None + size: "20dp", "20dp" size_hint_x: None width: self.texture_size[0] - shorten: True - shorten_from: "right" + theme_text_color: "Custom" - - on_focus: - self.anim_rect((self.x, self.y, self.right, self.y, self.right, \ - self.top, self.x, self.top, self.x, self.y), 1) if self.focus \ - else self.anim_rect((self.x - dp(60), self.y - dp(60), \ - self.right + dp(60), self.y - dp(60), - self.right + dp(60), self.top + dp(60), \ - self.x - dp(60), self.top + dp(60), \ - self.x - dp(60), self.y - dp(60)), 0) + + role: "large" + theme_font_size: "Custom" - canvas.after: - Color: - group: "color" - rgba: self._primary_color - Line: - group: "rectangle" - width: dp(1.5) - points: - ( - self.x - dp(60), self.y - dp(60), - self.right + dp(60), self.y - dp(60), - self.right + dp(60), self.top + dp(60), - self.x - dp(60), self.top + dp(60), - self.x - dp(60), self.y - dp(60) - ) + + + size_hint_x: None + width: self.texture_size[0] + adaptive_width: True + shorten: True + shorten_from: "right" + font_style: "Body" + role: "small" + theme_text_color: "Custom" diff --git a/kivymd/uix/textfield/textfield.py b/kivymd/uix/textfield/textfield.py index a351580f7..820cd3a45 100755 --- a/kivymd/uix/textfield/textfield.py +++ b/kivymd/uix/textfield/textfield.py @@ -1,295 +1,249 @@ """ -Components/TextField -==================== +Components/Text fields +====================== .. seealso:: - `Material Design spec, Text fields `_ + `Material Design spec, Text fields `_ -.. rubric:: Text fields let users enter and edit text. +.. rubric:: Text fields let users enter text into a UI. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields.png :align: center -`KivyMD` provides the following field classes for use: +- Make sure text fields look interactive +- Two types: filled and outlined +- The text field’s state (blank, with input, error, etc) should be visible at a glance +- Keep labels and error messages brief and easy to act on +- Text fields commonly appear in forms and dialogs -- MDTextField_ -- MDTextFieldRect_ - -.. Note:: :class:`~MDTextField` inherited from - :class:`~kivy.uix.textinput.TextInput`. Therefore, most parameters and all - events of the :class:`~kivy.uix.textinput.TextInput` class are also - available in the :class:`~MDTextField` class. - -.. MDTextField: -MDTextField ------------ - -:class:`~MDTextField` can be with helper text and without. - -Without helper text mode ------------------------- - -.. code-block:: kv - - MDTextField: - hint_text: "No helper text" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-no-helper-mode.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/available-fields.png :align: center -Helper text mode on ``on_focus`` event --------------------------------------- - -.. code-block:: kv - - MDTextField: - hint_text: "Helper text on focus" - helper_text: "This will disappear when you click off" - helper_text_mode: "on_focus" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-on-focus.gif - :align: center +1. Filled text field +2. Outlined text field -Persistent helper text mode ---------------------------- +Usage +----- .. code-block:: kv MDTextField: - hint_text: "Persistent helper text" - helper_text: "Text is always here" - helper_text_mode: "persistent" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-persistent.gif - :align: center - -Helper text mode `'on_error'` ------------------------------ + mode: "filled" -To display an error in a text field when using the -``helper_text_mode: "on_error"`` parameter, set the `"error"` text field -parameter to `True`: + MDTextFieldLeadingIcon: + icon: "magnify" -.. code-block:: python + MDTextFieldHintText: + text: "Hint text" - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDScreen: - - MDTextField: - id: text_field_error - hint_text: "Helper text on error (press 'Enter')" - helper_text: "There will always be a mistake" - helper_text_mode: "on_error" - pos_hint: {"center_x": .5, "center_y": .5} - size_hint_x: .5 - ''' - - - class Test(MDApp): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.screen = Builder.load_string(KV) - - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - self.screen.ids.text_field_error.bind( - on_text_validate=self.set_error_message, - on_focus=self.set_error_message, - ) - return self.screen + MDTextFieldHelperText: + text: "Helper text" + mode: "persistent" - def set_error_message(self, instance_textfield): - self.screen.ids.text_field_error.error = True + MDTextFieldTrailingIcon: + icon: "information" + MDTextFieldMaxLengthText: + max_text_length: 10 - Test().run() +Anatomy +------- -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-on-error.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-anatomy.png :align: center -Helper text mode `'on_error'` (with required) ---------------------------------------------- - -.. code-block:: kv - - MDTextField: - hint_text: "required = True" - text: "required = True" - required: True - helper_text_mode: "on_error" - helper_text: "Enter text" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-required.gif - :align: center +Available types of text fields +============================== -Text length control -------------------- +Filled mode +----------- .. code-block:: kv MDTextField: - hint_text: "Max text length = 5" - max_text_length: 5 + mode: "filled" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-length.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-filled-mode.png :align: center - -Multi line text ---------------- +Outlined mode +------------- .. code-block:: kv MDTextField: - multiline: True - hint_text: "Multi-line text" + mode: "outlined" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-multi-line.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-outlined-mode.png :align: center -Rectangle mode --------------- +Example +------- -.. code-block:: kv +.. tabs:: - MDTextField: - hint_text: "Rectangle mode" - mode: "rectangle" + .. tab:: Declarative KV style -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rectangle-mode.gif - :align: center + .. code-block:: python -Fill mode ---------- + from kivy.lang import Builder -.. code-block:: kv + from kivymd.app import MDApp - MDTextField: - hint_text: "Fill mode" - mode: "fill" + KV = ''' + MDScreen: + md_bg_color: app.theme_cls.backgroundColor -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode.gif - :align: center + MDTextField: + mode: "outlined" + size_hint_x: None + width: "240dp" + pos_hint: {"center_x": .5, "center_y": .5} -Round mode ---------- + MDTextFieldLeadingIcon: + icon: "account" -.. code-block:: kv + MDTextFieldHintText: + text: "Outlined" - MDTextField: - hint_text: "Round mode" - mode: "round" - max_text_length: 15 - helper_text: "Massage" + MDTextFieldHelperText: + text: "Helper text" + mode: "persistent" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-mode.gif - :align: center + MDTextFieldTrailingIcon: + icon: "information" -.. MDTextFieldRect: -MDTextFieldRect ---------------- + MDTextFieldMaxLengthText: + max_text_length: 10 + ''' -.. Note:: :class:`~MDTextFieldRect` inherited from - :class:`~kivy.uix.textinput.TextInput`. You can use all parameters and - attributes of the :class:`~kivy.uix.textinput.TextInput` class in the - :class:`~MDTextFieldRect` class. -.. code-block:: kv + class Example(MDApp): + def build(self): + self.theme_cls.primary_palette = "Olive" + return Builder.load_string(KV) - MDTextFieldRect: - size_hint: 1, None - height: "30dp" - background_color: app.theme_cls.bg_normal -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rect.gif - :align: center + Example().run() -.. Warning:: While there is no way to change the color of the border. + .. tab:: Declarative Python style -Clickable icon for MDTextField ------------------------------- + .. code-block:: python -.. code-block:: python + from kivymd.uix.textfield import ( + MDTextField, + MDTextFieldLeadingIcon, + MDTextFieldHintText, + MDTextFieldHelperText, + MDTextFieldTrailingIcon, + MDTextFieldMaxLengthText, + ) - from kivy.lang import Builder - from kivy.properties import StringProperty + from kivymd.uix.screen import MDScreen + from kivymd.app import MDApp + + + class Example(MDApp): + def build(self): + self.theme_cls.primary_palette = "Olive" + return MDScreen( + MDTextField( + MDTextFieldLeadingIcon( + icon="account", + ), + MDTextFieldHintText( + text="Hint text", + ), + MDTextFieldHelperText( + text="Helper text", + mode="persistent", + ), + MDTextFieldTrailingIcon( + icon="information", + ), + MDTextFieldMaxLengthText( + max_text_length=10, + ), + mode="outlined", + size_hint_x=None, + width="240dp", + pos_hint={"center_x": 0.5, "center_y": 0.5}, + ), + md_bg_color=self.theme_cls.backgroundColor, + ) - from kivymd.app import MDApp - from kivymd.uix.relativelayout import MDRelativeLayout - KV = ''' - : - size_hint_y: None - height: text_field.height + Example().run() - MDTextField: - id: text_field - hint_text: root.hint_text - text: root.text - password: True - icon_left: "key-variant" +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-example.png + :align: center - MDIconButton: - icon: "eye-off" - pos_hint: {"center_y": .5} - pos: text_field.width - self.width + dp(8), 0 - theme_text_color: "Hint" - on_release: - self.icon = "eye" if self.icon == "eye-off" else "eye-off" - text_field.password = False if text_field.password is True else True +API break +========= +1.2.0 version +------------- - MDScreen: +.. code-block:: kv - ClickableTextFieldRound: - size_hint_x: None - width: "300dp" - hint_text: "Password" - pos_hint: {"center_x": .5, "center_y": .5} - ''' + MDTextField: + mode: "rectangle" + hint_text: "Hint text" + helper_text: "Helper text" + helper_text_mode: "persistent" + max_text_length: 10 + icon_right: "information" +2.0.0 version +------------- - class ClickableTextFieldRound(MDRelativeLayout): - text = StringProperty() - hint_text = StringProperty() - # Here specify the required parameters for MDTextFieldRound: - # [...] +.. note:: The text field with the `round` type was removed in version `2.0.0`. +.. code-block:: kv - class Test(MDApp): - def build(self): - return Builder.load_string(KV) + MDTextField: + mode: "outlined" + MDTextFieldLeadingIcon: + icon: "phone" - Test().run() + MDTextFieldTrailingIcon: + icon: "information" -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-clickable_right-icon.gif - :align: center + MDTextFieldHintText: + text: "Hint text" -.. seealso:: + MDTextFieldHelperText: + text: "Helper text" + mode: "persistent" - See more information in the :class:`~MDTextFieldRect` class. + MDTextFieldMaxLengthText: + max_text_length: 10 """ -__all__ = ("MDTextField", "MDTextFieldRect") +__all__ = ( + "BaseTextFieldIcon", + "BaseTextFieldLabel", + "Validator", + "AutoFormatTelephoneNumber", + "MDTextField", + "MDTextFieldHelperText", + "MDTextFieldMaxLengthText", + "MDTextFieldHintText", + "MDTextFieldLeadingIcon", + "MDTextFieldTrailingIcon", +) import os import re from datetime import date -from typing import Union from kivy.animation import Animation from kivy.clock import Clock from kivy.lang import Builder -from kivy.metrics import dp, sp +from kivy.metrics import dp from kivy.properties import ( - AliasProperty, BooleanProperty, ColorProperty, ListProperty, @@ -297,15 +251,16 @@ def build(self): ObjectProperty, OptionProperty, StringProperty, + VariableListProperty, ) -from kivy.uix.label import Label from kivy.uix.textinput import TextInput from kivymd import uix_path from kivymd.font_definitions import theme_font_styles -from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import DeclarativeBehavior -from kivymd.uix.label import MDIcon +from kivymd.theming import ThemableBehavior, ThemeManager +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior +from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior +from kivymd.uix.label import MDIcon, MDLabel with open( os.path.join(uix_path, "textfield", "textfield.kv"), encoding="utf-8" @@ -320,19 +275,24 @@ class AutoFormatTelephoneNumber: """ Implements automatic formatting of the text entered in the text field according to the mask, for example '+38 (###) ### ## ##'. + + .. warning:: This class has not yet been implemented and it is not + recommended to use it yet. """ def __init__(self): self._backspace = False - def isnumeric(self, value): + def isnumeric(self, value) -> bool: try: int(value) return True except ValueError: return False - def do_backspace(self, *args): + def do_backspace(self, *args) -> None: + """Do backspace operation from the current cursor position.""" + if self.validator and self.validator == "phone": self._backspace = True text = self.text @@ -340,7 +300,7 @@ def do_backspace(self, *args): self.text = text self._backspace = False - def field_filter(self, value, boolean): + def field_filter(self, value, boolean) -> None: if self.validator and self.validator == "phone": if len(self.text) == 14: return @@ -348,7 +308,7 @@ def field_filter(self, value, boolean): return value return value - def format(self, value): + def format(self, value) -> None: if value != "" and not value.isspace() and not self._backspace: if len(value) <= 1 and self.focus: self.text = value @@ -418,11 +378,15 @@ class Validator: """ def is_email_valid(self, text: str) -> bool: + """Checks the validity of the email.""" + if not re.match(r"[^@]+@[^@]+\.[^@]+", text): return True return False def is_time_valid(self, text: str) -> bool: + """Checks the validity of the time.""" + if re.match(r"^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$", text) or re.match( r"^(2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9])$", text ): @@ -431,6 +395,8 @@ def is_time_valid(self, text: str) -> bool: return True def is_date_valid(self, text: str) -> bool: + """Checks the validity of the date.""" + if not self.date_format: raise Exception("TextInput date_format was not defined.") @@ -439,24 +405,24 @@ def is_date_valid(self, text: str) -> bool: mm = "[0][1-9]|[1][0-2]" yyyy = "[0-9][0-9][0-9][0-9]" fmt = self.date_format.split("/") - largs = locals() + args = locals() # Access the local variables dict in the correct format based on # date_format split. Example: "mm/dd/yyyy" -> ["mm", "dd", "yyyy"] - # largs[fmt[0]] would be largs["mm"] so the month regex string. + # args[fmt[0]] would be args["mm"] so the month regex string. if re.match( - f"^({largs[fmt[0]]})/({largs[fmt[1]]})/({largs[fmt[2]]})$", text + f"^({args[fmt[0]]})/({args[fmt[1]]})/({args[fmt[2]]})$", text ): input_split = text.split("/") - largs[fmt[0]] = input_split[0] - largs[fmt[1]] = input_split[1] - largs[fmt[2]] = input_split[2] + args[fmt[0]] = input_split[0] + args[fmt[1]] = input_split[1] + args[fmt[2]] = input_split[2] # Organize input into correct slots and try to convert # to datetime object. This way February exceptions are # tested. Also tests with the date_interval are simpler # using datetime objects. try: datetime = date( - int(largs["yyyy"]), int(largs["mm"]), int(largs["dd"]) + int(args["yyyy"]), int(args["mm"]), int(args["dd"]) ) except ValueError: return True @@ -482,7 +448,7 @@ def on_date_interval(): raise Exception("TextInput date_format was not defined.") fmt = self.date_format.split("/") - largs = {} + args = {} # Convert string inputs into datetime.date objects and store # them back into self.date_interval. try: @@ -490,28 +456,28 @@ def on_date_interval(): self.date_interval[0], date ): split = self.date_interval[0].split("/") - largs[fmt[0]] = split[0] - largs[fmt[1]] = split[1] - largs[fmt[2]] = split[2] + args[fmt[0]] = split[0] + args[fmt[1]] = split[1] + args[fmt[2]] = split[2] self.date_interval[0] = date( - int(largs["yyyy"]), int(largs["mm"]), int(largs["dd"]) + int(args["yyyy"]), int(args["mm"]), int(args["dd"]) ) if self.date_interval[1] and not isinstance( self.date_interval[1], date ): split = self.date_interval[1].split("/") - largs[fmt[0]] = split[0] - largs[fmt[1]] = split[1] - largs[fmt[2]] = split[2] + args[fmt[0]] = split[0] + args[fmt[1]] = split[1] + args[fmt[2]] = split[2] self.date_interval[1] = date( - int(largs["yyyy"]), int(largs["mm"]), int(largs["dd"]) + int(args["yyyy"]), int(args["mm"]), int(args["dd"]) ) except Exception: raise Exception( - r"TextInput date_interval was defined incorrectly, it must " - r"be composed of objects or strings" - r" following current date_format." + r"TextInput date_interval was defined incorrectly, " + r"it must be composed of objects " + r"or strings following current date_format." ) # Test if the interval is valid. @@ -520,668 +486,553 @@ def on_date_interval(): ): if self.date_interval[0] >= self.date_interval[1]: raise Exception( - "TextInput date_interval last date must be greater" - " than the first date or set to None." + "TextInput date_interval last date must be greater " + "than the first date or set to None." ) Clock.schedule_once(lambda x: on_date_interval()) -class MDTextFieldRect(ThemableBehavior, TextInput): +class BaseTextFieldLabel(MDLabel): """ - Textfield rect class. + Base texture for :class:`~MDTextField` class (helper text, max length, + hint text). For more information, see in the - :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivy.uix.textinput.TextInput` - classes documentation. - """ + :class:`~kivymd.uix.label.label.MDLabel` class documentation. - line_anim = BooleanProperty(True) + .. versionadded:: 2.0.0 """ - If True, then text field shows animated line when on focus. - :attr:`line_anim` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. + text_color_normal = ColorProperty(None) """ + Text color in (r, g, b, a) or string format when text field is out + of focus. - def get_rect_instruction(self): - canvas_instructions = self.canvas.after.get_group("rectangle") - return canvas_instructions[0] + .. versionadded:: 1.0.0 - _rectangle = AliasProperty(get_rect_instruction, cache=True) - """ - It is the :class:`~kivy.graphics.vertex_instructions.Line` - instruction reference of the field rectangle. + .. versionchanged:: 2.0.0 + The property was moved from class:`~MDTextField` class and renamed + from `helper_text_color_normal` to `text_color_normal`. - :attr:`_rectangle` is an :class:`~kivy.properties.AliasProperty`. - """ + .. code-block:: kv - def get_color_instruction(self): - canvas_instructions = self.canvas.after.get_group("color") - return canvas_instructions[0] + MDTextField: + mode: "filled" + + MDTextFieldHintText: + text: "Hint text color normal" + text_color_normal: "brown" - _rectangle_color = AliasProperty(get_color_instruction, cache=True) - """ - It is the :class:`~kivy.graphics.context_instructions.Color` - instruction reference of the field rectangle. + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-text-color-normal.png + :align: center - :attr:`_rectangle_color` is an :class:`~kivy.properties.AliasProperty`. + :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - _primary_color = ColorProperty((0, 0, 0, 0)) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._update_primary_color() - self.theme_cls.bind(primary_color=self._update_primary_color) - - def anim_rect(self, points, alpha): - if alpha == 1: - d_line = 0.3 - d_color = 0.4 - else: - d_line = 0.05 - d_color = 0.05 - - Animation( - points=points, d=(d_line if self.line_anim else 0), t="out_cubic" - ).start(self._rectangle) - Animation(a=alpha, d=(d_color if self.line_anim else 0)).start( - self._rectangle_color - ) + text_color_focus = ColorProperty(None) + """ + Text color in (r, g, b, a) or string format when the text field has + focus. - def _update_primary_color(self, *args): - self._primary_color = self.theme_cls.primary_color - self._primary_color[3] = 0 + .. versionadded:: 1.0.0 + .. versionchanged:: 2.0.0 + The property was moved from class:`~MDTextField` class and renamed + from `helper_text_color_focus` to `text_color_focus`. -class TextfieldLabel(ThemableBehavior, Label): - """Base texture for :class:`~MDTextField` class.""" + .. code-block:: kv - font_style = OptionProperty("Body1", options=theme_font_styles) - # - field = ObjectProperty() + MDTextField: - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.font_size = sp(self.theme_cls.font_styles[self.font_style][1]) + MDTextFieldHelperText: + text: "Helper text color focus" + text_color_focus: "brown" + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-text-color-focus.png + :align: center -class MDTextField( - DeclarativeBehavior, - ThemableBehavior, - TextInput, - Validator, - AutoFormatTelephoneNumber, -): + :attr:`text_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - Textfield class. - For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and - :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivy.uix.textinput.TextInput` and - :class:`~Validator` and - :class:`~AutoFormatTelephoneNumber` - classes documentation. - """ - helper_text = StringProperty() +class MDTextFieldHelperText(BaseTextFieldLabel): """ - Text for ``helper_text`` mode. + Implements the helper text label. + + For more information, see in the :class:`~BaseTextFieldLabel` + class documentation. - :attr:`helper_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. + .. versionadded:: 2.0.0 """ - helper_text_mode = OptionProperty( + mode = OptionProperty( "on_focus", options=["on_error", "persistent", "on_focus"] ) """ Helper text mode. Available options are: `'on_error'`, `'persistent'`, `'on_focus'`. - :attr:`helper_text_mode` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'none'`. - """ - - max_text_length = NumericProperty(None) - """ - Maximum allowed value of characters in a text field. - - :attr:`max_text_length` is an :class:`~kivy.properties.NumericProperty` - and defaults to `None`. - """ - - required = BooleanProperty(False) - """ - Required text. If True then the text field requires text. - - :attr:`required` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - mode = OptionProperty( - "line", options=["rectangle", "round", "fill", "line"] - ) - """ - Text field mode. - Available options are: `'line'`, `'rectangle'`, `'fill'`, `'round'`. - - :attr:`mode` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'line'`. - """ - - phone_mask = StringProperty("") - - validator = OptionProperty(None, options=["date", "email", "time", "phone"]) - """ - The type of text field for entering Email, time, etc. - Automatically sets the type of the text field as "error" if the user input - does not match any of the set validation types. - Available options are: `'date'`, `'email'`, `'time'`. - - When using `'date'`, :attr:`date_format` must be defined. + .. versionchanged:: 2.0.0 + The property was moved from class:`~MDTextField` class and renamed + from `helper_text_mode` to `mode`. - .. versionadded:: 1.1.0 + On focus + -------- - .. code-block:: python + .. code-block:: kv MDTextField: - hint_text: "Email" - helper_text: "user@gmail.com" - validator: "email" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-validator.png - :align: center - - .. tabs:: - - .. tab:: Declarative KV style - - .. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDScreen: - - MDBoxLayout: - orientation: "vertical" - spacing: "20dp" - adaptive_height: True - size_hint_x: .8 - pos_hint: {"center_x": .5, "center_y": .5} - - MDTextField: - hint_text: "Date dd/mm/yyyy without limits" - helper_text: "Enter a valid dd/mm/yyyy date" - validator: "date" - date_format: "dd/mm/yyyy" - - MDTextField: - hint_text: "Date mm/dd/yyyy without limits" - helper_text: "Enter a valid mm/dd/yyyy date" - validator: "date" - date_format: "mm/dd/yyyy" - - MDTextField: - hint_text: "Date yyyy/mm/dd without limits" - helper_text: "Enter a valid yyyy/mm/dd date" - validator: "date" - date_format: "yyyy/mm/dd" - - MDTextField: - hint_text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval" - helper_text: "Enter a valid dd/mm/yyyy date" - validator: "date" - date_format: "dd/mm/yyyy" - date_interval: "01/01/1900", "01/01/2100" - - MDTextField: - hint_text: "Date dd/mm/yyyy in [01/01/1900, None] interval" - helper_text: "Enter a valid dd/mm/yyyy date" - validator: "date" - date_format: "dd/mm/yyyy" - date_interval: "01/01/1900", None - - MDTextField: - hint_text: "Date dd/mm/yyyy in [None, 01/01/2100] interval" - helper_text: "Enter a valid dd/mm/yyyy date" - validator: "date" - date_format: "dd/mm/yyyy" - date_interval: None, "01/01/2100" - ''' - - - 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.boxlayout import MDBoxLayout - from kivymd.uix.screen import MDScreen - from kivymd.uix.textfield import MDTextField - - - class Test(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return ( - MDScreen( - MDBoxLayout( - MDTextField( - hint_text="Date dd/mm/yyyy without limits", - helper_text="Enter a valid dd/mm/yyyy date", - validator="date", - date_format="dd/mm/yyyy", - ), - MDTextField( - hint_text="Date mm/dd/yyyy without limits", - helper_text="Enter a valid mm/dd/yyyy date", - validator="date", - date_format="mm/dd/yyyy", - ), - MDTextField( - hint_text="Date yyyy/mm/dd without limits", - helper_text="Enter a valid yyyy/mm/dd date", - validator="date", - date_format="yyyy/mm/dd", - ), - MDTextField( - hint_text="Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval", - helper_text="Enter a valid dd/mm/yyyy date", - validator="date", - date_format="dd/mm/yyyy", - date_interval=["01/01/1900", "01/01/2100"], - ), - MDTextField( - hint_text="Date dd/mm/yyyy in [01/01/1900, None] interval", - helper_text="Enter a valid dd/mm/yyyy date", - validator="date", - date_format="dd/mm/yyyy", - date_interval=["01/01/1900", None], - ), - MDTextField( - hint_text="Date dd/mm/yyyy in [None, 01/01/2100] interval", - helper_text="Enter a valid dd/mm/yyyy date", - validator="date", - date_format="dd/mm/yyyy", - date_interval=[None, "01/01/2100"], - ), - orientation="vertical", - spacing="20dp", - adaptive_height=True, - size_hint_x=0.8, - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - ) - + mode: "filled" - Test().run() + MDTextFieldHelperText: + text: "Helper text" + mode: "on_focus" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-validator-date.png + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-helper-text-mode-on-focus.gif :align: center - :attr:`validator` is an :class:`~kivy.properties.OptionProperty` - and defaults to `None`. - """ - - line_color_normal = ColorProperty([0, 0, 0, 0]) - """ - Line color normal (static underline line) in (r, g, b, a) or string format. + On error + -------- .. code-block:: kv MDTextField: - hint_text: "line_color_normal" - line_color_normal: "red" + mode: "filled" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-line-color-normal.png - :align: center + MDTextFieldHelperText: + text: "Helper text" + mode: "on_error" - :attr:`line_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. - """ + MDTextFieldMaxLengthText: + max_text_length: 5 - line_color_focus = ColorProperty([0, 0, 0, 0]) - """ - Line color focus (active underline line) in (r, g, b, a) or string format. + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-helper-text-mode-on-error.gif + :align: center + + Persistent + ---------- .. code-block:: kv MDTextField: - hint_text: "line_color_focus" - line_color_focus: "red" + mode: "filled" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-line-color-focus.gif + MDTextFieldHelperText: + text: "Helper text" + mode: "persistent" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-helper-text-mode-persistent.gif :align: center - :attr:`line_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'on_focus'`. """ - line_anim = BooleanProperty(True) - """ - If True, then text field shows animated underline when on focus. - :attr:`line_anim` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. +class MDTextFieldMaxLengthText(BaseTextFieldLabel): """ + Implements the max length text label. - error_color = ColorProperty([0, 0, 0, 0]) - """ - Error color in (r, g, b, a) or string format for ``required = True``. + For more information, see in the :class:`~BaseTextFieldLabel` + class documentation. - :attr:`error_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + .. versionadded:: 2.0.0 """ - fill_color_normal = ColorProperty([0, 0, 0, 0]) + max_text_length = NumericProperty(None) """ - Fill background color in (r, g, b, a) or string format in 'fill' mode when] - text field is out of focus. + Maximum allowed value of characters in a text field. - .. code=block:: kv + .. versionchanged:: 2.0.0 + The property was moved from class:`~MDTextField`. + + .. code-block:: kv MDTextField: - hint_text: "Fill mode" - mode: "fill" - fill_color_normal: "brown" + mode: "filled" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-color-normal.png + MDTextFieldMaxLengthText: + max_text_length: 10 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-max-text-length.png :align: center - :attr:`fill_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`max_text_length` is an :class:`~kivy.properties.NumericProperty` + and defaults to `None`. """ - fill_color_focus = ColorProperty([0, 0, 0, 0]) + +class MDTextFieldHintText(BaseTextFieldLabel): """ - Fill background color in (r, g, b, a) or string format in 'fill' mode when - the text field has focus. + Implements the hint text label. + + For more information, see in the :class:`~BaseTextFieldLabel` + class documentation. - .. code=block:: kv + .. versionadded:: 2.0.0 + + .. code-block:: kv MDTextField: - hint_text: "Fill mode" - mode: "fill" - fill_color_focus: "brown" + mode: "filled" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-color-focus.gif - :align: center + MDTextFieldHintText: + text: "Hint text" - :attr:`fill_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-hint-text.gif + :align: center """ - active_line = BooleanProperty(True) - """ - Show active line or not. - :attr:`active_line` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. +class BaseTextFieldIcon(MDIcon): """ + Base texture for :class:`~MDTextField` class (helper text, max length, + hint text). - error = BooleanProperty(False) - """ - If True, then the text field goes into ``error`` mode. + For more information, see in the :class:`~kivymd.uix.label.label.MDIcon` + class documentation. - :attr:`error` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. + .. versionchanged:: 2.0.0 """ - hint_text_color_normal = ColorProperty([0, 0, 0, 0]) + icon_color_normal = ColorProperty(None) """ - Hint text color in (r, g, b, a) or string format when text field is out + Icon color in (r, g, b, a) or string format when text field is out of focus. .. versionadded:: 1.0.0 + .. versionchanged:: 2.0.0 + The property was moved from class:`~MDTextField` class and renamed + from `icon_right_color_normal/icon_left_color_normal` + to `icon_color_normal`. + .. code-block:: kv MDTextField: - hint_text: "hint_text_color_normal" - hint_text_color_normal: "red" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-hint-text-color-normal.png + mode: "filled" + + MDTextFieldLeadingIcon: + icon: "phone" + theme_icon_color: "Custom" + icon_color_normal: "lightgreen" + + MDTextFieldHintText: + text: "Leading icon color normal" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-leading-icon-color-normal.png :align: center - :attr:`hint_text_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`icon_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - hint_text_color_focus = ColorProperty([0, 0, 0, 0]) + icon_color_focus = ColorProperty(None) """ - Hint text color in (r, g, b, a) or string format when the text field has + Icon color in (r, g, b, a) or string format when the text field has focus. .. versionadded:: 1.0.0 + .. versionchanged:: 2.0.0 + The property was moved from class:`~MDTextField` class and renamed + from `icon_right_color_focus/icon_left_color_focus ` + to `icon_color_focus`. + .. code-block:: kv MDTextField: - hint_text: "hint_text_color_focus" - hint_text_color_focus: "red" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-hint-text-color-focus.gif + mode: "filled" + + MDTextFieldLeadingIcon: + icon: "phone" + theme_icon_color: "Custom" + icon_color_focus: "lightgreen" + + MDTextFieldHintText: + text: "Leading icon color focus" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-leading-icon-color-focus.png :align: center - :attr:`hint_text_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`icon_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - helper_text_color_normal = ColorProperty([0, 0, 0, 0]) + +class MDTextFieldLeadingIcon(BaseTextFieldIcon): """ - Helper text color in (r, g, b, a) or string format when text field is out - of focus. + Implements the leading icon. - .. versionadded:: 1.0.0 + For more information, see in the :class:`~BaseTextFieldIcon` + class documentation. + + .. versionadded:: 2.0.0 .. code-block:: kv MDTextField: - helper_text: "helper_text_color_normal" - helper_text_mode: "persistent" - helper_text_color_normal: "red" + mode: "filled" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-text-color-normal.png - :align: center + MDTextFieldLeadingIcon: + icon: "phone" - :attr:`helper_text_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + MDTextFieldHintText: + text: "Field with leading icon" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-leading-icon.png + :align: center """ - helper_text_color_focus = ColorProperty([0, 0, 0, 0]) + +class MDTextFieldTrailingIcon(BaseTextFieldIcon): """ - Helper text color in (r, g, b, a) or string format when the text field has - focus. + Implements the trailing icon. - .. versionadded:: 1.0.0 + For more information, see in the :class:`~BaseTextFieldIcon` + class documentation. + + .. versionadded:: 2.0.0 .. code-block:: kv MDTextField: - helper_text: "helper_text_color_focus" - helper_text_mode: "persistent" - helper_text_color_focus: "red" + mode: "filled" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-text-color-focus.gif + MDTextFieldTrailingIcon: + icon: "phone" + + MDTextFieldHintText: + text: "Field with trailing icon" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-trailing-icon.png :align: center + """ + - :attr:`helper_text_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. +class MDTextField( + DeclarativeBehavior, + StateLayerBehavior, + ThemableBehavior, + TextInput, + Validator, + AutoFormatTelephoneNumber, + BackgroundColorBehavior, +): """ + Textfield class. - icon_right_color_normal = ColorProperty([0, 0, 0, 0]) + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.textinput.TextInput` and + :class:`~Validator` and + :class:`~AutoFormatTelephoneNumber` and + :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior` + classes documentation. """ - Color in (r, g, b, a) or string format of right icon when text field is out - of focus. - .. versionadded:: 1.0.0 + font_style = StringProperty("Body") + """ + Name of the style for the input text. - .. code-block:: kv + .. versionadded:: 2.0.0 - MDTextField: - icon_right: "language-python" - hint_text: "icon_right_color_normal" - icon_right_color_normal: "red" + .. seealso:: - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-icon-right-color-normal.png - :align: center + `Font style names `_ - :attr:`icon_right_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`font_style` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Body'`. """ - icon_right_color_focus = ColorProperty([0, 0, 0, 0]) + role = StringProperty("large") """ - Color in (r, g, b, a) or string format of right icon when the text field - has focus. + Role of font style. - .. versionadded:: 1.0.0 + .. versionadded:: 2.0.0 - .. code-block:: kv + .. seealso:: - MDTextField: - icon_right: "language-python" - hint_text: "icon_right_color_focus" - icon_right_color_focus: "red" + `Font style roles `_ - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-icon-right-color-focus.gif - :align: center + :attr:`role` is an :class:`~kivy.properties.StringProperty` + and defaults to `'large'`. + """ - :attr:`icon_right_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + mode = OptionProperty("outlined", options=["outlined", "filled"]) """ + Text field mode. Available options are: `'outlined'`, `'filled'`. - icon_left_color_normal = ColorProperty([0, 0, 0, 0]) + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'outlined'`. """ - Color in (r, g, b, a) or string format of right icon when text field is out - of focus. - .. versionadded:: 1.0.0 + error_color = ColorProperty(None) + """ + Error color in (r, g, b, a) or string format for `required = True` + or when the text field is in `error` state. - :attr:`icon_left_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`error_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - icon_left_color_focus = ColorProperty([0, 0, 0, 0]) + error = BooleanProperty(False) """ - Color in (r, g, b, a) or string format of right icon when the text field - has focus. + If True, then the text field goes into `error` mode. + + :attr:`error` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + text_color_normal = ColorProperty(None) + """ + Text color in (r, g, b, a) or string format when text field is out of focus. .. versionadded:: 1.0.0 - :attr:`icon_left_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + .. code-block:: kv + + MDTextField: + theme_text_color: "Custom" + text_color_normal: "green" + text: "Text color normal" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-normal.png + :align: center + + :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - max_length_text_color = ColorProperty([0, 0, 0, 0]) + text_color_focus = ColorProperty(None) """ - Text color in (r, g, b, a) or string format of the maximum length of - characters to be input. + Text color in (r, g, b, a) or string format when text field has focus. .. versionadded:: 1.0.0 .. code-block:: kv MDTextField: - hint_text: "max_length_text_color" - max_length_text_color: "red" - max_text_length: 5 + theme_text_color: "Custom" + text_color_focus: "green" + text: "Text color focus" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-max-length-text-color.png + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-focus.png :align: center - :attr:`max_length_text_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`text_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - icon_right = StringProperty() + radius = VariableListProperty([dp(4), dp(4), dp(4), dp(4)]) """ - Right icon texture. + The corner radius for a text field in `filled/outlined` mode. - .. note:: It's just a texture. It has no press/touch events. + :attr:`radius` is a :class:`~kivy.properties.VariableListProperty` and + defaults to `[dp(4), dp(4), 0, 0]`. + """ - :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. + required = BooleanProperty(False) """ + Required text. If True then the text field requires text. - icon_left = StringProperty() + :attr:`required` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. """ - Left icon texture. - .. versionadded:: 1.0.0 + line_color_normal = ColorProperty(None) + """ + Line color normal (active indicator) in (r, g, b, a) or string format. - .. note:: It's just a texture. It has no press/touch events. - Also note that you cannot use the left and right icons at the same time yet. + .. code-block:: kv - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-left-icon.png + MDTextField: + mode: "filled" + theme_line_color: "Custom" + line_color_normal: "green" + + MDTextFieldHelperText: + text: "Line color normal" + mode: "persistent" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-line-color-normal.png :align: center - :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. + :attr:`line_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - text_color_normal = ColorProperty([0, 0, 0, 0]) + line_color_focus = ColorProperty(None) """ - Text color in (r, g, b, a) or string format when text field is out of focus. - - .. versionadded:: 1.0.0 + Line color focus (active indicator) in (r, g, b, a) or string format. .. code-block:: kv MDTextField: - hint_text: "text_color_normal" - text_color_normal: "red" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-normal.png + mode: "filled" + theme_line_color: "Custom" + line_color_focus: "green" + + MDTextFieldHelperText: + text: "Line color focus" + mode: "persistent" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-line-color-focus.png :align: center - :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`line_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - text_color_focus = ColorProperty([0, 0, 0, 0]) + fill_color_normal = ColorProperty(None) """ - Text color in (r, g, b, a) or string format when text field has focus. - - .. versionadded:: 1.0.0 + Fill background color in (r, g, b, a) or string format in 'fill' mode when] + text field is out of focus. .. code-block:: kv MDTextField: - hint_text: "text_color_focus" - text_color_focus: "red" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-focus.gif + mode: "filled" + theme_bg_color: "Custom" + fill_color_normal: 0, 1, 0, .2 + + MDTextFieldHelperText: + text: "Fill color normal" + mode: "persistent" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-fill-color-normal.png :align: center - :attr:`text_color_focus` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. + :attr:`fill_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ - font_size = NumericProperty("16sp") + fill_color_focus = ColorProperty(None) """ - Font size of the text in pixels. + Fill background color in (r, g, b, a) or string format in 'fill' mode when + the text field has focus. + + .. code-block:: kv + + MDTextField: + mode: "filled" + theme_bg_color: "Custom" + fill_color_focus: 0, 1, 0, .2 + + MDTextFieldHelperText: + text: "Fill color focus" + mode: "persistent" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-fill-color-focus.png + :align: center - :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and - defaults to `'16sp'`. + :attr:`fill_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ # TODO: Add minimum allowed height. Otherwise, if the value is, @@ -1193,689 +1044,612 @@ def build(self): .. code-block:: kv MDTextField: - size_hint_x: .5 - hint_text: "multiline=True" + mode: "filled" max_height: "200dp" - mode: "fill" - fill_color: 0, 0, 0, .4 multiline: True - pos_hint: {"center_x": .5, "center_y": .5} + + MDTextFieldHelperText: + text: "multiline=True" + mode: "persistent" - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode-multiline-max-height.gif + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-multiline.gif :align: center :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ - radius = ListProperty([10, 10, 0, 0]) - """ - The corner radius for a text field in `fill/rectangle` mode. - - :attr:`radius` is a :class:`~kivy.properties.ListProperty` and - defaults to `[10, 10, 0, 0]`. + phone_mask = StringProperty("") """ + This property has not yet been implemented and it is not recommended to + use it yet. - font_name_helper_text = StringProperty("Roboto") + :attr:`phone_mask` is a :class:`~kivy.properties.StringProperty` and + defaults to ''. """ - Font name for helper text. - :attr:`font_name_helper_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `'Roboto'`. + validator = OptionProperty(None, options=["date", "email", "time", "phone"]) """ + The type of text field for entering Email, time, etc. + Automatically sets the type of the text field as "error" if the user input + does not match any of the set validation types. + Available options are: `'date'`, `'email'`, `'time'`. - font_name_hint_text = StringProperty("Roboto") - """ - Font name for hint text. + When using `'date'`, :attr:`date_format` must be defined. - :attr:`font_name_hint_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `'Roboto'`. - """ + .. versionadded:: 1.1.0 - font_name_max_length = StringProperty("Roboto") - """ - Font name for max text length. + .. code-block:: kv - :attr:`font_name_max_length` is an :class:`~kivy.properties.StringProperty` - and defaults to `'Roboto'`. - """ + MDTextField: + mode: "filled" + validator: "email" + + MDTextFieldHintText: + text: "Email" + + MDTextFieldHelperText: + text: "user@gmail.com" + mode: "persistent" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-email-validator.png + :align: center - # The x-axis position of the hint text in the text field. - _hint_x = NumericProperty(0) - # The y-axis position of the hint text in the text field. - _hint_y = NumericProperty("38dp") - # Width of underline that animates when the focus of the text field. - _underline_width = NumericProperty(0) - # Font size for hint text. - _hint_text_font_size = NumericProperty(sp(16)) - - # Label object for `helper_text` parameter. - _helper_text_label = None - # Label object for `max_text_length` parameter. - _max_length_label = None - # Label object for `hint_text` parameter. - _hint_text_label = None - # `MDIcon` object for the icon on the right. - _icon_right_label = None - # `MDIcon` object for the icon on the left. - _icon_left_label = None - - # The left and right coordinates of the text field in 'rectangle' mode. - # - # ┍──blank_space_left blank_space_right──────────────┑ - # | | - # | | - # | | - # ┕──────────────────────────────────────────────────────┙ - _line_blank_space_right_point = NumericProperty(0) - _line_blank_space_left_point = NumericProperty(0) - - # The values of colors that are used in the KV file to display the color - # of the corresponding texture. - _fill_color = ColorProperty([0, 0, 0, 0]) - _text_color_normal = ColorProperty([0, 0, 0, 0]) - _hint_text_color = ColorProperty([0, 0, 0, 0]) - _helper_text_color = ColorProperty([0, 0, 0, 0]) - _max_length_text_color = ColorProperty([0, 0, 0, 0]) - _icon_right_color = ColorProperty([0, 0, 0, 0]) - _icon_left_color = ColorProperty([0, 0, 0, 0]) - _line_color_normal = ColorProperty([0, 0, 0, 0]) - _line_color_focus = ColorProperty([0, 0, 0, 0]) - - # Text to restore the text of the tale after clearing the text field. - __hint_text = StringProperty() - # List of color attribute names that should be updated when changing the - # application color palette. - _colors_to_updated = ListProperty() + .. code-block:: python - def __init__(self, *args, **kwargs): - self.set_objects_labels() - Clock.schedule_once(self._set_attr_names_to_updated) - Clock.schedule_once(self.set_colors_to_updated) - Clock.schedule_once(self.set_default_colors) - super().__init__(*args, **kwargs) - self.bind( - _hint_text_font_size=self._hint_text_label.setter("font_size"), - _icon_right_color=self._icon_right_label.setter("text_color"), - _icon_left_color=self._icon_left_label.setter("text_color"), - font_name_hint_text=self._hint_text_label.setter("font_name"), - text=self.set_text, - ) - self.theme_cls.bind( - primary_color=self.set_default_colors, - theme_style=self.set_default_colors, - ) - Clock.schedule_once(self.check_text) + from kivy.lang import Builder - # TODO: Is this method necessary? - # During testing, a quick double-click on the text box does not stop - # the animation of the hint text height. - def cancel_all_animations_on_double_click(self) -> None: - """ - Cancels the animations of the text field when double-clicking on the - text field. - """ + from kivymd.app import MDApp - if ( - self._hint_y == dp(38) - and not self.text - or self._hint_y == dp(14) - and self.text - ): - Animation.cancel_all( - self, - "_underline_width", - "_hint_y", - "_hint_x", - "_hint_text_font_size", - ) + KV = ''' + MDScreen: + md_bg_color: self.theme_cls.backgroundColor - def set_colors_to_updated(self, interval: Union[float, int]) -> None: - for attr_name in self._attr_names_to_updated.keys(): - if getattr(self, attr_name) == [0, 0, 0, 0]: - self._colors_to_updated.append(attr_name) + MDBoxLayout: + orientation: "vertical" + spacing: "20dp" + adaptive_height: True + size_hint_x: .8 + pos_hint: {"center_x": .5, "center_y": .5} - def set_default_colors( - self, interval: Union[float, int], updated: bool = False - ) -> None: - """ - Sets the default text field colors when initializing a text field - object. Also called when the application palette changes. + MDTextField: + validator: "date" + date_format: "dd/mm/yyyy" - :param updated: If `True` - the color theme of the application has - been changed. Updating the meanings of the colors. - """ + MDTextFieldHintText: + text: "Date dd/mm/yyyy without limits" - self._set_attr_names_to_updated(0) - for attr_name in self._attr_names_to_updated.keys(): - self._set_color( - attr_name, self._attr_names_to_updated[attr_name], updated - ) + MDTextFieldHelperText: + text: "Enter a valid dd/mm/yyyy date" - if self.error_color == [0, 0, 0, 0] or updated: - self.error_color = ( - self.theme_cls.error_color - if self.error_color == [0, 0, 0, 0] - else self.error_color - ) - if self.max_length_text_color == [0, 0, 0, 0] or updated: - self.max_length_text_color = ( - self.theme_cls.disabled_hint_text_color - if self.max_length_text_color == [0, 0, 0, 0] - else self.max_length_text_color - ) + MDTextField: + validator: "date" + date_format: "mm/dd/yyyy" - self._hint_text_color = self.hint_text_color_normal - self._text_color_normal = self.text_color_normal - self._fill_color = self.fill_color_normal - self._icon_right_color = self.icon_right_color_normal - self._icon_left_color = self.icon_left_color_normal - self._max_length_text_color = [0, 0, 0, 0] + MDTextFieldHintText: + text: "Date mm/dd/yyyy without limits" - if self.helper_text_mode in ("on_focus", "on_error"): - self._helper_text_color = [0, 0, 0, 0] - elif self.helper_text_mode == "persistent": - self._helper_text_color = self.helper_text_color_normal + MDTextFieldHelperText: + text: "Enter a valid mm/dd/yyyy date" - self._line_color_normal = self.line_color_normal - self._line_color_focus = self.line_color_focus + MDTextField: + validator: "date" + date_format: "yyyy/mm/dd" - def set_notch_rectangle(self, joining: bool = False) -> None: - """ - Animates a notch for the hint text in the rectangle of the text field - of type `rectangle`. - """ + MDTextFieldHintText: + text: "Date yyyy/mm/dd without limits" - def on_progress(*args): - self._line_blank_space_right_point = ( - self._hint_text_label.width + dp(17) if not joining else 0 - ) + MDTextFieldHelperText: + text: "Enter a valid yyyy/mm/dd date" - if self.hint_text: - animation = Animation( - _line_blank_space_left_point=self._hint_text_label.x - dp(-7) - if not joining - else 0, - duration=0.2, - t="out_quad", - ) - animation.bind(on_progress=on_progress) - animation.start(self) + MDTextField: + validator: "date" + date_format: "dd/mm/yyyy" + date_interval: "01/01/1900", "01/01/2100" - def set_active_underline_width(self, width: Union[float, int]) -> None: - """Animates the width of the active underline line.""" + MDTextFieldHintText: + text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval" - Animation( - _underline_width=width, - duration=(0.2 if self.line_anim else 0), - t="out_quad", - ).start(self) + MDTextFieldHelperText: + text: "Enter a valid dd/mm/yyyy date" - def set_static_underline_color(self, color: list) -> None: - """Animates the color of a static underline line.""" + MDTextField: + validator: "date" + date_format: "dd/mm/yyyy" + date_interval: "01/01/1900", None - Animation( - _line_color_normal=color, - duration=(0.2 if self.line_anim else 0), - t="out_quad", - ).start(self) + MDTextFieldHintText: + text: "Date dd/mm/yyyy in [01/01/1900, None] interval" - def set_active_underline_color(self, color: list) -> None: - """Animates the fill color for 'fill' mode.""" + MDTextFieldHelperText: + text: "Enter a valid dd/mm/yyyy date" - Animation(_line_color_focus=color, duration=0.2, t="out_quad").start( - self - ) + MDTextField: + validator: "date" + date_format: "dd/mm/yyyy" + date_interval: None, "01/01/2100" - def set_fill_color(self, color: list) -> None: - """Animates the color of the hint text.""" + MDTextFieldHintText: + text: "Date dd/mm/yyyy in [None, 01/01/2100] interval" - Animation(_fill_color=color, duration=0.2, t="out_quad").start(self) + MDTextFieldHelperText: + text: "Enter a valid dd/mm/yyyy date" + ''' - def set_helper_text_color(self, color: list) -> None: - """Animates the color of the hint text.""" - Animation(_helper_text_color=color, duration=0.2, t="out_quad").start( - self - ) + class Example(MDApp): + def build(self): + self.theme_cls.primary_palette = "Olive" + return Builder.load_string(KV) - def set_max_length_text_color(self, color: list) -> None: - """Animates the color of the max length text.""" - Animation( - _max_length_text_color=color, duration=0.2, t="out_quad" - ).start(self) + Example().run() - def set_icon_right_color(self, color: list) -> None: - """Animates the color of the icon right.""" + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-validator-date.png + :align: center - Animation(_icon_right_color=color, duration=0.2, t="out_quad").start( - self - ) + :attr:`validator` is an :class:`~kivy.properties.OptionProperty` + and defaults to `None`. + """ - def set_icon_left_color(self, color: list) -> None: - """Animates the color of the icon left.""" + # Helper text label object. + _helper_text_label = ObjectProperty() + # Hint text label object. + _hint_text_label = ObjectProperty() + # Leading icon object. + _leading_icon = ObjectProperty() + # Trailing icon object. + _trailing_icon = ObjectProperty() + # Max length label object. + _max_length_label = ObjectProperty() + # Maximum length of characters to be input. + _max_length = "0" + # Active indicator height. + _indicator_height = NumericProperty(dp(1)) + # Outline height. + _outline_height = NumericProperty(dp(1)) + # The x-axis position of the hint text in the text field. + _hint_x = NumericProperty(0) + # The y-axis position of the hint text in the text field. + _hint_y = NumericProperty(0) + # The right/left lines coordinates of the text field in 'outlined' mode. + _left_x_axis_pos = NumericProperty(dp(32)) + _right_x_axis_pos = NumericProperty(dp(32)) - Animation(_icon_left_color=color, duration=0.2, t="out_quad").start( - self + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bind(text=self.set_text) + self.theme_cls.bind( + primary_palette=self.update_colors, + theme_style=self.update_colors, ) + Clock.schedule_once(self._check_text) - def set_hint_text_color(self, focus: bool, error: bool = False) -> None: - """Animates the color of the hint text.""" + def update_colors( + self, theme_manager: ThemeManager, theme_color: str + ) -> None: + """Fired when the `primary_palette` or `theme_style` value changes.""" + + def update_colors(*args): + self.on_focus(self, self.focus) + + Clock.schedule_once(update_colors, 1) + + def add_widget(self, widget, index=0, canvas=None): + if isinstance(widget, MDTextFieldHelperText): + self._helper_text_label = widget + if isinstance(widget, MDTextFieldHintText): + self._hint_text_label = widget + if isinstance(widget, MDTextFieldLeadingIcon): + self._leading_icon = widget + if isinstance(widget, MDTextFieldTrailingIcon): + self._trailing_icon = widget + if isinstance(widget, MDTextFieldMaxLengthText): + self._max_length_label = widget + else: + return super().add_widget(widget) - if self.mode != "round": - Animation( - _hint_text_color=( - self.hint_text_color_normal - if not focus - else self.hint_text_color_focus - ) - if not error - else self.error_color, - duration=0.2, - t="out_quad", - ).start(self) + def set_texture_color( + self, texture, canvas_group, color: list, error: bool = False + ) -> None: + """ + Animates the color of the + leading/trailing icons/hint/helper/max length text. + """ - def set_pos_hint_text(self, y: float, x: float = 12) -> None: + def update_hint_text_rectangle(*args): + hint_text_rectangle = self.canvas.after.get_group( + "hint-text-rectangle" + )[0] + hint_text_rectangle.texture = None + texture.texture_update() + hint_text_rectangle.texture = texture.texture + + if texture: + Animation(rgba=color, d=0).start(canvas_group) + a = Animation(color=color, d=0) + if texture is self._hint_text_label: + a.bind(on_complete=update_hint_text_rectangle) + a.start(texture) + + def set_pos_hint_text(self, y: float, x: float) -> None: """Animates the x-axis width and y-axis height of the hint text.""" - if self.mode != "round": - Animation(_hint_y=y, duration=0.2, t="out_quad").start(self) - if self.mode == "rectangle": - if not self.icon_left: - _hint_x = x - else: - if y == dp(10): - _hint_x = dp(-4) - else: - _hint_x = dp(20) - - Animation( - _hint_x=_hint_x, - duration=0.2, - t="out_quad", - ).start(self) - elif self.mode == "fill": - Animation( - _hint_x=dp(16) if not self.icon_left else dp(36), - duration=0.2, - t="out_quad", - ).start(self) - elif self.mode == "line": - Animation( - _hint_x=dp(0) if not self.icon_left else dp(36), - duration=0.2, - t="out_quad", - ).start(self) - - def set_hint_text_font_size(self, font_size: float) -> None: + Animation(_hint_y=y, _hint_x=x, d=0.2, t="out_quad").start(self) + + def set_hint_text_font_size(self) -> None: """Animates the font size of the hint text.""" - if self.mode != "round": - Animation( - _hint_text_font_size=font_size, duration=0.2, t="out_quad" - ).start(self) + Animation( + size=self._hint_text_label.texture_size, d=0.2, t="out_quad" + ).start(self.canvas.after.get_group("hint-text-rectangle")[0]) + + def set_space_in_line( + self, left_width: float | int, right_width: float | int + ) -> None: + """ + Animates the length of the right line of the text field for the + hint text. + """ + + Animation(_left_x_axis_pos=left_width, d=0.2, t="out_quad").start(self) + Animation(_right_x_axis_pos=right_width, d=0.2, t="out_quad").start( + self + ) def set_max_text_length(self) -> None: - """Called when text is entered into a text field.""" + """ + Fired when text is entered into a text field. + Set max length text and updated max length texture. + """ - if self.max_text_length: + if self._max_length_label: + self._max_length_label.text = "" self._max_length_label.text = ( - f"{len(self.text)}/{self.max_text_length}" + f"{len(self.text)}/{self._max_length_label.max_text_length}" + ) + self._max_length_label.texture_update() + max_length_rect = self.canvas.before.get_group("max-length-rect")[0] + max_length_rect.texture = None + max_length_rect.texture = self._max_length_label.texture + max_length_rect.size = self._max_length_label.texture_size + max_length_rect.pos = ( + (self.x + self.width) + - (self._max_length_label.texture_size[0] + dp(16)), + self.y - dp(18), ) - def check_text(self, interval: Union[float, int]) -> None: - self.set_text(self, self.text) - - def set_text(self, instance_text_field, text: str) -> None: - """Called when text is entered into a text field.""" + def set_text(self, instance, text: str) -> None: + """Fired when text is entered into a text field.""" self.text = re.sub("\n", " ", text) if not self.multiline else text self.set_max_text_length() - if self.validator and self.validator == "phone": - pass - # self.format(self.text) - if (self.text and self.max_length_text_color) or self._get_has_error(): + if self.text and self._get_has_error() or self._get_has_error(): self.error = True - if ( - self.text - and self.max_length_text_color - and not self._get_has_error() - ): + elif self.text and not self._get_has_error(): self.error = False # Start the appropriate texture animations when programmatically # pasting text into a text field. if len(self.text) != 0 and not self.focus: - self.set_pos_hint_text( - (dp(28) if self.mode != "line" else dp(18)) - if self.mode != "rectangle" - else dp(10) - ) - - self.set_hint_text_font_size(sp(12)) - if self.mode == "rectangle": - self.set_notch_rectangle() + if self._hint_text_label: + self._hint_text_label.font_size = theme_font_styles[ + self._hint_text_label.font_style + ]["small"]["font-size"] + self._hint_text_label.texture_update() + self.set_hint_text_font_size() if (not self.text and not self.focus) or (self.text and not self.focus): - self.on_focus(instance_text_field, False) - - if self.mode == "round" and self.text: - self.hint_text = "" - if self.mode == "round" and not self.text: - self.hint_text = self.__hint_text - - def set_x_pos(self): - pass - - def set_objects_labels(self) -> None: - """ - Creates labels objects for the parameters`helper_text`,`hint_text`, - etc. - """ - - self._helper_text_label = TextfieldLabel( - font_style="Caption", - halign="left", - valign="middle", - field=self, - font_name=self.font_name_helper_text, - ) - self._max_length_label = TextfieldLabel( - font_style="Caption", - halign="right", - valign="middle", - text="", - field=self, - ) - self._hint_text_label = TextfieldLabel( - font_style="Subtitle1", halign="left", valign="middle", field=self - ) - self._icon_right_label = MDIcon(theme_text_color="Custom") - self._icon_left_label = MDIcon(theme_text_color="Custom") + self.on_focus(instance, False) - def on_helper_text(self, instance_text_field, helper_text: str) -> None: - self._helper_text_label.text = helper_text - - def on_focus(self, instance_text_field, focus: bool) -> None: - # TODO: See `cancel_all_animations_on_double_click` method. - # self.cancel_all_animations_on_double_click() + def on_focus(self, instance, focus: bool) -> None: + """Fired when the `focus` value changes.""" if focus: - if self.mode == "rectangle": - self.set_notch_rectangle() - self.set_static_underline_color([0, 0, 0, 0]) - if ( - self.helper_text_mode in ("on_focus", "persistent") - and self.helper_text - ): + if self.mode == "filled": + Animation(_indicator_height=dp(1.25), d=0).start(self) + else: + Animation(_outline_height=dp(1.25), d=0).start(self) + + if self._trailing_icon: Clock.schedule_once( - lambda x: self.set_helper_text_color( - self.helper_text_color_focus + lambda x: self.set_texture_color( + self._trailing_icon, + self.canvas.before.get_group("trailing-icons-color")[0], + ( + self.theme_cls.onSurfaceVariantColor + if self._trailing_icon.theme_icon_color == "Primary" + or not self._trailing_icon.icon_color_focus + else self._trailing_icon.icon_color_focus + ) + if not self.error + else self._get_error_color(), ) ) - if self.mode == "fill": + if self._leading_icon: Clock.schedule_once( - lambda x: self.set_fill_color(self.fill_color_focus) + lambda x: self.set_texture_color( + self._leading_icon, + self.canvas.before.get_group("leading-icons-color")[0], + self.theme_cls.onSurfaceVariantColor + if self._leading_icon.theme_icon_color == "Primary" + or not self._leading_icon.icon_color_focus + else self._leading_icon.icon_color_focus, + ) ) - self.set_active_underline_width(self.width) - - self.set_pos_hint_text( - (dp(28) if self.mode != "line" else dp(18)) - if self.mode != "rectangle" - else dp(10) - ) - Clock.schedule_once(lambda x: self.set_hint_text_color(focus)) - self.set_hint_text_font_size(sp(12)) - - if self.max_text_length: + if self._max_length_label and not self.error: Clock.schedule_once( - lambda x: self.set_max_length_text_color( - self.max_length_text_color + lambda x: self.set_texture_color( + self._max_length_label, + self.canvas.before.get_group("max-length-color")[0], + self.theme_cls.onSurfaceVariantColor + if not self._max_length_label.text_color_focus + else self._max_length_label.text_color_focus, ) ) - if self.icon_right: + + if self._helper_text_label and self._helper_text_label.mode in ( + "on_focus", + "persistent", + ): Clock.schedule_once( - lambda x: self.set_icon_right_color( - self.icon_right_color_focus + lambda x: self.set_texture_color( + self._helper_text_label, + self.canvas.before.get_group("helper-text-color")[0], + ( + self.theme_cls.onSurfaceVariantColor + if not self._helper_text_label.text_color_focus + else self._helper_text_label.text_color_focus + ) + if not self.error + else self._get_error_color(), ) ) - if self.icon_left: + if ( + self._helper_text_label + and self._helper_text_label.mode == "on_error" + and not self.error + ): Clock.schedule_once( - lambda x: self.set_icon_left_color( - self.icon_left_color_focus + lambda x: self.set_texture_color( + self._helper_text_label, + self.canvas.before.get_group("helper-text-color")[0], + self.theme_cls.transparentColor, ) ) - - if self.error: - if self.hint_text: - Clock.schedule_once( - lambda x: self.set_hint_text_color(focus, self.error) - ) - if self.helper_text: - Clock.schedule_once( - lambda x: self.set_helper_text_color(self.error_color) - ) - if self.max_text_length: - Clock.schedule_once( - lambda x: self.set_max_length_text_color( - self.error_color + if self._hint_text_label: + Clock.schedule_once( + lambda x: self.set_texture_color( + self._hint_text_label, + self.canvas.after.get_group("hint-text-color")[0], + ( + self.theme_cls.primaryColor + if not self._hint_text_label.text_color_focus + else self._hint_text_label.text_color_focus ) + if not self.error + else self._get_error_color(), ) - if self.icon_right: - Clock.schedule_once( - lambda x: self.set_icon_right_color(self.error_color) + ) + self.set_pos_hint_text( + 0 if self.mode != "outlined" else dp(-14), + ( + -(self._leading_icon.texture_size[0] + dp(12)) + if self._leading_icon + else 0 ) - if self.icon_left: - Clock.schedule_once( - lambda x: self.set_icon_left_color(self.error_color) + if self.mode == "outlined" + else -(self._leading_icon.texture_size[0] - dp(24)), + ) + self._hint_text_label.font_size = theme_font_styles[ + self._hint_text_label.font_style + ]["small"]["font-size"] + self._hint_text_label.texture_update() + self.set_hint_text_font_size() + if self.mode == "outlined": + self.set_space_in_line( + dp(14), self._hint_text_label.texture_size[0] + dp(18) ) else: - if self.helper_text_mode == "persistent" and self.helper_text: + if self.mode == "filled": + Animation(_indicator_height=dp(1), d=0).start(self) + else: + Animation(_outline_height=dp(1), d=0).start(self) + + if self._leading_icon: Clock.schedule_once( - lambda x: self.set_helper_text_color( - self.helper_text_color_normal + lambda x: self.set_texture_color( + self._leading_icon, + self.canvas.before.get_group("leading-icons-color")[0], + self.theme_cls.onSurfaceVariantColor + if self._leading_icon.theme_icon_color == "Primary" + or not self._leading_icon.icon_color_normal + else self._leading_icon.icon_color_normal, ) ) - if self.helper_text_mode == "on_focus" and self.helper_text: - Clock.schedule_once( - lambda x: self.set_helper_text_color([0.0, 0.0, 0.0, 0.0]) - ) - if self.mode == "rectangle" and not self.text: - self.set_notch_rectangle(joining=True) - if not self.text: - if self.mode == "rectangle": - y = dp(38) - elif self.mode == "fill": - y = dp(46) - else: - y = dp(34) - - self.set_pos_hint_text(y) - self.set_hint_text_font_size(sp(16)) - if self.icon_right and not self.error: + if self._trailing_icon: Clock.schedule_once( - lambda x: self.set_icon_right_color( - self.icon_right_color_normal + lambda x: self.set_texture_color( + self._trailing_icon, + self.canvas.before.get_group("trailing-icons-color")[0], + ( + self.theme_cls.onSurfaceVariantColor + if self._trailing_icon.theme_icon_color == "Primary" + or not self._trailing_icon.icon_color_normal + else self._trailing_icon.icon_color_normal + ) + if not self.error + else self._get_error_color(), ) ) - if self.icon_left and not self.error: + if self._max_length_label and not self.error: Clock.schedule_once( - lambda x: self.set_icon_left_color( - self.icon_left_color_normal + lambda x: self.set_texture_color( + self._max_length_label, + self.canvas.before.get_group("max-length-color")[0], + self.theme_cls.onSurfaceVariantColor + if not self._max_length_label.text_color_normal + else self._max_length_label.text_color_normal, ) ) - if self.hint_text: + if ( + self._helper_text_label + and self._helper_text_label.mode == "on_focus" + ): Clock.schedule_once( - lambda x: self.set_hint_text_color(focus, self.error) + lambda x: self.set_texture_color( + self._helper_text_label, + self.canvas.before.get_group("helper-text-color")[0], + self.theme_cls.transparentColor, + ) ) - - self.set_active_underline_width(0) - Clock.schedule_once( - lambda x: self.set_max_length_text_color([0, 0, 0, 0]) - ) - - if self.mode == "fill": + elif ( + self._helper_text_label + and self._helper_text_label.mode == "persistent" + ): Clock.schedule_once( - lambda x: self.set_fill_color(self.fill_color_normal) + lambda x: self.set_texture_color( + self._helper_text_label, + self.canvas.before.get_group("helper-text-color")[0], + ( + self.theme_cls.onSurfaceVariantColor + if not self._helper_text_label.text_color_normal + else self._helper_text_label.text_color_normal + ) + if not self.error + else self._get_error_color(), + ) ) - self.error = self._get_has_error() or self.error - if self.error: - self.set_static_underline_color(self.error_color) + if not self.text: + if self._hint_text_label: + if self.mode == "outlined": + self.set_space_in_line(dp(32), dp(32)) + self._hint_text_label.font_size = theme_font_styles[ + self._hint_text_label.font_style + ]["large"]["font-size"] + self._hint_text_label.texture_update() + self.set_hint_text_font_size() + self.set_pos_hint_text( + (self.height / 2) + - (self._hint_text_label.texture_size[1] / 2), + 0, + ) else: - Clock.schedule_once( - lambda x: self.set_static_underline_color( - self.line_color_normal + if self._hint_text_label: + if self.mode == "outlined": + self.set_space_in_line( + dp(14), + self._hint_text_label.texture_size[0] + dp(18), + ) + self.set_pos_hint_text( + 0 if self.mode != "outlined" else dp(-14), + ( + -(self._leading_icon.texture_size[0] + dp(12)) + if self._leading_icon + else 0 + ) + if self.mode == "outlined" + else -(self._leading_icon.texture_size[0] - dp(24)), ) + + if self._hint_text_label: + Clock.schedule_once( + lambda x: self.set_texture_color( + self._hint_text_label, + self.canvas.after.get_group("hint-text-color")[0], + ( + self.theme_cls.onSurfaceVariantColor + if not self._hint_text_label.text_color_normal + else self._hint_text_label.text_color_normal + ) + if not self.error + else self._get_error_color(), + ), ) - def on_icon_left(self, instance_text_field, icon_name: str) -> None: - self._icon_left_label.icon = icon_name + def on_disabled(self, instance, disabled: bool) -> None: + """Fired when the `disabled` value changes.""" - def on_icon_right(self, instance_text_field, icon_name: str) -> None: - self._icon_right_label.icon = icon_name + super().on_disabled(instance, disabled) - def on_disabled(self, instance_text_field, disabled_value: bool) -> None: - pass + if self._max_length_label and disabled: + Clock.schedule_once( + lambda x: self.set_texture_color( + self._max_length_label, + self.canvas.before.get_group("max-length-color")[0], + ( + self._max_length_label.color + if not self.error + else self.theme_cls.disabledTextColor + )[:-1] + + [self.text_field_opacity_value_disabled_max_length_label], + ) + ) + elif self._max_length_label and not disabled: + Clock.schedule_once( + lambda x: self.set_texture_color( + self._max_length_label, + self.canvas.before.get_group("max-length-color")[0], + ( + self._max_length_label.color + if not self.error + else self._get_error_color() + )[:-1] + + [1], + ) + ) - def on_error(self, instance_text_field, error: bool) -> None: + def on_error(self, instance, error: bool) -> None: """ Changes the primary colors of the text box to match the `error` value (text field is in an error state or not). """ if error: - Clock.schedule_once( - lambda x: self.set_max_length_text_color(self.error_color) - ) - self.set_active_underline_color(self.error_color) - if self.hint_text: - self.set_hint_text_color(self.focus, self.error) - if self.helper_text: - Clock.schedule_once( - lambda x: self.set_helper_text_color(self.error_color) - ) - if self.icon_right: - Clock.schedule_once( - lambda x: self.set_icon_right_color(self.error_color) - ) - if self.icon_left: - Clock.schedule_once( - lambda x: self.set_icon_left_color(self.error_color) - ) - if self.helper_text_mode == "on_error": - Clock.schedule_once( - lambda x: self.set_helper_text_color(self.error_color) - ) - else: - Clock.schedule_once( - lambda x: self.set_max_length_text_color( - self.max_length_text_color - ) - ) - self.set_active_underline_color(self.line_color_focus) - if self.hint_text: - self.set_hint_text_color(self.focus) - if self.helper_text: + if self._max_length_label: Clock.schedule_once( - lambda x: self.set_helper_text_color( - self.helper_text_color_focus + lambda x: self.set_texture_color( + self._max_length_label, + self.canvas.before.get_group("max-length-color")[0], + self._get_error_color(), ) ) - if self.icon_right: + if self._hint_text_label: Clock.schedule_once( - lambda x: self.set_icon_right_color( - self.icon_right_color_focus - ) + lambda x: self.set_texture_color( + self._hint_text_label, + self.canvas.after.get_group("hint-text-color")[0], + self._get_error_color(), + ), ) - if self.icon_left: + if self._helper_text_label and self._helper_text_label.mode in ( + "persistent", + "on_error", + ): Clock.schedule_once( - lambda x: self.set_icon_left_color( - self.icon_left_color_focus + lambda x: self.set_texture_color( + self._helper_text_label, + self.canvas.before.get_group("helper-text-color")[0], + self._get_error_color(), ) ) - if self.helper_text_mode == "persistent": + if self._trailing_icon: Clock.schedule_once( - lambda x: self.set_helper_text_color( - self.helper_text_color_normal + lambda x: self.set_texture_color( + self._trailing_icon, + self.canvas.before.get_group("trailing-icons-color")[0], + self._get_error_color(), ) ) + else: + self.on_focus(self, self.focus) - def on_hint_text(self, instance_text_field, hint_text: str) -> None: - if hint_text: - self.__hint_text = hint_text - self._hint_text_label.text = hint_text - self._hint_text_label.font_size = sp(16) - - def on_width(self, instance_text_field, width: float) -> None: - """Called when the application window is resized.""" - - if self.focus: - self._underline_width = self.width - - def on_height(self, instance_text_field, value_height: float) -> None: + def on_height(self, instance, value_height: float) -> None: if value_height >= self.max_height and self.max_height: self.height = self.max_height - def on_text_color_normal( - self, instance_text_field, color: Union[list, str] - ) -> None: - self._text_color_normal = color - - def on_hint_text_color_normal( - self, instance_text_field, color: Union[list, str] - ) -> None: - self._hint_text_color = color - - def on_helper_text_color_normal( - self, instance_text_field, color: Union[list, str] - ) -> None: - self._helper_text_color = color - - def on_icon_right_color_normal( - self, instance_text_field, color: Union[list, str] - ) -> None: - self._icon_right_color = color - - def on_line_color_normal( - self, instance_text_field, color: Union[list, str] - ) -> None: - self._line_color_normal = color - - def on_max_length_text_color( - self, instance_text_field, color: Union[list, str] - ) -> None: - self._max_length_text_color = color - - def _set_color(self, attr_name: str, color: str, updated: bool) -> None: - if attr_name in self._colors_to_updated or updated: - if attr_name in self._colors_to_updated: - setattr(self, attr_name, color) - - def _set_attr_names_to_updated(self, interval: Union[float, int]) -> None: - """ - Sets and update the default color dictionary for text field textures. - """ - - self._attr_names_to_updated = { - "line_color_normal": self.theme_cls.disabled_hint_text_color, - "line_color_focus": self.theme_cls.primary_color, - "hint_text_color_normal": self.theme_cls.disabled_hint_text_color, - "hint_text_color_focus": self.theme_cls.primary_color, - "helper_text_color_normal": self.theme_cls.disabled_hint_text_color, - "helper_text_color_focus": self.theme_cls.disabled_hint_text_color, - "text_color_normal": self.theme_cls.disabled_hint_text_color, - "text_color_focus": self.theme_cls.primary_color, - "fill_color_normal": self.theme_cls.bg_darkest, - "fill_color_focus": self.theme_cls.bg_dark, - "icon_right_color_normal": self.theme_cls.disabled_hint_text_color, - "icon_right_color_focus": self.theme_cls.primary_color, - "icon_left_color_normal": self.theme_cls.disabled_hint_text_color, - "icon_left_color_focus": self.theme_cls.primary_color, - } - def _get_has_error(self) -> bool: """ Returns `False` or `True` depending on the state of the text field, @@ -1890,7 +1664,10 @@ def _get_has_error(self) -> bool: "time": self.is_time_valid, }[self.validator](self.text) return has_error - if self.max_text_length and len(self.text) > self.max_text_length: + if ( + self._max_length_label + and len(self.text) > self._max_length_label.max_text_length + ): has_error = True else: if all((self.required, len(self.text) == 0)): @@ -1899,91 +1676,15 @@ def _get_has_error(self) -> bool: has_error = False return has_error - def _refresh_hint_text(self): - """Method override to avoid duplicate hint text texture.""" - - -if __name__ == "__main__": - from kivy.lang import Builder - from kivy.uix.textinput import TextInput - - from kivymd.app import MDApp - - KV = """ -MDScreen: - - MDScrollView: - - MDList: - id: box - spacing: "32dp" - padding: "56dp", "12dp", "56dp", "12dp" - - MDTextField: - hint_text: "Label" - helper_text: "Error message" - mode: "rectangle" - max_text_length: 5 - - MDTextField: - icon_left: "git" - hint_text: "Label" - helper_text: "Error message" - mode: "rectangle" - - MDTextField: - icon_left: "git" - hint_text: "Label" - helper_text: "Error message" - mode: "fill" - - MDTextField: - hint_text: "Label" - helper_text: "Error message" - mode: "fill" - - MDTextField: - hint_text: "Label" - helper_text: "Error message" - - MDTextField: - icon_left: "git" - hint_text: "Label" - helper_text: "Error message" - - MDTextField: - hint_text: "Round mode" - mode: "round" - max_text_length: 15 - helper_text: "Message" - - MDTextField: - hint_text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval" - helper_text: "Enter a valid dd/mm/yyyy date" - validator: "date" - date_format: "dd/mm/yyyy" - date_interval: "01/01/1900", "01/01/2100" - - MDTextField: - hint_text: "Email" - helper_text: "user@gmail.com" - validator: "email" - - MDFlatButton: - text: "SET TEXT" - pos_hint: {"center_x": .5} - on_release: app.set_text() -""" - - class Test(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) + def _get_error_color(self): + return ( + self.theme_cls.errorColor + if not self.error_color + else self.error_color + ) - def set_text(self): - for widget in self.root.ids.box.children: - if issubclass(widget.__class__, TextInput): - widget.text = "Input text" + def _check_text(self, *args) -> None: + self.set_text(self, self.text) - Test().run() + def _refresh_hint_text(self): + """Method override to avoid duplicate hint text texture.""" diff --git a/kivymd/uix/toolbar/__init__.py b/kivymd/uix/toolbar/__init__.py deleted file mode 100644 index e4f163e91..000000000 --- a/kivymd/uix/toolbar/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# NOQA F401 -from .toolbar import ( - MDActionBottomAppBarButton, - MDActionOverFlowButton, - MDBottomAppBar, - MDFabBottomAppBarButton, - MDTopAppBar, -) diff --git a/kivymd/uix/toolbar/toolbar.kv b/kivymd/uix/toolbar/toolbar.kv deleted file mode 100644 index b5538afae..000000000 --- a/kivymd/uix/toolbar/toolbar.kv +++ /dev/null @@ -1,113 +0,0 @@ - - - IconLeftWidget: - icon: root.icon - - - - size_hint_y: None - padding: [root.theme_cls.horizontal_margins - dp(12), 0] - elevation: root.elevation - - canvas: - Color: - rgba: - ( \ - root.theme_cls.primary_color \ - if root.md_bg_color == [0, 0, 0, 0] \ - else root.md_bg_color \ - ) \ - if root.type == "top" else \ - ( \ - ( \ - root.theme_cls.primary_color \ - if not self.md_bg_bottom_color else \ - self.md_bg_bottom_color \ - ) \ - if root.parent and root.parent.md_bg_color == [0, 0, 0, 0] \ - else \ - ( \ - root.parent.md_bg_color if root.parent else root.md_bg_color \ - ) \ - ) - Mesh: - vertices: root._vertices_left - indices: root._indices_left - mode: "triangle_fan" - - Mesh: - vertices: root._vertices_right - indices: root._indices_right - mode: "triangle_fan" - - RoundedRectangle: - pos: root._rectangle_left_pos - size: root._rectangle_left_width, root._rounded_rectangle_height - radius: - [0,] if root.mode == "normal" \ - else [0, root.notch_radius * root._rounding_percentage, 0, 0] - - RoundedRectangle: - pos: root._rectangle_right_pos - size: root._rectangle_right_width, root._rounded_rectangle_height - radius: - [0,] if root.mode == "normal" \ - else [root.notch_radius * root._rounding_percentage, 0, 0, 0] - - - - orientation: "vertical" - - MDBoxLayout: - padding: 0, 0, 0, root.height - headline_box.height - (dp(48) + dp(20)) - - MDBoxLayout: - id: left_actions - orientation: "horizontal" - size_hint_x: None - padding: [0, (self.height - dp(48)) / 2] - - MDLabel: - id: label_title - font_style: "H6" - opposite_colors: root.opposite_colors - theme_text_color: "Custom" if not root.opposite_colors else "Primary" - text_color: root.specific_text_color - text: root.title - shorten: True - shorten_from: "right" - markup: True - padding: dp(12), 0 - halign: - root.anchor_title \ - if root.anchor_title else \ - root.update_anchor_title(app.theme_cls.material_style) - - MDBoxLayout: - id: right_actions - orientation: "horizontal" - adaptive_width: True - padding: [0, (self.height - dp(48)) / 2] - - MDBoxLayout: - id: headline_box - size_hint_y: None - height: label_headline.texture_size[1] if label_headline.text else 0 - padding: "16dp" - - MDLabel: - id: label_headline - adaptive_height: True - shorten: True - shorten_from: "right" - theme_text_color: "Custom" - text_color: - label_title.text_color \ - if not root.headline_text_color else \ - root.headline_text_color - text: - root.headline_text \ - if root.type_height in ("medium", "large") \ - and app.theme_cls.material_style == "M3" \ - and root.type != "bottom" else \ - "" diff --git a/kivymd/uix/toolbar/toolbar.py b/kivymd/uix/toolbar/toolbar.py deleted file mode 100755 index 091b04083..000000000 --- a/kivymd/uix/toolbar/toolbar.py +++ /dev/null @@ -1,2225 +0,0 @@ -""" -Components/Toolbar -================== - -.. seealso:: - - `Material Design spec, App bars: top `_ - - `Material Design spec, App bars: bottom `_ - - `Material Design 3 spec, App bars: top `_ - - `Material Design 3 spec, App bars: bottom `_ - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-top.png - :align: center - -`KivyMD` provides the following bar positions for use: - -- TopAppBar_ -- BottomAppBar_ - -.. TopAppBar_: -TopAppBar ---------- - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDBoxLayout: - orientation: "vertical" - md_bg_color: "#1E1E15" - - MDTopAppBar: - title: "MDTopAppBar" - - MDLabel: - text: "Content" - halign: "center" - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-1.png - :align: center - -Add left menu -------------- - -.. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - anchor_title: "left" - left_action_items: [["menu", lambda x: app.callback()]] - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-2.png - :align: center - -.. note:: - - The callback is optional. ``left_action_items: [["menu"]]`` would also work for a button that does nothing. - -Add right menu --------------- - -.. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - anchor_title: "left" - right_action_items: [["dots-vertical", lambda x: app.callback()]] - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-3.png - :align: center - -Add two item to the right menu ------------------------------- - -.. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - anchor_title: "left" - right_action_items: - [ - ["dots-vertical", lambda x: app.callback_1()], - ["clock", lambda x: app.callback_2()] - ] - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-4.png - :align: center - -Change bar color ----------------- - -.. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - anchor_title: "left" - md_bg_color: "brown" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-5.png - :align: center - -Change bar text color ---------------------- - -.. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - anchor_title: "left" - specific_text_color: "white" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-6.png - :align: center - -Shadow elevation control ------------------------- - -.. code-block:: kv - - MDTopAppBar: - title: "Elevation 4" - anchor_title: "left" - elevation: 4 - shadow_color: "brown" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-7.png - :align: center - -.. BottomAppBar: -BottomAppBar ------------- - -M2 style bottom app bar ------------------------ - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom.png - :align: center - -Usage ------ - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDBoxLayout: - md_bg_color: "#1E1E15" - - # Will always be at the bottom of the screen. - MDBottomAppBar: - - MDTopAppBar: - title: "MDBottomAppBar" - icon: "git" - type: "bottom" - left_action_items: [["menu", lambda x: x]] - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.material_style = "M2" - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-8.png - :align: center - -Event on floating button ------------------------- - -Event ``on_action_button``: - -.. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - title: "MDBottomAppBar" - icon: "git" - type: "bottom" - left_action_items: [["menu", lambda x: x]] - on_action_button: app.callback(self.icon) - -Floating button position ------------------------- - -Mode: - -- `'free-end'` -- `'free-center'` -- `'end'` -- `'center'` - -.. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - title: "MDBottomAppBar" - icon: "git" - type: "bottom" - left_action_items: [["menu", lambda x: x]] - mode: "end" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-9.png - :align: center - -.. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - title: "MDBottomAppBar" - icon: "git" - type: "bottom" - left_action_items: [["menu", lambda x: x]] - mode: "free-end" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-10.png - :align: center - -Custom color ------------- - -.. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - title: "MDBottomAppBar" - icon: "git" - type: "bottom" - left_action_items: [["menu", lambda x: x]] - icon_color: 0, 1, 0, 1 - md_bg_bottom_color: "brown" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-11.png - :align: center - -M3 style bottom app bar ------------------------ - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom-m3.png - :align: center - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDFloatLayout: - md_bg_color: "#151511" - - MDBottomAppBar: - md_bg_color: "#232217" - icon_color: "#8A8D79" - - MDFabBottomAppBarButton: - icon: "plus" - md_bg_color: "#373A22" - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-1.png - :align: center - -Add action items ----------------- - -.. code-block:: kv - - #:import MDActionBottomAppBarButton kivymd.uix.toolbar.MDActionBottomAppBarButton - - - MDFloatLayout: - - MDBottomAppBar: - action_items: - [ - MDActionBottomAppBarButton(icon="gmail"), - MDActionBottomAppBarButton(icon="label-outline"), - MDActionBottomAppBarButton(icon="bookmark"), - ] - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-2.png - :align: center - -Change action items -------------------- - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - #:import MDActionBottomAppBarButton kivymd.uix.toolbar.MDActionBottomAppBarButton - - - MDFloatLayout: - md_bg_color: "#151511" - - MDBottomAppBar: - id: bottom_appbar - md_bg_color: "#232217" - icon_color: "#8A8D79" - action_items: - [ - MDActionBottomAppBarButton(icon="gmail"), - MDActionBottomAppBarButton(icon="bookmark"), - ] - - MDFabBottomAppBarButton: - icon: "plus" - md_bg_color: "#373A22" - on_release: app.change_actions_items() - ''' - - - class Example(MDApp): - def change_actions_items(self): - self.root.ids.bottom_appbar.action_items = [ - MDActionBottomAppBarButton(icon="magnify"), - MDActionBottomAppBarButton(icon="trash-can-outline"), - MDActionBottomAppBarButton(icon="download-box-outline"), - ] - - def build(self): - self.theme_cls.theme_style = "Dark" - return Builder.load_string(KV) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-3.gif - :align: center - -A practical example -------------------- - -.. code-block:: python - - from kivy.clock import Clock - from kivy.lang import Builder - from kivy.properties import StringProperty, BooleanProperty, ObjectProperty - from kivy.uix.behaviors import FocusBehavior - from kivy.uix.recycleboxlayout import RecycleBoxLayout - from kivy.uix.recycleview.layout import LayoutSelectionBehavior - from kivy.uix.recycleview.views import RecycleDataViewBehavior - - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.toolbar import MDActionBottomAppBarButton - from kivymd.app import MDApp - from kivymd.utils import asynckivy - - from faker import Faker # pip install Faker - - KV = ''' - #:import MDFabBottomAppBarButton kivymd.uix.toolbar.MDFabBottomAppBarButton - - - - orientation: "vertical" - adaptive_height: True - md_bg_color: "#373A22" if self.selected else "#1F1E15" - radius: 16 - padding: 0, 0, 0, "16dp" - - TwoLineAvatarListItem: - divider: None - _no_ripple_effect: True - text: root.name - secondary_text: root.time - theme_text_color: "Custom" - text_color: "#8A8D79" - secondary_theme_text_color: self.theme_text_color - secondary_text_color: self.text_color - - ImageLeftWidget: - source: root.avatar - radius: self.height / 2 - - MDLabel: - text: root.text - adaptive_height: True - theme_text_color: "Custom" - text_color: "#8A8D79" - padding_x: "16dp" - shorten: True - shorten_from: "right" - - Widget: - - - MDFloatLayout: - md_bg_color: "#151511" - - RecycleView: - id: card_list - viewclass: "UserCard" - - SelectableRecycleGridLayout: - orientation: 'vertical' - spacing: "16dp" - padding: "16dp" - default_size: None, dp(120) - default_size_hint: 1, None - size_hint_y: None - height: self.minimum_height - multiselect: True - touch_multiselect: True - - MDBottomAppBar: - id: bottom_appbar - scroll_cls: card_list - allow_hidden: True - md_bg_color: "#232217" - icon_color: "#8A8D79" - - MDFabBottomAppBarButton: - id: fab_button - icon: "plus" - md_bg_color: "#373A22" - ''' - - - class UserCard(RecycleDataViewBehavior, MDBoxLayout): - name = StringProperty() - time = StringProperty() - text = StringProperty() - avatar = StringProperty() - callback = ObjectProperty(lambda x: x) - - index = None - selected = BooleanProperty(False) - selectable = BooleanProperty(True) - - def refresh_view_attrs(self, rv, index, data): - self.index = index - return super().refresh_view_attrs(rv, index, data) - - def on_touch_down(self, touch): - if super().on_touch_down(touch): - return True - if self.collide_point(*touch.pos) and self.selectable: - Clock.schedule_once(self.callback) - return self.parent.select_with_touch(self.index, touch) - - def apply_selection(self, rv, index, is_selected): - self.selected = is_selected - rv.data[index]["selected"] = is_selected - - - class SelectableRecycleGridLayout( - FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout - ): - pass - - - class Test(MDApp): - selected_cards = False - - def build(self): - return Builder.load_string(KV) - - def on_tap_card(self, *args): - datas = [data["selected"] for data in self.root.ids.card_list.data] - if True in datas and not self.selected_cards: - self.root.ids.bottom_appbar.action_items = [ - MDActionBottomAppBarButton(icon="gmail"), - MDActionBottomAppBarButton(icon="label-outline"), - MDActionBottomAppBarButton(icon="bookmark"), - ] - self.root.ids.fab_button.icon = "pencil" - self.selected_cards = True - else: - if len(list(set(datas))) == 1 and not list(set(datas))[0]: - self.selected_cards = False - if not self.selected_cards: - self.root.ids.bottom_appbar.action_items = [ - MDActionBottomAppBarButton(icon="magnify"), - MDActionBottomAppBarButton(icon="trash-can-outline"), - MDActionBottomAppBarButton(icon="download-box-outline"), - ] - self.root.ids.fab_button.icon = "plus" - - def on_start(self): - async def generate_card(): - for i in range(10): - await asynckivy.sleep(0) - self.root.ids.card_list.data.append( - { - "name": fake.name(), - "time": fake.date(), - "avatar": fake.image_url(), - "text": fake.text(), - "selected": False, - "callback": self.on_tap_card, - } - ) - - self.on_tap_card() - fake = Faker() - Clock.schedule_once(lambda x: asynckivy.start(generate_card())) - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-4.gif - :align: center - -Tooltips --------- - -You can add MDTooltips to the icons by adding a text string to the bar item, -as shown below: - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.snackbar import Snackbar - - KV = ''' - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - title: "MDTopAppBar" - left_action_items: [["menu", "This is the navigation"]] - right_action_items: - [ - [ - "dots-vertical", - lambda x: app.callback(x), - "this is the More Actions" - ] - ] - - MDLabel: - text: "Content" - halign: "center" - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.material_style = "M2" - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) - - def callback(self, button): - Snackbar(text="Hello World").open() - - - Example().run() - -M3 style top app bar --------------------- - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.toolbar import MDTopAppBar - - KV = ''' - MDScreen: - - MDBoxLayout: - id: box - orientation: "vertical" - spacing: "12dp" - pos_hint: {"top": 1} - adaptive_height: True - ''' - - - class Example(MDApp): - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) - - def on_start(self): - for type_height in ["medium", "large", "small"]: - self.root.ids.box.add_widget( - MDTopAppBar( - type_height=type_height, - headline_text=f"Headline {type_height.lower()}", - md_bg_color="brown", - left_action_items=[["arrow-left", lambda x: x]], - right_action_items=[ - ["attachment", lambda x: x], - ["calendar", lambda x: x], - ["dots-vertical", lambda x: x], - ], - title="Title" if type_height == "small" else "", - anchor_title="left", - ) - ) - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-m3.png - :align: center -""" - -from __future__ import annotations - -__all__ = ( - "MDTopAppBar", - "MDBottomAppBar", - "MDActionBottomAppBarButton", - "MDFabBottomAppBarButton", - "MDActionOverFlowButton", -) - -import os -from math import cos, radians, sin -from typing import Union - -from kivy import Logger -from kivy.animation import Animation -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 ( - BooleanProperty, - ColorProperty, - ListProperty, - NumericProperty, - ObjectProperty, - OptionProperty, - StringProperty, -) -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.floatlayout import FloatLayout -from kivy.uix.scrollview import ScrollView - -from kivymd import uix_path -from kivymd.color_definitions import text_colors -from kivymd.material_resources import TOP_APP_BAR_ELEVATION -from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import ( - CommonElevationBehavior, - DeclarativeBehavior, - RotateBehavior, - ScaleBehavior, - SpecificBackgroundColorBehavior, -) -from kivymd.uix.button import MDFloatingActionButton, MDIconButton -from kivymd.uix.controllers import WindowController -from kivymd.uix.list import OneLineIconListItem -from kivymd.uix.menu import MDDropdownMenu -from kivymd.uix.tooltip import MDTooltip -from kivymd.utils import asynckivy -from kivymd.utils.set_bars_colors import set_bars_colors - -with open( - os.path.join(uix_path, "toolbar", "toolbar.kv"), encoding="utf-8" -) as kv_file: - Builder.load_string(kv_file.read()) - - -class MDFabBottomAppBarButton( - MDFloatingActionButton, RotateBehavior, ScaleBehavior, MDTooltip -): - """ - Implements a floating action button (FAB) for a bar with type 'bottom'. - - For more information, see in the - :class:`~kivymd.uix.button.MDFloatingActionButton` and - :class:`~kivymd.uix.behaviors.RotateBehavior` and - :class:`~kivymd.uix.behaviors.ScaleBehavior` and - :class:`~kivymd.uix.tooltip.MDTooltip` - classes documentation. - """ - - def set__radius(self, *args) -> None: - super().set__radius() - if self.theme_cls.material_style == "M3": - self.elevation = 0 - - -class ActionTopAppBarButton(MDIconButton, MDTooltip): - """ - Implements action buttons on the bar. - - For more information, see in the - :class:`~kivymd.uix.button.MDIconButton` and - :class:`~kivymd.uix.tooltip.MDTooltip` - classes documentation. - """ - - # The text of the menu item of the corresponding action button that will - # be displayed in the `OverFlowMenu` menu. - overflow_text = StringProperty() - - -class MDActionBottomAppBarButton(ActionTopAppBarButton): - """ - Implements action buttons for a :class:'MDBottomAppBar' class. - - .. versionadded:: 1.2.0 - - For more information, see in the - :class:`~kivymd.uix.button.MDIconButton` and - :class:`~kivymd.uix.tooltip.MDTooltip` - classes documentation. - """ - - -class MDActionOverFlowButton(ActionTopAppBarButton): - """ - Implements a bar action button for the `OverFlowMenu` menu. - - For more information, see in the - :class:`~kivymd.uix.button.MDIconButton` and - :class:`~kivymd.uix.tooltip.MDTooltip` - classes documentation. - """ - - icon = "dots-vertical" - - -class OverFlowMenu(MDDropdownMenu): - """ - Implements a menu for the items (:class:`~OverFlowMenuItem`) of the - corresponding action buttons. - """ - - -class OverFlowMenuItem(OneLineIconListItem): - """Implements a menu (:class:`~OverFlowMenu`) item.""" - - icon = StringProperty() - - -class NotchedBox( - ThemableBehavior, - CommonElevationBehavior, - SpecificBackgroundColorBehavior, - BoxLayout, -): - elevation = NumericProperty(TOP_APP_BAR_ELEVATION) - notch_radius = NumericProperty() - notch_center_x = NumericProperty("100dp") - - _indices_right = ListProperty() - _vertices_right = ListProperty() - _indices_left = ListProperty() - _vertices_left = ListProperty() - _rounded_rectangle_height = NumericProperty("6dp") - _total_angle = NumericProperty(180) - _rectangle_left_pos = ListProperty([0, 0]) - _rectangle_left_width = NumericProperty() - _rectangle_right_pos = ListProperty([0, 0]) - _rectangle_right_width = NumericProperty() - _rounding_percentage = NumericProperty(0.15) - _shift = NumericProperty(dp(4)) - - def __init__(self, **kw): - super().__init__(**kw) - self.bind( - size=self._update_canvas, - pos=self._update_canvas, - notch_radius=self._update_canvas, - notch_center_x=self._update_canvas, - ) - Clock.schedule_once(self._update_canvas) - - def _update_canvas(self, *args): - pos = self.pos - size = [ - self.width, - self.size[1] - self._rounded_rectangle_height / 2, - ] - notch_center_x = self.pos[0] + self.notch_center_x - circle_radius = self.notch_radius - degree_diff = int((180 - self._total_angle) / 2) - circle_center = [notch_center_x, pos[1] + size[1]] - left_circle_pos = self._points_on_circle( - circle_center, circle_radius, 180 + degree_diff, 270 - ) - - self._rectangle_left_pos = [ - pos[0], - pos[1] + size[1] - self._rounded_rectangle_height / 2, - ] - self._rectangle_left_width = left_circle_pos[0][0] - self.pos[0] - - right_circle_pos = self._points_on_circle( - circle_center, circle_radius, -degree_diff, -90 - ) - - self._rectangle_right_pos = [ - right_circle_pos[0][0], - pos[1] + size[1] - self._rounded_rectangle_height / 2, - ] - self._rectangle_right_width = pos[0] + size[0] - right_circle_pos[0][0] - - raw_vertices_left = self._make_vertices( - pos, [notch_center_x - pos[0], size[1]], "left", left_circle_pos - ) - raw_vertices_right = self._make_vertices( - [notch_center_x, pos[1]], - [size[0] + pos[0] - notch_center_x, size[1]], - "right", - right_circle_pos, - ) - - left_vertices, left_indices = self._make_vertices_indices( - raw_vertices_left - ) - right_vertices, right_indices = self._make_vertices_indices( - raw_vertices_right - ) - - self._update_mesh(left_vertices, left_indices, "left") - self._update_mesh(right_vertices, right_indices, "right") - - def _update_mesh(self, vertices, indices, mode): - if mode == "left": - self._indices_left = indices - self._vertices_left = vertices - else: - self._indices_right = indices - self._vertices_right = vertices - return True - - @staticmethod - def _make_vertices_indices(points_list): - vertices = [] - indices = [] - for index, point in enumerate(points_list): - indices.append(index) - vertices.extend([point[0], point[1], 0, 0]) - - return [vertices, indices] - - @staticmethod - def _make_vertices(rectangle_pos, rectangle_size, mode, notch_points=[]): - x = rectangle_pos[0] - y = rectangle_pos[1] - w = rectangle_size[0] - h = rectangle_size[1] - - if mode == "left": - rectangle_vertices = [[x, y], [x, y + h]] - elif mode == "right": - rectangle_vertices = [[x + w, y], [x + w, y + h]] - rectangle_vertices.extend(notch_points) - if mode == "left": - rectangle_vertices.extend([[x + w, y]]) - elif mode == "right": - rectangle_vertices.extend([[x, y]]) - - return rectangle_vertices - - @staticmethod - def _points_on_circle(center, radius, start_angle, end_angle): - points = [] - y_diff = False - if end_angle >= 180: - step = 1 - end_angle += 1 - elif end_angle <= 0: - step = -1 - end_angle -= 1 - else: - raise Exception("Invalid value for start angle") - - for degree in range(start_angle, end_angle, step): - angle = radians(degree) - x = center[0] + (radius * cos(angle)) - y = center[1] + (radius * sin(angle)) - - if y_diff is False: - y_diff = abs(y - center[1]) - - y += y_diff - points.append([x, y]) - - return points - - -class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): - """ - Top app bar class. - - For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and - :class:`~NotchedBox` and - :class:`~kivymd.uix.controllers.WindowController` - classes documentation. - - :Events: - `on_action_button` - Method for the button used for the :class:`~MDBottomAppBar` class. - """ - - left_action_items = ListProperty() - """ - The icons on the left of the bar. - To add one, append a list like the following: - - .. code-block:: kv - - MDTopAppBar: - left_action_items: - ["dots-vertical", callback, "tooltip text", "overflow text"] - - ``icon_name`` - is a string that corresponds to an icon definition: - - .. code-block:: kv - - MDTopAppBar: - right_action_items: [["home"]] - - ``callback`` - is the function called on a touch release event and: - - .. code-block:: kv - - MDTopAppBar: - right_action_items: [["home", lambda x: app.callback(x)]] - - .. code-block:: python - - class Test(MDApp): - def callback(self, instance_action_top_appbar_button): - print(instance_action_top_appbar_button) - - ``tooltip text`` - is the text to be displayed in the tooltip: - - .. code-block:: kv - - MDTopAppBar: - right_action_items: - [ - ["home", lambda x: app.callback(x), "Home"], - ["message-star", lambda x: app.callback(x), "Message star"], - ["message-question", lambda x: app.callback(x), "Message question"], - ["message-reply", lambda x: app.callback(x), "Message reply"], - ] - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-tooltip-text.png - :align: center - - ``overflow text`` - is the text for menu items (:class:`~OverFlowMenuItem`) - of the corresponding action buttons: - - .. code-block:: kv - - MDTopAppBar: - use_overflow: True - right_action_items: - [ - ["home", lambda x: x, "", "Home"], - ["message-star", lambda x: x, "", "Message star"], - ["message-question", lambda x: x, "" , "Message question"], - ["message-reply", lambda x: x, "", "Message reply"], - ] - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-overflow-text.png - :align: center - - ``icon color`` - icon color: - - .. code-block:: kv - - MDTopAppBar: - right_action_items: - [ - [ - "dots-vertical", - callback, - "tooltip text", - "overflow text", - (1, 1, 1, 1), - ] - ] - - Both the ``callback`` and ``tooltip text`` and ``overflow text`` and ``icon color`` are - optional but the order must be preserved. - - :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty` - and defaults to `[]`. - """ - - right_action_items = ListProperty() - """ - The icons on the left of the bar. - Works the same way as :attr:`left_action_items`. - - :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty` - and defaults to `[]`. - """ - - title = StringProperty() - """ - Text app bar. - - .. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-title.png - :align: center - - :attr:`title` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - mode = OptionProperty( - "center", options=["free-end", "free-center", "end", "center"] - ) - """ - Floating button position. Only for :class:`~MDBottomAppBar` class. - Available options are: `'free-end'`, `'free-center'`, `'end'`, `'center'`. - - .. rubric:: Mode "end": - - .. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - title: "Title" - icon: "git" - type: "bottom" - left_action_items: [["menu", lambda x: x]] - mode: "end" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-9.png - :align: center - - .. rubric:: Mode "free-end": - - .. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - mode: "free-end" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-10.png - :align: center - - .. rubric:: Mode "free-center": - - .. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - mode: "free-center" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-free-center.png - :align: center - - .. rubric:: Mode "center": - - .. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - mode: "center" - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-center.png - :align: center - - :attr:`mode` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'center'`. - """ - - type = OptionProperty("top", options=["top", "bottom"]) - """ - When using the :class:`~MDBottomAppBar` class, the parameter ``type`` - must be set to `'bottom'`: - - .. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - type: "bottom" - - Available options are: `'top'`, `'bottom'`. - - :attr:`type` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'top'`. - """ - - opposite_colors = BooleanProperty(False) - """ - Changes the color of the label to the color opposite to the main theme. - - .. code-block:: kv - - MDTopAppBar: - opposite_colors: True - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-true.png - :align: center - - .. code-block:: kv - - MDTopAppBar: - opposite_colors: False - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-false.png - :align: center - """ - - md_bg_bottom_color = ColorProperty(None) - """ - The background color in (r, g, b, a) or string format for the bar with the - ``bottom`` mode. - - .. versionadded:: 1.0.0 - - .. code-block:: kv - - MDBottomAppBar: - - MDTopAppBar: - md_bg_bottom_color: "brown" - icon_color: self.md_bg_bottom_color - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-md-bg-bottom-color.png - :align: center - - :attr:`md_bg_bottom_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - set_bars_color = BooleanProperty(False) - """ - If `True` the background color of the bar status will be set automatically - according to the current color of the bar. - - .. versionadded:: 1.0.0 - - See `set_bars_colors `_ - for more information. - - :attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - use_overflow = BooleanProperty(False) - """ - As a top app bar is resized, actions move to the overflow menu from right - to left. - - .. versionadded:: 1.0.0 - - .. code-block:: kv - - MDTopAppBar: - title: "MDTopAppBar" - use_overflow: True - right_action_items: - [ - ["home", lambda x: x, "Home", "Home"], - ["message-star", lambda x: x, "Message star", "Message star"], - ["message-question", lambda x: x, "Message question", "Message question"], - ["message-reply", lambda x: x, "Message reply", "Message reply"], - ] - - :attr:`use_overflow` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - overflow_cls = ObjectProperty() - """ - Must be an object of the :class:`~kivymd.uix.menu.MDDropdownMenu' class. - See :class:`~kivymd.uix.menu.MDDropdownMenu` class documentation for more - information. - - .. versionadded:: 1.0.0 - - .. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - from kivymd.uix.menu import MDDropdownMenu - - KV = ''' - #:import CustomOverFlowMenu __main__.CustomOverFlowMenu - - - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - title: "MDTopAppBar" - use_overflow: True - overflow_cls: CustomOverFlowMenu() - right_action_items: - [ - ["home", lambda x: x, "Home", "Home"], - ["message-star", lambda x: x, "Message star", "Message star"], - ["message-question", lambda x: x, "Message question", "Message question"], - ["message-reply", lambda x: x, "Message reply", "Message reply"], - ] - - MDLabel: - text: "Content" - halign: "center" - ''' - - - class CustomOverFlowMenu(MDDropdownMenu): - # In this class you can set custom properties for the overflow menu. - pass - - - class Example(MDApp): - def build(self): - return Builder.load_string(KV) - - def callback(self, instance_action_top_appbar_button): - print(instance_action_top_appbar_button) - - - Example().run() - - :attr:`overflow_cls` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - # Attributes only for the BottomAppBar class. - - icon = StringProperty() - """ - Floating button. Only for :class:`~MDBottomAppBar` class. - - :attr:`icon` is an :class:`~kivy.properties.StringProperty` - and defaults to `'android'`. - """ - - icon_color = ColorProperty() - """ - Color in (r, g, b, a) or string format action button. Only for - :class:`~MDBottomAppBar` class. - - :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[]`. - """ - - # MD3 Style attributes. - - anchor_title = OptionProperty(None, options=["left", "center", "right"]) - """ - Position bar title. Only used with `material_style = 'M3'` - Available options are: `'left'`, `'center'`, `'right'`. - - :attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty` - and defaults to `None`. - """ - - headline_text = StringProperty() - """ - Headline text bar. - - .. versionadded:: 1.0.0 - - :attr:`headline_text` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - headline_text_color = ColorProperty(None) - """ - Headline text color in (r, g, b, a) or string format. - - .. versionadded:: 1.0.0 - - :attr:`headline_text_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - type_height = OptionProperty("small", options=["medium", "large", "small"]) - """ - Bar height type. - - .. versionadded:: 1.0.0 - - Available options are: 'medium', 'large', 'small'. - - :attr:`type_height` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'small'`. - """ - - # List of action buttons (ActionTopAppBarButton instance) that have been - # .added to the overflow - _hidden_items = [] - # See `kivymd.uix.menu.MDDropdownMenu.items` attribute. - _overflow_menu_items = [] - - def __init__(self, **kwargs): - self.action_button = MDFabBottomAppBarButton() - super().__init__(**kwargs) - self.register_event_type("on_action_button") - - if not self.icon_color: - self.icon_color = self.theme_cls.primary_color - - self.bind(specific_text_color=self.update_action_bar_text_colors) - self.theme_cls.bind( - material_style=self.update_bar_height, - primary_palette=self.update_md_bg_color, - ) - - Clock.schedule_once( - lambda x: self.on_left_action_items(0, self.left_action_items) - ) - Clock.schedule_once( - lambda x: self.on_right_action_items(0, self.right_action_items) - ) - Clock.schedule_once(lambda x: self.set_md_bg_color(0, self.md_bg_color)) - Clock.schedule_once(lambda x: self.on_type_height(0, self.type_height)) - Clock.schedule_once( - lambda x: self.update_anchor_title(self.theme_cls.material_style) - ) - Clock.schedule_once(self.update_floating_radius) - Clock.schedule_once(self.check_overflow_cls) - - def set_headline_font_style(self, interval: Union[int, float]) -> None: - if self.type_height in ("medium", "large"): - self.ids.label_headline.font_style = { - "medium": "H6", - "large": "H5", - }[self.type_height] - - def on_width(self, instance_toolbar, width: float) -> None: - """ - Called when the bar is resized (size of the application window). - """ - - if self.mode == "center": - self.action_button.x = width / 2 - self.action_button.width / 2 - else: - self.action_button.x = width - self.action_button.width * 2 - - # The user reduces the width of the window. - if ( - self.get_window_width_resizing_direction() == "left" - and self.use_overflow - and self.ids.label_title.is_shortened - ): - if not self.overflow_action_button_is_added(): - self.add_overflow_button() - self.add_action_button_to_overflow() - # The user increases the width of the window. - if ( - self.get_window_width_resizing_direction() == "right" - and self.use_overflow - and not self.ids.label_title.is_shortened - and self.overflow_cls.items - ): - self.return_action_button_to_toolbar() - - def return_action_button_to_toolbar(self) -> None: - if len(self._hidden_items): - action_button = self._hidden_items.pop() - self.ids.right_actions.add_widget(action_button, index=1) - self.update_overflow_menu_items(action_button) - if not len(self._hidden_items): - self.remove_overflow_button() - - def remove_overflow_button(self) -> None: - """Removes an overflow button to the bar.""" - - if self.overflow_action_button_is_added(): - action_overflow_button = self.ids.right_actions.children[0] - self.ids.right_actions.remove_widget(action_overflow_button) - self._overflow_menu_items = [] - - def add_overflow_button(self) -> None: - """Adds an overflow button to the bar.""" - - self.ids.right_actions.add_widget( - MDActionOverFlowButton( - theme_text_color="Custom" - if not self.opposite_colors - else "Primary", - text_color=self.specific_text_color, - opposite_colors=self.opposite_colors, - on_release=lambda x: self.overflow_cls.open(), - ) - ) - - def overflow_action_button_is_added(self) -> bool: - """ - Returns `True` if at least one action button - (:class:`~ActionTopAppBarButton') on the bar is added to the - overflow. - """ - - if ( - not self.ids.right_actions.children[0].__class__ - is MDActionOverFlowButton - ): - return False - return True - - def add_action_button_to_overflow(self): - """Adds an overflow button to the bar.""" - - if len(self.ids.right_actions.children) > 1: - button_to_be_added = self.ids.right_actions.children[1] - self._hidden_items.append(button_to_be_added) - self.ids.right_actions.remove_widget(button_to_be_added) - - self._overflow_menu_items.append( - { - "viewclass": "OverFlowMenuItem", - "icon": button_to_be_added.icon, - "text": button_to_be_added.overflow_text, - "height": dp(48), - "on_press": lambda *x: button_to_be_added.on_release(*x), - } - ) - self.overflow_cls.items = self._overflow_menu_items - self.overflow_cls.caller = self.ids.right_actions.children[0] - - def check_overflow_cls(self, interval: Union[int, float]) -> None: - """ - If the user does not set the :attr:`overflow_cls` attribute but uses - overflows, the :attr:`overflow_cls` attribute will use the default - value. - """ - - if not self.overflow_cls: - self.overflow_cls = self.get_default_overflow_cls() - - def on_type(self, instance_toolbar, type_value: str) -> None: - """Called when the value of the :attr:`type` attribute changes.""" - - if type_value == "bottom": - self.action_button.bind(center_x=self.setter("notch_center_x")) - self.action_button.bind( - on_release=lambda x: self.dispatch("on_action_button") - ) - self.action_button.x = ( - Window.width / 2 - self.action_button.width / 2 - ) - self.action_button.y = ( - (self.center[1] - self.height / 2) - + self.theme_cls.standard_increment / 2 - + self._shift - ) - self.shadow_offset = [0, 30] - self.on_mode(None, self.mode) - - def on_type_height(self, instance_toolbar, height_type_value: str) -> None: - """ - Called when the value of the :attr:`type_height` attribute changes. - """ - - if self.theme_cls.material_style == "M2": - self.height = self.theme_cls.standard_increment - else: - if self.type != "bottom": - if height_type_value == "small": - self.height = dp(64) - elif height_type_value == "medium": - self.height = dp(112) - elif height_type_value == "large": - self.height = dp(152) - else: - self.height = self.theme_cls.standard_increment - Clock.schedule_once(self.set_headline_font_style) - - def on_action_button(self, *args): - """ - Method for the button used for the :class:`~MDBottomAppBar` class. - """ - - def on_overflow_cls( - self, instance_toolbar, instance_overflow_cls: MDDropdownMenu - ) -> None: - """ - Called when the value of the :attr:`overflow_cls` attribute changes. - """ - - self.overflow_cls = instance_overflow_cls - - def on_md_bg_color(self, instance_toolbar, color_value: list) -> None: - """ - Called when the value of the :attr:`md_bg_color` attribute changes. - """ - - def on_md_bg_color(interval: Union[int, float]): - if self.type == "bottom": - self.md_bg_color = [0, 0, 0, 0] - else: - if self.set_bars_color: - set_bars_colors( - color_value, None, self.theme_cls.theme_style - ) - - Clock.schedule_once(on_md_bg_color) - - def on_left_action_items(self, instance_toolbar, items_value: list) -> None: - """ - Called when the value of the :attr:`left_action_items` attribute - changes. - """ - - def on_left_action_items(interval: Union[int, float]): - self.update_action_bar(self.ids.left_actions, items_value) - - Clock.schedule_once(on_left_action_items) - - def on_right_action_items( - self, instance_toolbar, items_value: list - ) -> None: - """ - Called when the value of the :attr:`right_action_items` attribute - changes. - """ - - def on_right_actions(interval: Union[int, float]): - self.update_action_bar(self.ids.right_actions, items_value) - - Clock.schedule_once(on_right_actions) - - def on_icon(self, instance_toolbar, icon_name: str) -> None: - """Called when the value of the :attr:`icon` attribute changes.""" - - self.action_button.icon = icon_name - - def on_icon_color(self, instance, icon_name: str) -> None: - """ - Called when the value of the :attr:`icon_color` attribute changes. - """ - - self.action_button.md_bg_color = icon_name - - def on_md_bg_bottom_color( - self, instance_toolbar, color_value: list - ) -> None: - """ - Called when the value of the :attr:`md_bg_bottom_color` attribute - changes. - """ - - set_bars_colors(None, color_value, self.theme_cls.theme_style) - - def on_anchor_title(self, instance_toolbar, anchor_value: str) -> None: - """ - Called when the value of the :attr:`anchor_title` attribute changes. - """ - - def on_anchor_title(interval: Union[int, float]): - self.ids.label_title.halign = anchor_value - - Clock.schedule_once(on_anchor_title) - - def on_mode(self, instance_toolbar, mode_value: str) -> None: - """Called when the value of the :attr:`made` attribute changes.""" - - if self.type == "top": - return - - def on_mode(interval: Union[int, float]): - def set_button_pos(*args): - self.action_button.x = x - self.action_button.y = y - self._rounded_rectangle_height / 2 - self.action_button._hard_shadow_size = (0, 0) - self.action_button._soft_shadow_size = (0, 0) - anim = Animation( - scale_value_x=1, scale_value_y=1, scale_value_z=1, d=0.05 - ) - anim.bind(on_complete=self.set_shadow) - anim.start(self.action_button) - - if mode_value == "center": - self.set_notch() - x = Window.width / 2 - self.action_button.width / 2 - y = ( - (self.center[1] - self.height / 2) - + self.theme_cls.standard_increment / 2 - + self._shift - ) - elif mode_value == "end": - self.set_notch() - x = Window.width - self.action_button.width * 2 - y = ( - (self.center[1] - self.height / 2) - + self.theme_cls.standard_increment / 2 - + self._shift - ) - self.right_action_items = [] - elif mode_value == "free-end": - self.remove_notch() - x = Window.width - self.action_button.width - dp(10) - y = self.action_button.height + self.action_button.height / 2 - elif mode_value == "free-center": - self.remove_notch() - x = Window.width / 2 - self.action_button.width / 2 - y = self.action_button.height + self.action_button.height / 2 - self.remove_shadow() - anim = Animation( - scale_value_x=0, scale_value_y=0, scale_value_z=0, d=0.1 - ) - anim.bind(on_complete=set_button_pos) - anim.start(self.action_button) - - Clock.schedule_once(on_mode) - - def set_md_bg_color(self, instance_toolbar, color_value: list) -> None: - if color_value == [1.0, 1.0, 1.0, 0.0]: - self.md_bg_color = self.theme_cls.primary_color - - def set_notch(self) -> None: - anim = Animation(d=0.1) + Animation( - notch_radius=self.action_button.width / 2 + dp(8), - d=0.1, - ) - anim.start(self) - - def set_shadow(self, *args) -> None: - self.action_button._elevation = self.action_button.elevation - - def get_default_overflow_cls(self) -> OverFlowMenu: - return OverFlowMenu(width_mult=4) - - def update_overflow_menu_items(self, action_button) -> None: - for data in self.overflow_cls.items: - if data["icon"] == action_button.icon: - self.overflow_cls.items.remove(data) - break - - def update_bar_height( - self, instance_theme_manager, material_style_value: str - ) -> None: - self.on_type_height(self, self.type_height) - self.update_anchor_title(material_style_value) - - def update_floating_radius(self, interval: Union[int, float]) -> None: - self.action_button.radius = self.action_button.width / 2 - - def update_anchor_title(self, material_style_value: str) -> str: - if material_style_value == "M2": - self.anchor_title = "left" - elif material_style_value == "M3" and self.type != "bottom": - if not self.anchor_title: - self.anchor_title = "center" - elif material_style_value == "M3" and self.type == "bottom": - self.anchor_title = "left" - return self.anchor_title - - def update_action_bar( - self, instance_box_layout, action_bar_items: list - ) -> None: - instance_box_layout.clear_widgets() - new_width = 0 - - for item in action_bar_items: - new_width += dp(48) - if len(item) == 1: - item.append(lambda x: None) - if len(item) > 1 and not item[1]: - item[1] = lambda x: None - if len(item) == 2: - if isinstance(item[1], str) or isinstance(item[1], tuple): - item.insert(1, lambda x: None) - else: - item.append("") - if len(item) == 3: - if isinstance(item[2], tuple): - item.insert(2, "") - - instance_box_layout.add_widget( - ActionTopAppBarButton( - icon=item[0], - on_release=item[1], - tooltip_text=item[2], - overflow_text=item[3] - if (len(item) == 4 and isinstance(item[3], str)) - else "", - theme_text_color="Custom" - if not self.opposite_colors - else "Primary", - text_color=self.specific_text_color - if not (len(item) == 4 and isinstance(item[3], tuple)) - else item[3], - opposite_colors=self.opposite_colors, - ) - ) - - instance_box_layout.width = new_width - - def update_md_bg_color(self, *args) -> None: - self.md_bg_color = self.theme_cls._get_primary_color() - - def update_action_bar_text_colors(self, *args) -> None: - for child in self.ids.left_actions.children: - child.text_color = self.specific_text_color - for child in self.ids.right_actions.children: - child.text_color = self.specific_text_color - - def remove_notch(self) -> None: - anim = Animation(d=0.1) + Animation(notch_radius=0, d=0.1) - anim.start(self) - - def remove_shadow(self) -> None: - self.action_button._elevation = 0 - - def _update_specific_text_color(self, instance, value): - if self.specific_text_color in ( - [0.0, 0.0, 0.0, 0.87], - [0.0, 0.0, 0.0, 1.0], - [1.0, 1.0, 1.0, 1.0], - ): - self.specific_text_color = text_colors[ - self.theme_cls.primary_palette - ][self.theme_cls.primary_hue] - - -class MDBottomAppBar( - DeclarativeBehavior, - ThemableBehavior, - SpecificBackgroundColorBehavior, - CommonElevationBehavior, - FloatLayout, -): - """ - Bottom app bar class. - - For more information, see in the - :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and - :class:`~kivymd.theming.ThemableBehavior` and - :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and - :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and - :class:`~kivy.uix.floatlayout.FloatLayout` - classes documentation. - - :Events: - `on_show_bar` - The method is called when the :class:`~MDBottomAppBar` panel - is shown. - `on_hide_bar` - The method is called when the :class:`~MDBottomAppBar` panel - is hidden. - """ - - md_bg_color = ColorProperty([0, 0, 0, 0]) - """ - Color bar in (r, g, b, a) or string format. - - :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0, 0, 0, 0]`. - """ - - icon_color = ColorProperty(None) - """ - Color bar in (r, g, b, a) or string format. - - .. versionadded:: 1.2.0 - - :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - action_items = ListProperty() - """ - The icons on the left bar. - - .. versionadded:: 1.2.0 - - :attr:`action_items` is an :class:`~kivy.properties.ListProperty` - and defaults to `[]`. - """ - - animation = BooleanProperty(True) - """ - # TODO: add description. - # FIXME: changing the value does not affect anything. - - .. versionadded:: 1.2.0 - - :attr:`animation` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `True`. - """ - - show_transition = StringProperty("linear") - """ - Type of button display transition. - - .. versionadded:: 1.2.0 - - :attr:`show_transition` is a :class:`~kivy.properties.StringProperty` - and defaults to `'linear'`. - """ - - hide_transition = StringProperty("in_back") - """ - Type of button hidden transition. - - .. versionadded:: 1.2.0 - - :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty` - and defaults to `'in_back'`. - """ - - hide_duration = NumericProperty(0.4) - """ - Duration of button hidden transition. - - .. versionadded:: 1.2.0 - - :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - show_duration = NumericProperty(0.2) - """ - Duration of button display transition. - - .. versionadded:: 1.2.0 - - :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - - scroll_cls = ObjectProperty() - """ - Widget inherited from the :class:`~kivy.uix.scrollview.ScrollView` class. - The value must be set if the :attr:`allow_hidden` parameter is `True`. - - .. versionadded:: 1.2.0 - - :attr:`scroll_cls` is a :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. - """ - - allow_hidden = BooleanProperty(False) - """ - Allows or disables hiding the panel when scrolling content. - If the value is `True`, the :attr:`scroll_cls` parameter must be specified. - - .. versionadded:: 1.2.0 - - :attr:`allow_hidden` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - bar_is_hidden = BooleanProperty(False) - """ - Is the panel currently hidden. - - .. versionadded:: 1.2.0 - - :attr:`bar_is_hidden` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - _padding = dp(16) - _x = -dp(48) - _scroll_cls_y = 0 - _cache = [] - _current_data = [] - _wait_removed = False - _animated_hidden = True - _animated_show = True - _fab_bottom_app_bar_button = None - _action_overflow_button = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.size_hint_y = None - if self.theme_cls.material_style == "M3": - self.register_event_type("on_show_bar") - self.register_event_type("on_hide_bar") - - self.height = dp(80) - Clock.schedule_once(self.set_bg_color) - - def button_centering_animation( - self, - button: MDActionOverFlowButton - | MDActionBottomAppBarButton - | MDFabBottomAppBarButton, - ) -> None: - """ - Animation of centering buttons for - :class:`~MDActionOverFlowButton`, - :class:`~MDActionBottomAppBarButton` and - :class:`~MDFabBottomAppBarButton` classes. - """ - - if self.animation: - Animation( - y=self.height / 2 - dp(48) / 2, - opacity=1, - d=self.show_duration, - t=self.show_transition, - ).start(button) - - def check_scroll_direction(self, scroll_cls, y: float) -> None: - """ - Checks the scrolling direction. - Depending on the scrolling direction, hides or shows the - :class:`~MDBottomAppBar` panel. - """ - - if round(y, 1) < self._scroll_cls_y and not self.bar_is_hidden: - self.hide_bar() - if round(y, 1) > self._scroll_cls_y and self.bar_is_hidden: - self.show_bar() - - self._scroll_cls_y = round(y, 1) - - def show_bar(self) -> None: - """Show :class:`~MDBottomAppBar` panel.""" - - def on_complete(*args): - self.dispatch("on_show_bar") - - def on_progress(animation, instance, progress): - if progress > 0.5 and self._animated_show: - self._animated_show = False - for i, widget in enumerate(self.children): - if isinstance(widget, MDActionBottomAppBarButton): - anim_icon = Animation( - y=self.height / 2 - dp(48) / 2, - d=self.show_duration, - t=self.show_transition, - ) - Clock.schedule_once( - lambda x, y=widget: anim_icon.start(y), - i / 10, - ) - if self._fab_bottom_app_bar_button: - Animation( - y=self._fab_bottom_app_bar_button.y + dp(4), - d=self.show_duration, - t=self.show_transition, - ).start(self._fab_bottom_app_bar_button) - - self.bar_is_hidden = False - self._animated_show = True - anim = Animation( - y=0, - d=self.show_duration, - t=self.show_transition, - ) - anim.bind(on_progress=on_progress, on_complete=on_complete) - anim.start(self) - - def hide_bar(self) -> None: - """Hide :class:`~MDBottomAppBar` panel.""" - - def on_complete(*args): - self.dispatch("on_hide_bar") - - def on_progress(animation, instance, progress): - if ( - progress > 0.5 - and self._animated_hidden - and widget_icon == instance.icon - ): - self._animated_hidden = False - anim_bar = Animation( - y=-self.height, - d=self.hide_duration, - # t=self.hide_transition, - ) - anim_bar.bind(on_complete=on_complete) - anim_bar.start(self) - - if self._fab_bottom_app_bar_button: - Animation( - y=self._fab_bottom_app_bar_button.y - dp(4), - d=self.hide_duration, - t=self.hide_transition, - ).start(self._fab_bottom_app_bar_button) - - self.bar_is_hidden = True - self._animated_hidden = True - len_children = len(self.children) - widget_icon = "" - - for i, widget in enumerate(self.children): - if isinstance(widget, MDActionBottomAppBarButton): - anim = Animation( - y=-widget.height, - d=self.hide_duration, - t=self.hide_transition, - ) - if i + 2 == len_children: - widget_icon = widget.icon - anim.bind(on_progress=on_progress) - Clock.schedule_once( - lambda x, y=widget: anim.start(y), - i / 10, - ) - - def on_show_bar(self, *args) -> None: - """ - The method is called when the :class:`~MDBottomAppBar` panel - is shown. - """ - - def on_hide_bar(self, *args) -> None: - """ - The method is called when the :class:`~MDBottomAppBar` panel - is hidden. - """ - - def on_scroll_cls(self, instance, scroll_cls) -> None: - """ - Called when the value of the :attr:`scroll_cls` attribute changes. - """ - - def on_scroll_cls(*args): - if not self.allow_hidden: - Logger.warning( - "KivyMD: " - "In order for the bottom bar to be automatically hidden " - "in addition to the `scroll_cls` parameter, set the value " - "of the `allow_hidden` parameter to `True`" - ) - - if issubclass(scroll_cls.__class__, ScrollView): - if self.allow_hidden: - scroll_cls.bind(scroll_y=self.check_scroll_direction) - else: - raise TypeError( - f"The `scroll_cls` parameter must be an object inherited from " - f"the {ScrollView} class" - ) - - if self.theme_cls.material_style == "M3": - Clock.schedule_once(on_scroll_cls) - - def on_size(self, *args) -> None: - """Called when the root screen is resized.""" - - if ( - self._fab_bottom_app_bar_button - and self.theme_cls.material_style == "M3" - ): - self._fab_bottom_app_bar_button.x = Window.width - (dp(56) + dp(16)) - - def on_action_items(self, instance, value: list) -> None: - """ - Called when the value of the :attr:`action_items` attribute changes. - """ - - if self.theme_cls.material_style == "M2": - return - - def wait_removed(*args): - if len(self.children) == 1 or not self.children: - Clock.unschedule(wait_removed) - self._wait_removed = False - self._x = -dp(48) - asynckivy.start(add_widget()) - - async def add_widget(): - for button in value: - await asynckivy.sleep(0) - self.add_widget(button) - - if self._cache: - self._cache.append(value) - - for data in self._cache: - if value[0] in data: - for i, widget in enumerate(self.children): - if not self._wait_removed: - Clock.schedule_interval(wait_removed, 0) - self._wait_removed = True - if isinstance(widget, MDActionBottomAppBarButton): - anim = Animation( - y=-widget.height, - d=self.hide_duration, - t=self.hide_transition, - ) - anim.bind( - on_complete=lambda x, y=widget: self.remove_widget( - y - ) - ) - Clock.schedule_once( - lambda x, y=widget: anim.start(y), - i / 10, - ) - else: - self._cache.append(value) - self._current_data = value - asynckivy.start(add_widget()) - - def set_fab_opacity(self, *ars) -> None: - """ - Sets the transparency value of the:class:`~MDFabBottomAppBarButton` - button. - """ - - self._fab_bottom_app_bar_button.ids.lbl_ic.opacity = 1 - - def set_fab_icon(self, instance, value) -> None: - """ - Animates the size of the :class:`~MDFabBottomAppBarButton` button. - """ - - self._fab_bottom_app_bar_button.ids.lbl_ic.opacity = 0 - anim = Animation( - scale_value_x=0, - scale_value_y=0, - opacity=0, - d=self.hide_duration, - t=self.hide_transition, - ) + Animation( - scale_value_x=1, - scale_value_y=1, - opacity=1, - d=self.show_duration, - t=self.show_transition, - ) - anim.bind(on_complete=self.set_fab_opacity) - anim.start(instance) - - def set_bg_color(self, *args) -> None: - """ - Sets the background color for the :class:`~MDBottomAppBar` class. - """ - - if self.md_bg_color == [0, 0, 0, 0]: - self.md_bg_color = self.theme_cls.primary_color - - def set_icon_color( - self, widget: MDActionOverFlowButton | MDActionBottomAppBarButton - ) -> None: - """ - Sets the icon color for the :class:`~MDActionOverFlowButton` and - :class:`~MDActionBottomAppBarButton` classes. - """ - - if self.icon_color: - widget.theme_icon_color = "Custom" - widget.icon_color = self.icon_color - - def add_widget(self, widget, index=0, canvas=None): - # For M2 style. - if ( - isinstance(widget, MDTopAppBar) - and self.theme_cls.material_style == "M2" - ): - super().add_widget(widget) - widget.elevation = 0 - return super().add_widget(widget.action_button) - # For M3 style. - if self.theme_cls.material_style == "M3": - if isinstance(widget, MDActionBottomAppBarButton): - self._x += widget.width - widget.pos = ( - self._x + self._padding, - -dp(48) if self.animation else self.height / 2 - dp(48) / 2, - ) - widget.opacity = int(not self.animation) - self.set_icon_color(widget) - super().add_widget(widget) - self.button_centering_animation(widget) - elif isinstance(widget, MDFabBottomAppBarButton): - widget.bind(icon=self.set_fab_icon) - self._fab_bottom_app_bar_button = widget - Clock.schedule_once(self.set_fab_opacity) - widget.scale_value_x = int(not self.animation) - widget.scale_value_y = int(not self.animation) - widget.pos = ( - Window.width - (dp(56) + self._padding), - self.height / 2 - dp(56) / 2, - ) - super().add_widget(widget) diff --git a/kivymd/uix/widget.py b/kivymd/uix/widget.py index 27c1371e5..4107bd088 100644 --- a/kivymd/uix/widget.py +++ b/kivymd/uix/widget.py @@ -16,7 +16,7 @@ canvas: Color: - rgba: app.theme_cls.primary_color + rgba: app.theme_cls.primaryColor RoundedRectangle: pos: self.pos size: self.size @@ -31,7 +31,7 @@ size_hint: .5, None height: self.width radius: self.height / 2 - md_bg_color: app.theme_cls.primary_color + md_bg_color: app.theme_cls.primaryColor """ __all__ = ("MDWidget",) @@ -40,12 +40,26 @@ from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior -class MDWidget(DeclarativeBehavior, ThemableBehavior, MDAdaptiveWidget, Widget): +class MDWidget( + DeclarativeBehavior, + ThemableBehavior, + BackgroundColorBehavior, + MDAdaptiveWidget, + Widget, +): """ - See :class:`~kivy.uix.Widget` class documentation for more information. + Widget class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and + :class:`~kivymd.uix.MDAdaptiveWidget` and + :class:`~kivy.uix.widget.Widget` and + classes documentation. .. versionadded:: 1.0.0 """ diff --git a/kivymd/utils/get_wallpaper.py b/kivymd/utils/get_wallpaper.py new file mode 100644 index 000000000..3564ede39 --- /dev/null +++ b/kivymd/utils/get_wallpaper.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import os + +from kivy import platform, Logger + + +def get_wallpaper( + user_data_dir: str, path_to_wallpaper: str = "" +) -> bool | str: + if platform == "android": + try: + from jnius import autoclass, cast + + PythonActivity = autoclass("org.kivy.android.PythonActivity") + CompressFormat = autoclass("android.graphics.Bitmap$CompressFormat") + FileOutputStream = autoclass("java.io.FileOutputStream") + CurrentActivity = cast( + "android.app.Activity", PythonActivity.mActivity + ) + WallpaperManager = autoclass("android.app.WallpaperManager") + Context = cast( + "android.content.Context", + CurrentActivity.getApplicationContext(), + ) + + mWallpaperManager = WallpaperManager.getInstance(Context) + mWallpaperManager.getBitmap().compress( + CompressFormat.PNG, + 100, + FileOutputStream(f"{user_data_dir}/wallpaper.png"), + ) + return f"{user_data_dir}/wallpaper.png" + except Exception as exc: + Logger.error( + f"KivyMD: Dynamic color will not be used. The default palette is set. {exc}" + ) + return False + else: + if path_to_wallpaper: + return ( + path_to_wallpaper + if os.path.exists(path_to_wallpaper) + else False + ) + else: + return False