Sideband/sbapp/kivymd/uix/menu/menu.py
2023-07-10 02:49:58 +02:00

1461 lines
40 KiB
Python
Executable File

"""
Components/Menu
===============
.. seealso::
`Material Design spec, Menus <https://m3.material.io/components/menus/overview>`_
.. rubric:: Menus display a list of choices on temporary surfaces.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-preview.png
:align: center
- Menus should be easy to open, close, and interact with
- Menu content should be suited to user needs
- Menu items should be easy to scan
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
MDScreen:
MDRaisedButton:
id: button
text: "Press me"
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.menu_open()
'''
class Test(MDApp):
def menu_open(self):
menu_items = [
{
"text": f"Item {i}",
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
MDDropdownMenu(
caller=self.root.ids.button, items=menu_items
).open()
def menu_callback(self, text_item):
print(text_item)
def build(self):
self.theme_cls.primary_palette = "Orange"
self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-usage.gif
:align: center
Anatomy
-------
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-anatomy.png
:align: center
You can combine the following parameters:
-----------------------------------------
- leading_icon
- text
- trailing_icon
- trailing_text
...to create the necessary types of menu items:
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"leading_icon": "check",
"trailing_icon": "apple-keyboard-command",
"trailing_text": "+Shift+X",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-icon-trailing-text.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"trailing_icon": "apple-keyboard-command",
"trailing_text": "+Shift+X",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-icon-trailing-text.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"trailing_icon": "apple-keyboard-command",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-icon.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"trailing_text": "Shift+X",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-text.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"leading_icon": "check",
"trailing_icon": "apple-keyboard-command",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-icon.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"leading_icon": "check",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"leading_icon": "check",
"trailing_text": "Shift+X",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-text.png
:align: center
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-text.png
:align: center
You can use the following parameters to customize the menu items:
-----------------------------------------------------------------
- text_color
- leading_icon_color
- trailing_icon_color
- trailing_text_color
.. code-block:: python
menu_items = [
{
"text": "Strikethrough",
"leading_icon": "check",
"trailing_icon": "apple-keyboard-command",
"trailing_text": "+Shift+X",
"leading_icon_color": "orange",
"trailing_icon_color": "green",
"trailing_text_color": "red",
}
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-customize.png
:align: center
.. Header:
Header
------
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
<MenuHeader>
spacing: "12dp"
padding: "4dp"
adaptive_height: True
MDIconButton:
icon: "gesture-tap-button"
pos_hint: {"center_y": .5}
MDLabel:
text: "Actions"
adaptive_size: True
pos_hint: {"center_y": .5}
MDScreen:
MDRaisedButton:
id: button
text: "PRESS ME"
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.menu.open()
'''
class MenuHeader(MDBoxLayout):
'''An instance of the class that will be added to the menu header.'''
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"text": f"Item {i}",
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
self.menu = MDDropdownMenu(
header_cls=MenuHeader(),
caller=self.screen.ids.button,
items=menu_items,
)
def menu_callback(self, text_item):
print(text_item)
def build(self):
self.theme_cls.primary_palette = "Orange"
self.theme_cls.theme_style = "Dark"
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header.png
:align: center
Menu with MDTopAppBar
---------------------
The :class:`~MDDropdownMenu` works well with the standard
:class:`~kivymd.uix.toolbar.MDTopAppBar`. Since the buttons on the Toolbar are created
by the MDTopAppBar component, it is necessary to pass the button as an argument to
the callback using `lambda x: app.callback(x)`. This example uses drop down menus
for both the righthand and lefthand menus.
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.snackbar import Snackbar
KV = '''
MDBoxLayout:
orientation: "vertical"
MDTopAppBar:
title: "MDTopAppBar"
left_action_items: [["menu", lambda x: app.callback(x)]]
right_action_items: [["dots-vertical", lambda x: app.callback(x)]]
MDLabel:
text: "Content"
halign: "center"
'''
class Test(MDApp):
def build(self):
self.theme_cls.primary_palette = "Orange"
self.theme_cls.theme_style = "Dark"
menu_items = [
{
"text": f"Item {i}",
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
self.menu = MDDropdownMenu(items=menu_items)
return Builder.load_string(KV)
def callback(self, button):
self.menu.caller = button
self.menu.open()
def menu_callback(self, text_item):
self.menu.dismiss()
Snackbar(text=text_item).open()
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-menu.png
:align: center
.. Position:
Position
========
Bottom position
---------------
.. seealso::
:attr:`~MDDropdownMenu.position`
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
MDScreen:
MDTextField:
id: field
pos_hint: {'center_x': .5, 'center_y': .6}
size_hint_x: None
width: "200dp"
hint_text: "Password"
on_focus: if self.focus: app.menu.open()
'''
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"text": f"Item {i}",
"on_release": lambda x=f"Item {i}": self.set_item(x),
} for i in range(5)]
self.menu = MDDropdownMenu(
caller=self.screen.ids.field,
items=menu_items,
position="bottom",
)
def set_item(self, text_item):
self.screen.ids.field.text = text_item
self.menu.dismiss()
def build(self):
self.theme_cls.primary_palette = "Orange"
self.theme_cls.theme_style = "Dark"
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png
:align: center
Center position
---------------
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
MDScreen:
MDDropDownItem:
id: drop_item
pos_hint: {'center_x': .5, 'center_y': .5}
text: 'Item 0'
on_release: app.menu.open()
'''
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"text": f"Item {i}",
"on_release": lambda x=f"Item {i}": self.set_item(x),
} for i in range(5)
]
self.menu = MDDropdownMenu(
caller=self.screen.ids.drop_item,
items=menu_items,
position="center",
)
self.menu.bind()
def set_item(self, text_item):
self.screen.ids.drop_item.set_item(text_item)
self.menu.dismiss()
def build(self):
self.theme_cls.primary_palette = "Orange"
self.theme_cls.theme_style = "Dark"
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position-center.gif
:align: center
API break
=========
1.1.1 version
-------------
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem
from kivymd.uix.menu import MDDropdownMenu
KV = '''
<RightContentCls>
disabled: True
adaptive_size: True
pos_hint: {"center_y": .5}
MDIconButton:
icon: root.icon
icon_size: "16sp"
md_bg_color_disabled: 0, 0, 0, 0
MDLabel:
text: root.text
font_style: "Caption"
adaptive_size: True
pos_hint: {"center_y": .5}
<Item>
IconLeftWidget:
icon: root.left_icon
RightContentCls:
id: container
icon: root.right_icon
text: root.right_text
MDScreen:
MDRaisedButton:
id: button
text: "PRESS ME"
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.menu.open()
'''
class RightContentCls(IRightBodyTouch, MDBoxLayout):
icon = StringProperty()
text = StringProperty()
class Item(OneLineAvatarIconListItem):
left_icon = StringProperty()
right_icon = StringProperty()
right_text = StringProperty()
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"text": f"Item {i}",
"right_text": "+Shift+X",
"right_icon": "apple-keyboard-command",
"left_icon": "web",
"viewclass": "Item",
"height": dp(54),
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
self.menu = MDDropdownMenu(
caller=self.screen.ids.button,
items=menu_items,
bg_color="#bdc6b0",
width_mult=4,
)
def menu_callback(self, text_item):
print(text_item)
def build(self):
return self.screen
Test().run()
1.2.0 version
-------------
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
MDScreen:
MDRaisedButton:
id: button
text: "PRESS ME"
pos_hint: {"center_x": .5, "center_y": .5}
on_release: app.menu.open()
'''
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"text": f"Item {i}",
"leading_icon": "web",
"trailing_icon": "apple-keyboard-command",
"trailing_text": "+Shift+X",
"trailing_icon_color": "grey",
"trailing_text_color": "grey",
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
self.menu = MDDropdownMenu(
md_bg_color="#bdc6b0",
caller=self.screen.ids.button,
items=menu_items,
)
def menu_callback(self, text_item):
print(text_item)
def build(self):
return self.screen
Test().run()
"""
from __future__ import annotations
__all__ = (
"BaseDropdownItem",
"MDDropdownMenu",
"MDDropdownTextItem",
"MDDropdownLeadingIconItem",
"MDDropdownTrailingIconItem",
"MDDropdownTrailingIconTextItem",
"MDDropdownTrailingTextItem",
"MDDropdownLeadingTrailingIconTextItem",
"MDDropdownLeadingIconTrailingTextItem",
)
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,
VariableListProperty,
StringProperty,
)
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
with open(
os.path.join(uix_path, "menu", "menu.kv"), encoding="utf-8"
) as kv_file:
Builder.load_string(kv_file.read())
class MDMenu(RecycleView):
width_mult = NumericProperty(1)
"""
See :attr:`~MDDropdownMenu.width_mult`.
.. deprecated:: 1.2.0
"""
drop_cls = ObjectProperty()
"""
See :class:`~MDDropdownMenu` class.
"""
class BaseDropdownItem(RectangularRippleBehavior, MDBoxLayout):
"""
Base class for menu items.
.. versionadded:: 1.2.0
For more information, see in the
:class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
:class:`~kivymd.uix.boxlayout.MDBoxLayout` classes.
"""
text = StringProperty()
"""
The text of the menu item.
:attr:`text` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
leading_icon = StringProperty()
"""
The leading icon of the menu item.
:attr:`leading_icon` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
trailing_icon = StringProperty()
"""
The trailing icon of the menu item.
:attr:`trailing_icon` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
trailing_text = StringProperty()
"""
The trailing text of the menu item.
:attr:`trailing_text` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
text_color = ColorProperty(None)
"""
The color of the text in (r, g, b, a) or string format for the text of the
menu item.
:attr:`text_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
leading_icon_color = ColorProperty(None)
"""
The color of the text in (r, g, b, a) or string format for the leading icon
of the menu item.
:attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
trailing_icon_color = ColorProperty(None)
"""
The color of the text in (r, g, b, a) or string format for the trailing
icon of the menu item.
:attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
trailing_text_color = ColorProperty(None)
"""
The color of the text in (r, g, b, a) or string format for the trailing
text of the menu item.
:attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
divider = OptionProperty("Full", options=["Full", None], allownone=True)
"""
Divider mode. Available options are: `'Full'`, `None`
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.
:attr:`divider_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
class MDTrailingTextContainer(BaseDropdownItem, IRightBody, MDLabel):
"""
Implements a container for trailing text.
.. versionadded:: 1.2.0
For more information, see in the
:class:`~BaseDropdownItem` and
:class:`~kivymd.uix.list.IRightBody` and
:class:`~kivymd.uix.label.MDLabel` classes.
"""
class MDTrailingIconTextContainer(BaseDropdownItem, IRightBody, MDBoxLayout):
"""
Implements a container for trailing icons and trailing text.
.. versionadded:: 1.2.0
For more information, see in the
:class:`~BaseDropdownItem` and
:class:`~kivymd.uix.list.IRightBody` and
:class:`~kivymd.uix.boxlayout.MDBoxLayout` classes.
"""
class MDDropdownTextItem(BaseDropdownItem):
"""
Implements a menu item with text without leading and trailing icons.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownLeadingIconItem(BaseDropdownItem):
"""
Implements a menu item with text, leading icon and without trailing icon.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownTrailingIconItem(BaseDropdownItem):
"""
Implements a menu item with text, without leading icon and with trailing
icon.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownTrailingIconTextItem(BaseDropdownItem):
"""
Implements a menu item with text, without leading icon, with trailing
icon and with trailing text.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownTrailingTextItem(BaseDropdownItem):
"""
Implements a menu item with text, without leading icon, without trailing
icon and with trailing text.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownLeadingIconTrailingTextItem(BaseDropdownItem):
"""
Implements a menu item with text, leading icon and with trailing text.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownLeadingTrailingIconTextItem(BaseDropdownItem):
"""
Implements a menu item with text, with leading icon, with trailing
icon and with trailing text.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownLeadingTrailingIconItem(BaseDropdownItem):
"""
Implements a menu item with text, with leading icon, with trailing icon.
.. versionadded:: 1.2.0
For more information, see in the :class:`~BaseDropdownItem` class.
"""
class MDDropdownMenu(MotionDropDownMenuBehavior, StencilBehavior, MDCard):
"""
Dropdown menu class.
For more information, see in the
:class:`~kivymd.uix.behaviors.MotionDropDownMenuBehavior` and
:class:`~kivymd.uix.behaviors.StencilBehavior` and
:class:`~kivymd.uix.card.MDCard`
classes documentation.
:Events:
`on_release`
The method that will be called when you click menu items.
"""
header_cls = ObjectProperty()
"""
An instance of the class (`Kivy` or `KivyMD` widget) that will be added
to the menu header.
.. versionadded:: 0.104.2
See Header_ for more information.
:attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
items = ListProperty()
"""
List of dictionaries with properties for menu items.
:attr:`items` is a :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
width_mult = NumericProperty(1, deprecated=True)
"""
This number multiplied by the standard increment ('56dp' on mobile, '64dp'
on desktop), determines the width of the menu items.
If the resulting number were to be too big for the application Window,
the multiplier will be adjusted for the biggest possible one.
.. deprecated:: 1.2.0
Use `width` instead.
.. code-block:: python
self.menu = MDDropdownMenu(
width=dp(240),
...,
)
:attr:`width_mult` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
min_height = NumericProperty(dp(48))
max_height = NumericProperty()
"""
The menu will grow no bigger than this number. Set to 0 for no limit.
:attr:`max_height` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
border_margin = NumericProperty("4dp")
"""
Margin between Window border and menu.
.. code-block:: python
self.menu = MDDropdownMenu(
border_margin=dp(24),
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-border-margin-24.png
:align: center
:attr:`border_margin` is a :class:`~kivy.properties.NumericProperty`
and defaults to `4dp`.
"""
ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
"""
Where the menu will grow vertically to when opening. Set to `None` to let
the widget pick for you. Available options are: `'up'`, `'down'`.
.. code-block:: python
self.menu = MDDropdownMenu(
ver_growth="up",
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-up.png
:align: center
.. code-block:: python
self.menu = MDDropdownMenu(
ver_growth="down",
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-down.png
:align: center
:attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
hor_growth = OptionProperty(None, allownone=True, options=["left", "right"])
"""
Where the menu will grow horizontally to when opening. Set to `None` to let
the widget pick for you. Available options are: `'left'`, `'right'`.
.. code-block:: python
self.menu = MDDropdownMenu(
hor_growth="left",
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-left.png
:align: center
.. code-block:: python
self.menu = MDDropdownMenu(
hor_growth="right",
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-right.png
:align: center
:attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
background_color = ColorProperty(None, deprecated=True)
"""
Color in (r, g, b, a) or string format of the background of the menu.
.. deprecated:: 1.2.0
Use `md_bg_color` instead.
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
caller = ObjectProperty()
"""
The widget object that calls the menu window.
:attr:`caller` is a :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
position = OptionProperty(
"auto", options=["top", "auto", "center", "bottom"]
)
"""
Menu window position relative to parent element.
Available options are: `'auto'`, `'top'`, `'center'`, `'bottom'`.
See Position_ for more information.
:attr:`position` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'auto'`.
"""
radius = VariableListProperty([dp(7)])
"""
Menu radius.
:attr:`radius` is a :class:`~kivy.properties.VariableListProperty`
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
_tar_y = 0
def __init__(self, **kwargs):
super().__init__(**kwargs)
Window.bind(
on_resize=self._remove_menu,
on_maximize=self._remove_menu,
on_restore=self._remove_menu,
)
self.register_event_type("on_dismiss")
self.menu = self.ids.md_menu
self.target_height = 0
def adjust_width(self) -> None:
"""
Adjust the width of the menu if the width of the menu goes beyond
the boundaries of the parent window from starting point.
"""
if self._start_coords[0] >= Window.width / 2:
if self.width > self._start_coords[0]:
self.width = (
self._start_coords[0]
- self.border_margin
- (
(self.caller.width / 2 + self.border_margin)
if self.position in ["right", "left"]
else 0
)
)
else:
if Window.width - self._start_coords[0] < self.width:
self.width = (
Window.width - self._start_coords[0] - self.border_margin
)
def check_ver_growth(self) -> None:
"""
Checks whether the height of the lower/upper borders of the menu
exceeds the limits borders of the parent window.
"""
if self.target_height > self._start_coords[1] - self.border_margin:
self.ver_growth = "up"
else:
if self._start_coords[1] > Window.height - self._start_coords[1]:
self.ver_growth = "down"
def check_hor_growth(self) -> None:
"""
Checks whether the width of the left/right menu borders exceeds the
boundaries of the parent window.
"""
if (
Window.width - (self._start_coords[0] + self.border_margin)
<= self.width
):
self.hor_growth = "left"
elif self.width >= self._start_coords[0] + self.border_margin:
self.hor_growth = "right"
def get_target_pos(self) -> [float, float]:
self._tar_x, self._tar_y = self._start_coords
if self.ver_growth == "up":
self._tar_y = self._start_coords[1] + self.height
else:
self._tar_y = self._start_coords[1]
if self.hor_growth == "left":
self._tar_x = self._start_coords[0] - self.width
else:
self._tar_x = self._start_coords[0]
return self._tar_x, self._tar_y
def set_target_height(self) -> None:
"""
Set the target height of the menu depending on the size of each item.
"""
self.target_height = 0
for item in self.menu.data:
self.target_height += item.get("height", self.min_height)
if 0 < self.max_height < self.target_height:
self.target_height = self.max_height
if self._start_coords[1] >= Window.height / 2:
if self.target_height > self._start_coords[1]:
self.target_height = (
self._start_coords[1]
- self.border_margin
- (
(self.caller.height / 2 + self.border_margin)
if self.position in ["top", "bottom"]
else 0
)
)
else:
if Window.height - self._start_coords[1] < self.target_height:
self.target_height = (
Window.height - self._start_coords[1] - self.border_margin
)
def set_menu_properties(self, *args) -> None:
"""Sets the size and position for the menu window."""
if self.caller:
self.menu.data = self._items
# We need to pick a starting point, see how big we need to be,
# and where to grow to.
self._start_coords = self.caller.to_window(*self.caller.center)
self.adjust_width()
self.set_target_height()
self.check_ver_growth()
self.check_hor_growth()
def set_menu_pos(self, *args) -> None:
if self.position == "auto":
self.menu.x = self._tar_x
self.menu.y = self._tar_y - (
self.header_cls.height if self.header_cls else 0
)
else:
if self.position == "center":
self.pos = (
self._start_coords[0] - self.width / 2,
self._start_coords[1] - self.height / 2,
)
elif self.position == "bottom":
self.pos = (
(self._start_coords[0] - self.width / 2)
if not self.hor_growth
else (
(self._start_coords[0] - self.width)
if self.hor_growth == "left"
else (self._start_coords[0])
),
self._start_coords[1]
- (
self.height
+ self.border_margin
+ self.caller.height / 2
),
)
elif self.position == "top":
self.pos = (
(self._start_coords[0] - self.width / 2)
if not self.hor_growth
else (
(self._start_coords[0] - self.width)
if self.hor_growth == "left"
else (self._start_coords[0])
),
self._start_coords[1]
+ self.caller.height / 2
+ self.border_margin,
)
def adjust_position(self) -> str:
"""
Return value 'auto' for the menu position if the menu position is out
of screen.
"""
position = self.position
if position == "bottom":
if (
self._start_coords[1]
- (self.height + self.border_margin + self.caller.height / 2)
< 0
):
position = "auto"
elif position == "top":
if (
self._start_coords[1]
+ self.caller.height / 2
+ self.border_margin
> Window.height
):
position = "auto"
elif position == "center":
if (
(
self._start_coords[1] + self.height / 2 > Window.height
or self._start_coords[1] - self.height / 2 < 0
)
or Window.width - (self._start_coords[0] + self.border_margin)
< self.width / 2
or self._start_coords[0] + self.border_margin < self.width / 2
):
position = "auto"
return position
def open(self) -> None:
"""Animate the opening of a menu window."""
self.set_menu_properties()
Window.add_widget(self)
self.position = self.adjust_position()
if self.width <= 100:
self.width = dp(240)
self.height = self.target_height
self._tar_x, self._tar_y = self.get_target_pos()
self.x = self._tar_x
self.y = self._tar_y - self.target_height
self.scale_value_center = self.caller.center
self.set_menu_pos()
self.on_open()
def on_items(self, instance, value: list) -> None:
"""
The method sets the class that will be used to create the menu item.
"""
items = []
viewclass = "MDDropdownTextItem"
for data in value:
if "viewclass" not in data:
if (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MDDropdownTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingIconTrailingTextItem"
data["viewclass"] = viewclass
if "height" not in data:
data["height"] = dp(48)
items.append(data)
self._items = items
def on_header_cls(
self, instance_dropdown_menu, instance_user_menu_header
) -> None:
"""Called when a value is set to the :attr:`header_cls` parameter."""
def add_content_header_cls(interval):
self.ids.content_header.clear_widgets()
self.ids.content_header.add_widget(instance_user_menu_header)
Clock.schedule_once(add_content_header_cls, 1)
def on_touch_down(self, touch):
if not self.menu.collide_point(*touch.pos):
self.dispatch("on_dismiss")
return True
super().on_touch_down(touch)
return True
def on_touch_move(self, touch):
super().on_touch_move(touch)
return True
def on_touch_up(self, touch):
super().on_touch_up(touch)
return True
def dismiss(self, *args) -> None:
"""Closes the menu."""
self.on_dismiss()
def _remove_menu(self, *args):
Window.remove_widget(self)
self.set_scale()
if __name__ == "__main__":
# To test the correct menu position.
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.screen import MDScreen
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = MDScreen()
menu_items = [{"text": f"Item {i}"} for i in range(55)]
self.menu = MDDropdownMenu(items=menu_items, width_mult=4)
def open_menu(self, caller):
self.menu.caller = caller
self.menu.open()
def on_start(self):
pos_hints = [
{"top": 1, "left": 0.1},
{"top": 1, "center_x": 0.5},
{"top": 1, "right": 1},
{"center_y": 0.5, "left": 1},
{"bottom": 1, "left": 1},
{"bottom": 1, "center_x": 0.5},
{"bottom": 1, "right": 1},
{"center_y": 0.5, "right": 1},
{"center_y": 0.5, "center_x": 0.5},
]
for pos_hint in pos_hints:
self.screen.add_widget(
MDRaisedButton(pos_hint=pos_hint, on_release=self.open_menu)
)
def build(self):
return self.screen
Test().run()