1199 lines
34 KiB
Python
1199 lines
34 KiB
Python
|
"""
|
||
|
Components/Menu
|
||
|
===============
|
||
|
|
||
|
.. seealso::
|
||
|
|
||
|
`Material Design spec, Menus <https://material.io/components/menus>`_
|
||
|
|
||
|
.. rubric:: Menus display a list of choices on temporary surfaces.
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-previous.png
|
||
|
:align: center
|
||
|
|
||
|
Usage
|
||
|
-----
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu
|
||
|
|
||
|
KV = '''
|
||
|
Screen:
|
||
|
|
||
|
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}"} for i in range(5)]
|
||
|
self.menu = MDDropdownMenu(
|
||
|
caller=self.screen.ids.button,
|
||
|
items=menu_items,
|
||
|
width_mult=4,
|
||
|
)
|
||
|
self.menu.bind(on_release=self.menu_callback)
|
||
|
|
||
|
def menu_callback(self, instance_menu, instance_menu_item):
|
||
|
print(instance_menu, instance_menu_item)
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-usage.gif
|
||
|
:align: center
|
||
|
|
||
|
.. Warning:: Do not create the :class:`~MDDropdownMenu` object when you open
|
||
|
the menu window. Because on a mobile device this one will be very slow!
|
||
|
|
||
|
Wrong
|
||
|
-----
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
menu = MDDropdownMenu(caller=self.screen.ids.button, items=menu_items)
|
||
|
menu.open()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-wrong.gif
|
||
|
:align: center
|
||
|
|
||
|
Customization of menu item
|
||
|
--------------------------
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right.gif
|
||
|
:align: center
|
||
|
|
||
|
You must create a new class that inherits from the :class:`~RightContent` class:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
class RightContentCls(RightContent):
|
||
|
pass
|
||
|
|
||
|
Now in the KV rule you can create your own elements that will be displayed in
|
||
|
the menu item on the right:
|
||
|
|
||
|
.. code-block:: kv
|
||
|
|
||
|
<RightContentCls>
|
||
|
disabled: True
|
||
|
|
||
|
MDIconButton:
|
||
|
icon: root.icon
|
||
|
user_font_size: "16sp"
|
||
|
pos_hint: {"center_y": .5}
|
||
|
|
||
|
MDLabel:
|
||
|
text: root.text
|
||
|
font_style: "Caption"
|
||
|
size_hint_x: None
|
||
|
width: self.texture_size[0]
|
||
|
text_size: None, None
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right-detail.png
|
||
|
:align: center
|
||
|
|
||
|
Now create menu items as usual, but add the key ``right_content_cls`` whose
|
||
|
value is the class ``RightContentCls`` that you created:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
menu_items = [
|
||
|
{
|
||
|
"right_content_cls": RightContentCls(
|
||
|
text=f"R+{i}", icon="apple-keyboard-command",
|
||
|
),
|
||
|
"icon": "git",
|
||
|
"text": f"Item {i}",
|
||
|
}
|
||
|
for i in range(5)
|
||
|
]
|
||
|
self.menu = MDDropdownMenu(
|
||
|
caller=self.screen.ids.button, items=menu_items, width_mult=4
|
||
|
)
|
||
|
|
||
|
Full example
|
||
|
------------
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu, RightContent
|
||
|
|
||
|
KV = '''
|
||
|
<RightContentCls>
|
||
|
disabled: True
|
||
|
|
||
|
MDIconButton:
|
||
|
icon: root.icon
|
||
|
user_font_size: "16sp"
|
||
|
pos_hint: {"center_y": .5}
|
||
|
|
||
|
MDLabel:
|
||
|
text: root.text
|
||
|
font_style: "Caption"
|
||
|
size_hint_x: None
|
||
|
width: self.texture_size[0]
|
||
|
text_size: None, None
|
||
|
|
||
|
|
||
|
Screen:
|
||
|
|
||
|
MDRaisedButton:
|
||
|
id: button
|
||
|
text: "PRESS ME"
|
||
|
pos_hint: {"center_x": .5, "center_y": .5}
|
||
|
on_release: app.menu.open()
|
||
|
'''
|
||
|
|
||
|
|
||
|
class RightContentCls(RightContent):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class Test(MDApp):
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.screen = Builder.load_string(KV)
|
||
|
menu_items = [
|
||
|
{
|
||
|
"right_content_cls": RightContentCls(
|
||
|
text=f"R+{i}", icon="apple-keyboard-command",
|
||
|
),
|
||
|
"icon": "git",
|
||
|
"text": f"Item {i}",
|
||
|
}
|
||
|
for i in range(5)
|
||
|
]
|
||
|
self.menu = MDDropdownMenu(
|
||
|
caller=self.screen.ids.button, items=menu_items, width_mult=4
|
||
|
)
|
||
|
self.menu.bind(on_release=self.menu_callback)
|
||
|
|
||
|
def menu_callback(self, instance_menu, instance_menu_item):
|
||
|
instance_menu.dismiss()
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
Menu without icons on the left
|
||
|
------------------------------
|
||
|
|
||
|
If you do not want to use the icons in the menu items on the left,
|
||
|
then do not use the "icon" key when creating menu items:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
menu_items = [
|
||
|
{
|
||
|
"right_content_cls": RightContentCls(
|
||
|
text=f"R+{i}", icon="apple-keyboard-command",
|
||
|
),
|
||
|
"text": f"Item {i}",
|
||
|
}
|
||
|
for i in range(5)
|
||
|
]
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-without-icon.png
|
||
|
:align: center
|
||
|
|
||
|
Item height adjustment
|
||
|
----------------------
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
menu_items = [
|
||
|
{
|
||
|
"right_content_cls": RightContentCls(
|
||
|
text=f"R+{i}", icon="apple-keyboard-command",
|
||
|
),
|
||
|
"text": f"Item {i}",
|
||
|
"height": "36dp",
|
||
|
"top_pad": "10dp",
|
||
|
"bot_pad": "10dp",
|
||
|
}
|
||
|
for i in range(5)
|
||
|
]
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-pad.png
|
||
|
:align: center
|
||
|
|
||
|
Mixin items
|
||
|
-----------
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu, RightContent
|
||
|
|
||
|
KV = '''
|
||
|
<RightContentCls>
|
||
|
disabled: True
|
||
|
|
||
|
MDIconButton:
|
||
|
icon: root.icon
|
||
|
user_font_size: "16sp"
|
||
|
pos_hint: {"center_y": .5}
|
||
|
|
||
|
MDLabel:
|
||
|
text: root.text
|
||
|
font_style: "Caption"
|
||
|
size_hint_x: None
|
||
|
width: self.texture_size[0]
|
||
|
text_size: None, None
|
||
|
|
||
|
|
||
|
Screen:
|
||
|
|
||
|
MDRaisedButton:
|
||
|
id: button
|
||
|
text: "PRESS ME"
|
||
|
pos_hint: {"center_x": .5, "center_y": .5}
|
||
|
on_release: app.menu.open()
|
||
|
'''
|
||
|
|
||
|
|
||
|
class RightContentCls(RightContent):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class Test(MDApp):
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.screen = Builder.load_string(KV)
|
||
|
|
||
|
menu_items = []
|
||
|
data = [
|
||
|
{"": "Open"},
|
||
|
{},
|
||
|
{"open-in-app": "Open in app >"},
|
||
|
{"trash-can-outline": "Move to Trash"},
|
||
|
{"rename-box": "Rename"},
|
||
|
{"zip-box-outline": "Create zip"},
|
||
|
{},
|
||
|
{"": "Properties"},
|
||
|
]
|
||
|
|
||
|
for data_item in data:
|
||
|
if data_item:
|
||
|
if list(data_item.items())[0][1].endswith(">"):
|
||
|
menu_items.append(
|
||
|
{
|
||
|
"right_content_cls": RightContentCls(
|
||
|
icon="menu-right-outline",
|
||
|
),
|
||
|
"icon": list(data_item.items())[0][0],
|
||
|
"text": list(data_item.items())[0][1][:-2],
|
||
|
"height": "36dp",
|
||
|
"top_pad": "10dp",
|
||
|
"bot_pad": "10dp",
|
||
|
"divider": None,
|
||
|
}
|
||
|
)
|
||
|
else:
|
||
|
menu_items.append(
|
||
|
{
|
||
|
"text": list(data_item.items())[0][1],
|
||
|
"icon": list(data_item.items())[0][0],
|
||
|
"font_style": "Caption",
|
||
|
"height": "36dp",
|
||
|
"top_pad": "10dp",
|
||
|
"bot_pad": "10dp",
|
||
|
"divider": None,
|
||
|
}
|
||
|
)
|
||
|
else:
|
||
|
menu_items.append(
|
||
|
{"viewclass": "MDSeparator", "height": 1}
|
||
|
)
|
||
|
self.menu = MDDropdownMenu(
|
||
|
caller=self.screen.ids.button,
|
||
|
items=menu_items,
|
||
|
width_mult=4,
|
||
|
)
|
||
|
self.menu.bind(on_release=self.menu_callback)
|
||
|
|
||
|
def menu_callback(self, instance_menu, instance_menu_item):
|
||
|
print(instance_menu, instance_menu_item
|
||
|
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-mixin.png
|
||
|
:align: center
|
||
|
|
||
|
Hover Behavior
|
||
|
--------------
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
self.menu = MDDropdownMenu(
|
||
|
...,
|
||
|
...,
|
||
|
selected_color=self.theme_cls.primary_dark_hue,
|
||
|
)
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-with-hover.gif
|
||
|
:align: center
|
||
|
|
||
|
Create submenu
|
||
|
--------------
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu
|
||
|
|
||
|
KV = '''
|
||
|
Screen:
|
||
|
|
||
|
MDRaisedButton:
|
||
|
id: button
|
||
|
text: "PRESS ME"
|
||
|
pos_hint: {"center_x": .5, "center_y": .5}
|
||
|
on_release: app.menu.open()
|
||
|
'''
|
||
|
|
||
|
|
||
|
class CustomDrop(MDDropdownMenu):
|
||
|
def set_bg_color_items(self, instance_selected_item):
|
||
|
if self.selected_color and not MDApp.get_running_app().submenu:
|
||
|
for item in self.menu.ids.box.children:
|
||
|
if item is not instance_selected_item:
|
||
|
item.bg_color = (0, 0, 0, 0)
|
||
|
else:
|
||
|
instance_selected_item.bg_color = self.selected_color
|
||
|
|
||
|
|
||
|
class Test(MDApp):
|
||
|
submenu = None
|
||
|
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.screen = Builder.load_string(KV)
|
||
|
menu_items = [
|
||
|
{
|
||
|
"icon": "git",
|
||
|
"text": f"Item {i}" if i != 3 else "Open submenu",
|
||
|
}
|
||
|
for i in range(5)
|
||
|
]
|
||
|
self.menu = CustomDrop(
|
||
|
caller=self.screen.ids.button,
|
||
|
items=menu_items,
|
||
|
width_mult=4,
|
||
|
selected_color=self.theme_cls.bg_darkest
|
||
|
)
|
||
|
self.menu.bind(on_enter=self.check_item)
|
||
|
|
||
|
def check_item(self, menu, item):
|
||
|
if item.text == "Open submenu" and not self.submenu:
|
||
|
menu_items = [{"text": f"Item {i}"} for i in range(5)]
|
||
|
self.submenu = MDDropdownMenu(
|
||
|
caller=item,
|
||
|
items=menu_items,
|
||
|
width_mult=4,
|
||
|
selected_color=self.theme_cls.bg_darkest,
|
||
|
)
|
||
|
self.submenu.bind(on_dismiss=self.set_state_submenu)
|
||
|
self.submenu.open()
|
||
|
|
||
|
def set_state_submenu(self, *args):
|
||
|
self.submenu = None
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-submenu.gif
|
||
|
:align: center
|
||
|
|
||
|
Menu with MDToolbar
|
||
|
-------------------
|
||
|
|
||
|
.. Warning:: The :class:`~MDDropdownMenu` does not work with the standard
|
||
|
:class:`~kivymd.uix.toolbar.MDToolbar`. You can use your own
|
||
|
``CustomToolbar`` and bind the menu window output to its elements.
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu
|
||
|
from kivymd.theming import ThemableBehavior
|
||
|
from kivymd.uix.behaviors import RectangularElevationBehavior
|
||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
||
|
|
||
|
KV = '''
|
||
|
<CustomToolbar>:
|
||
|
size_hint_y: None
|
||
|
height: self.theme_cls.standard_increment
|
||
|
padding: "5dp"
|
||
|
spacing: "12dp"
|
||
|
|
||
|
MDIconButton:
|
||
|
id: button_1
|
||
|
icon: "menu"
|
||
|
pos_hint: {"center_y": .5}
|
||
|
on_release: app.menu_1.open()
|
||
|
|
||
|
MDLabel:
|
||
|
text: "MDDropdownMenu"
|
||
|
pos_hint: {"center_y": .5}
|
||
|
size_hint_x: None
|
||
|
width: self.texture_size[0]
|
||
|
text_size: None, None
|
||
|
font_style: 'H6'
|
||
|
|
||
|
Widget:
|
||
|
|
||
|
MDIconButton:
|
||
|
id: button_2
|
||
|
icon: "dots-vertical"
|
||
|
pos_hint: {"center_y": .5}
|
||
|
on_release: app.menu_2.open()
|
||
|
|
||
|
|
||
|
Screen:
|
||
|
|
||
|
CustomToolbar:
|
||
|
id: toolbar
|
||
|
elevation: 10
|
||
|
pos_hint: {"top": 1}
|
||
|
'''
|
||
|
|
||
|
|
||
|
class CustomToolbar(
|
||
|
ThemableBehavior, RectangularElevationBehavior, MDBoxLayout,
|
||
|
):
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.md_bg_color = self.theme_cls.primary_color
|
||
|
|
||
|
|
||
|
class Test(MDApp):
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.screen = Builder.load_string(KV)
|
||
|
self.menu_1 = self.create_menu(
|
||
|
"Button menu", self.screen.ids.toolbar.ids.button_1,
|
||
|
)
|
||
|
self.menu_2 = self.create_menu(
|
||
|
"Button dots", self.screen.ids.toolbar.ids.button_2,
|
||
|
)
|
||
|
|
||
|
def create_menu(self, text, instance):
|
||
|
menu_items = [{"icon": "git", "text": text} for i in range(5)]
|
||
|
menu = MDDropdownMenu(caller=instance, items=menu_items, width_mult=5)
|
||
|
menu.bind(on_release=self.menu_callback)
|
||
|
return menu
|
||
|
|
||
|
def menu_callback(self, instance_menu, instance_menu_item):
|
||
|
instance_menu.dismiss()
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-with-toolbar.gif
|
||
|
:align: center
|
||
|
|
||
|
Position menu
|
||
|
=============
|
||
|
|
||
|
Bottom position
|
||
|
---------------
|
||
|
|
||
|
.. seealso::
|
||
|
|
||
|
:attr:`~MDDropdownMenu.position`
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.clock import Clock
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu
|
||
|
|
||
|
KV = '''
|
||
|
Screen
|
||
|
|
||
|
MDTextField:
|
||
|
id: field
|
||
|
pos_hint: {'center_x': .5, 'center_y': .5}
|
||
|
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 = [{"icon": "git", "text": f"Item {i}"} for i in range(5)]
|
||
|
self.menu = MDDropdownMenu(
|
||
|
caller=self.screen.ids.field,
|
||
|
items=menu_items,
|
||
|
position="bottom",
|
||
|
width_mult=4,
|
||
|
)
|
||
|
self.menu.bind(on_release=self.set_item)
|
||
|
|
||
|
def set_item(self, instance_menu, instance_menu_item):
|
||
|
def set_item(interval):
|
||
|
self.screen.ids.field.text = instance_menu_item.text
|
||
|
instance_menu.dismiss()
|
||
|
Clock.schedule_once(set_item, 0.5)
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.gif
|
||
|
:align: center
|
||
|
|
||
|
Center position
|
||
|
---------------
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.lang import Builder
|
||
|
|
||
|
from kivymd.app import MDApp
|
||
|
from kivymd.uix.menu import MDDropdownMenu
|
||
|
|
||
|
KV = '''
|
||
|
Screen
|
||
|
|
||
|
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 = [{"icon": "git", "text": f"Item {i}"} for i in range(5)]
|
||
|
self.menu = MDDropdownMenu(
|
||
|
caller=self.screen.ids.drop_item,
|
||
|
items=menu_items,
|
||
|
position="center",
|
||
|
width_mult=4,
|
||
|
)
|
||
|
self.menu.bind(on_release=self.set_item)
|
||
|
|
||
|
def set_item(self, instance_menu, instance_menu_item):
|
||
|
self.screen.ids.drop_item.set_item(instance_menu_item.text)
|
||
|
self.menu.dismiss()
|
||
|
|
||
|
def build(self):
|
||
|
return self.screen
|
||
|
|
||
|
|
||
|
Test().run()
|
||
|
|
||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position-center.gif
|
||
|
:align: center
|
||
|
"""
|
||
|
|
||
|
__all__ = ("MDDropdownMenu", "RightContent")
|
||
|
|
||
|
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 (
|
||
|
ListProperty,
|
||
|
NumericProperty,
|
||
|
ObjectProperty,
|
||
|
OptionProperty,
|
||
|
StringProperty,
|
||
|
)
|
||
|
from kivy.uix.floatlayout import FloatLayout
|
||
|
from kivy.uix.scrollview import ScrollView
|
||
|
|
||
|
import kivymd.material_resources as m_res
|
||
|
from kivymd.theming import ThemableBehavior
|
||
|
from kivymd.uix.behaviors import HoverBehavior
|
||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
||
|
from kivymd.uix.list import (
|
||
|
IRightBodyTouch,
|
||
|
OneLineAvatarIconListItem,
|
||
|
OneLineListItem,
|
||
|
OneLineRightIconListItem,
|
||
|
)
|
||
|
|
||
|
Builder.load_string(
|
||
|
"""
|
||
|
#:import STD_INC kivymd.material_resources.STANDARD_INCREMENT
|
||
|
|
||
|
|
||
|
<RightContent>
|
||
|
adaptive_width: True
|
||
|
|
||
|
|
||
|
<MDMenuItemIcon>
|
||
|
|
||
|
IconLeftWidget:
|
||
|
id: icon_widget
|
||
|
icon: root.icon
|
||
|
|
||
|
|
||
|
<MDMenu>
|
||
|
size_hint: None, None
|
||
|
width: root.width_mult * STD_INC
|
||
|
bar_width: 0
|
||
|
|
||
|
MDGridLayout:
|
||
|
id: box
|
||
|
cols: 1
|
||
|
adaptive_height: True
|
||
|
|
||
|
|
||
|
<MDDropdownMenu>
|
||
|
|
||
|
MDCard:
|
||
|
id: card
|
||
|
elevation: 10
|
||
|
size_hint: None, None
|
||
|
size: md_menu.size
|
||
|
pos: md_menu.pos
|
||
|
md_bg_color: 0, 0, 0, 0
|
||
|
opacity: md_menu.opacity
|
||
|
|
||
|
canvas:
|
||
|
Clear
|
||
|
Color:
|
||
|
rgba: root.background_color if root.background_color else root.theme_cls.bg_dark
|
||
|
RoundedRectangle:
|
||
|
size: self.size
|
||
|
pos: self.pos
|
||
|
radius: [root.radius,]
|
||
|
|
||
|
MDMenu:
|
||
|
id: md_menu
|
||
|
drop_cls: root
|
||
|
width_mult: root.width_mult
|
||
|
size_hint: None, None
|
||
|
size: 0, 0
|
||
|
opacity: 0
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
|
||
|
class RightContent(IRightBodyTouch, MDBoxLayout):
|
||
|
text = StringProperty()
|
||
|
"""
|
||
|
Text item.
|
||
|
|
||
|
:attr:`text` is a :class:`~kivy.properties.StringProperty`
|
||
|
and defaults to `''`.
|
||
|
"""
|
||
|
|
||
|
icon = StringProperty()
|
||
|
"""
|
||
|
Icon item.
|
||
|
|
||
|
:attr:`icon` is a :class:`~kivy.properties.StringProperty`
|
||
|
and defaults to `''`.
|
||
|
"""
|
||
|
|
||
|
|
||
|
class MDMenuItemBase(HoverBehavior):
|
||
|
"""
|
||
|
Base class for MenuItem
|
||
|
"""
|
||
|
|
||
|
def on_enter(self):
|
||
|
self.parent.parent.drop_cls.set_bg_color_items(self)
|
||
|
self.parent.parent.drop_cls.dispatch("on_enter", self)
|
||
|
|
||
|
def on_leave(self):
|
||
|
self.parent.parent.drop_cls.dispatch("on_leave", self)
|
||
|
|
||
|
|
||
|
class MDMenuItemIcon(MDMenuItemBase, OneLineAvatarIconListItem):
|
||
|
icon = StringProperty()
|
||
|
"""
|
||
|
Icon item.
|
||
|
|
||
|
:attr:`icon` is a :class:`~kivy.properties.StringProperty`
|
||
|
and defaults to `''`.
|
||
|
"""
|
||
|
|
||
|
|
||
|
class MDMenuItem(MDMenuItemBase, OneLineListItem):
|
||
|
|
||
|
pass
|
||
|
|
||
|
|
||
|
class MDMenuItemRight(MDMenuItemBase, OneLineRightIconListItem):
|
||
|
|
||
|
pass
|
||
|
|
||
|
|
||
|
class MDMenu(ScrollView):
|
||
|
width_mult = NumericProperty(1)
|
||
|
"""
|
||
|
See :attr:`~MDDropdownMenu.width_mult`.
|
||
|
"""
|
||
|
|
||
|
drop_cls = ObjectProperty()
|
||
|
"""
|
||
|
See :class:`~MDDropdownMenu` class.
|
||
|
"""
|
||
|
|
||
|
|
||
|
class MDDropdownMenu(ThemableBehavior, FloatLayout):
|
||
|
"""
|
||
|
:Events:
|
||
|
:attr:`on_enter`
|
||
|
Call when mouse enter the bbox of item menu.
|
||
|
:attr:`on_leave`
|
||
|
Call when the mouse exit the item menu.
|
||
|
:attr:`on_dismiss`
|
||
|
Call when closes menu.
|
||
|
:attr:`on_release`
|
||
|
The method that will be called when you click menu items.
|
||
|
"""
|
||
|
|
||
|
selected_color = ListProperty()
|
||
|
"""Custom color (``rgba`` format) for list item when hover behavior occurs.
|
||
|
|
||
|
:attr:`selected_color` is a :class:`~kivy.properties.ListProperty`
|
||
|
and defaults to `[]`.
|
||
|
"""
|
||
|
|
||
|
items = ListProperty()
|
||
|
"""
|
||
|
See :attr:`~kivy.uix.recycleview.RecycleView.data`.
|
||
|
|
||
|
:attr:`items` is a :class:`~kivy.properties.ListProperty`
|
||
|
and defaults to `[]`.
|
||
|
"""
|
||
|
|
||
|
width_mult = NumericProperty(1)
|
||
|
"""
|
||
|
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.
|
||
|
|
||
|
:attr:`width_mult` is a :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `1`.
|
||
|
"""
|
||
|
|
||
|
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.
|
||
|
|
||
|
: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'`.
|
||
|
|
||
|
: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'`.
|
||
|
|
||
|
:attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
|
||
|
and defaults to `None`.
|
||
|
"""
|
||
|
|
||
|
background_color = ListProperty()
|
||
|
"""
|
||
|
Color of the background of the menu.
|
||
|
|
||
|
:attr:`background_color` is a :class:`~kivy.properties.ListProperty`
|
||
|
and defaults to `[]`.
|
||
|
"""
|
||
|
|
||
|
opening_transition = StringProperty("out_cubic")
|
||
|
"""
|
||
|
Type of animation for opening a menu window.
|
||
|
|
||
|
:attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
|
||
|
and defaults to `'out_cubic'`.
|
||
|
"""
|
||
|
|
||
|
opening_time = NumericProperty(0.2)
|
||
|
"""
|
||
|
Menu window opening animation time.
|
||
|
|
||
|
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `0.2`.
|
||
|
"""
|
||
|
|
||
|
caller = ObjectProperty()
|
||
|
"""
|
||
|
The widget object that caller the menu window.
|
||
|
|
||
|
:attr:`caller` is a :class:`~kivy.properties.ObjectProperty`
|
||
|
and defaults to `None`.
|
||
|
"""
|
||
|
|
||
|
position = OptionProperty("auto", options=["auto", "center", "bottom"])
|
||
|
"""
|
||
|
Menu window position relative to parent element.
|
||
|
Available options are: `'auto'`, `'center'`, `'bottom'`.
|
||
|
|
||
|
:attr:`position` is a :class:`~kivy.properties.OptionProperty`
|
||
|
and defaults to `'auto'`.
|
||
|
"""
|
||
|
|
||
|
radius = NumericProperty(7)
|
||
|
"""
|
||
|
Menu radius.
|
||
|
|
||
|
:attr:`radius` is a :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `'7'`.
|
||
|
"""
|
||
|
|
||
|
_start_coords = []
|
||
|
_calculate_complete = False
|
||
|
_calculate_process = False
|
||
|
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
Window.bind(on_resize=self.check_position_caller)
|
||
|
Window.bind(on_maximize=self.set_menu_properties)
|
||
|
Window.bind(on_restore=self.set_menu_properties)
|
||
|
self.register_event_type("on_dismiss")
|
||
|
self.register_event_type("on_enter")
|
||
|
self.register_event_type("on_leave")
|
||
|
self.register_event_type("on_release")
|
||
|
self.menu = self.ids.md_menu
|
||
|
self.target_height = 0
|
||
|
|
||
|
def check_position_caller(self, instance, width, height):
|
||
|
self.set_menu_properties(0)
|
||
|
|
||
|
def set_bg_color_items(self, instance_selected_item):
|
||
|
"""Called when a Hover Behavior event occurs for a list item.
|
||
|
|
||
|
:type instance_selected_item: <kivymd.uix.menu.MDMenuItemIcon object>
|
||
|
"""
|
||
|
|
||
|
if self.selected_color:
|
||
|
for item in self.menu.ids.box.children:
|
||
|
if item is not instance_selected_item:
|
||
|
item.bg_color = (0, 0, 0, 0)
|
||
|
else:
|
||
|
instance_selected_item.bg_color = self.selected_color
|
||
|
|
||
|
def create_menu_items(self):
|
||
|
"""Creates menu items."""
|
||
|
|
||
|
for data in self.items:
|
||
|
if data.get("icon") and data.get("right_content_cls", None):
|
||
|
|
||
|
item = MDMenuItemIcon(
|
||
|
text=data.get("text", ""),
|
||
|
divider=data.get("divider", "Full"),
|
||
|
_txt_top_pad=data.get("top_pad", "20dp"),
|
||
|
_txt_bot_pad=data.get("bot_pad", "20dp"),
|
||
|
)
|
||
|
|
||
|
elif data.get("icon"):
|
||
|
item = MDMenuItemIcon(
|
||
|
text=data.get("text", ""),
|
||
|
divider=data.get("divider", "Full"),
|
||
|
_txt_top_pad=data.get("top_pad", "20dp"),
|
||
|
_txt_bot_pad=data.get("bot_pad", "20dp"),
|
||
|
)
|
||
|
|
||
|
elif data.get("right_content_cls", None):
|
||
|
item = MDMenuItemRight(
|
||
|
text=data.get("text", ""),
|
||
|
divider=data.get("divider", "Full"),
|
||
|
_txt_top_pad=data.get("top_pad", "20dp"),
|
||
|
_txt_bot_pad=data.get("bot_pad", "20dp"),
|
||
|
)
|
||
|
|
||
|
else:
|
||
|
|
||
|
item = MDMenuItem(
|
||
|
text=data.get("text", ""),
|
||
|
divider=data.get("divider", "Full"),
|
||
|
_txt_top_pad=data.get("top_pad", "20dp"),
|
||
|
_txt_bot_pad=data.get("bot_pad", "20dp"),
|
||
|
)
|
||
|
|
||
|
# Set height item.
|
||
|
if data.get("height", ""):
|
||
|
item.height = data.get("height")
|
||
|
# Compensate icon area by some left padding.
|
||
|
if not data.get("icon"):
|
||
|
item._txt_left_pad = data.get("left_pad", "32dp")
|
||
|
# Set left icon.
|
||
|
else:
|
||
|
item.icon = data.get("icon", "")
|
||
|
item.bind(on_release=lambda x=item: self.dispatch("on_release", x))
|
||
|
right_content_cls = data.get("right_content_cls", None)
|
||
|
# Set right content.
|
||
|
if isinstance(right_content_cls, RightContent):
|
||
|
item.ids._right_container.width = right_content_cls.width + dp(
|
||
|
20
|
||
|
)
|
||
|
item.ids._right_container.padding = ("10dp", 0, 0, 0)
|
||
|
item.add_widget(right_content_cls)
|
||
|
else:
|
||
|
if "_right_container" in item.ids:
|
||
|
item.ids._right_container.width = 0
|
||
|
self.menu.ids.box.add_widget(item)
|
||
|
|
||
|
def set_menu_properties(self, interval=0):
|
||
|
"""Sets the size and position for the menu window."""
|
||
|
|
||
|
if self.caller:
|
||
|
if not self.menu.ids.box.children:
|
||
|
self.create_menu_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_x, self.caller.center_y
|
||
|
)
|
||
|
self.target_width = self.width_mult * m_res.STANDARD_INCREMENT
|
||
|
|
||
|
# If we're wider than the Window...
|
||
|
if self.target_width > Window.width:
|
||
|
# ...reduce our multiplier to max allowed.
|
||
|
self.target_width = (
|
||
|
int(Window.width / m_res.STANDARD_INCREMENT)
|
||
|
* m_res.STANDARD_INCREMENT
|
||
|
)
|
||
|
|
||
|
# Set the target_height of the menu depending on the size of
|
||
|
# each MDMenuItem or MDMenuItemIcon
|
||
|
self.target_height = 0
|
||
|
for item in self.menu.ids.box.children:
|
||
|
self.target_height += item.height
|
||
|
|
||
|
# If we're over max_height...
|
||
|
if 0 < self.max_height < self.target_height:
|
||
|
self.target_height = self.max_height
|
||
|
|
||
|
# Establish vertical growth direction.
|
||
|
if self.ver_growth is not None:
|
||
|
ver_growth = self.ver_growth
|
||
|
else:
|
||
|
# If there's enough space below us:
|
||
|
if (
|
||
|
self.target_height
|
||
|
<= self._start_coords[1] - self.border_margin
|
||
|
):
|
||
|
ver_growth = "down"
|
||
|
# if there's enough space above us:
|
||
|
elif (
|
||
|
self.target_height
|
||
|
< Window.height - self._start_coords[1] - self.border_margin
|
||
|
):
|
||
|
ver_growth = "up"
|
||
|
# Otherwise, let's pick the one with more space and adjust ourselves.
|
||
|
else:
|
||
|
# If there"s more space below us:
|
||
|
if (
|
||
|
self._start_coords[1]
|
||
|
>= Window.height - self._start_coords[1]
|
||
|
):
|
||
|
ver_growth = "down"
|
||
|
self.target_height = (
|
||
|
self._start_coords[1] - self.border_margin
|
||
|
)
|
||
|
# If there's more space above us:
|
||
|
else:
|
||
|
ver_growth = "up"
|
||
|
self.target_height = (
|
||
|
Window.height
|
||
|
- self._start_coords[1]
|
||
|
- self.border_margin
|
||
|
)
|
||
|
|
||
|
if self.hor_growth is not None:
|
||
|
hor_growth = self.hor_growth
|
||
|
else:
|
||
|
# If there's enough space to the right:
|
||
|
if (
|
||
|
self.target_width
|
||
|
<= Window.width - self._start_coords[0] - self.border_margin
|
||
|
):
|
||
|
hor_growth = "right"
|
||
|
# if there's enough space to the left:
|
||
|
elif (
|
||
|
self.target_width
|
||
|
< self._start_coords[0] - self.border_margin
|
||
|
):
|
||
|
hor_growth = "left"
|
||
|
# Otherwise, let's pick the one with more space and adjust ourselves.
|
||
|
else:
|
||
|
# if there"s more space to the right:
|
||
|
if (
|
||
|
Window.width - self._start_coords[0]
|
||
|
>= self._start_coords[0]
|
||
|
):
|
||
|
hor_growth = "right"
|
||
|
self.target_width = (
|
||
|
Window.width
|
||
|
- self._start_coords[0]
|
||
|
- self.border_margin
|
||
|
)
|
||
|
# if there"s more space to the left:
|
||
|
else:
|
||
|
hor_growth = "left"
|
||
|
self.target_width = (
|
||
|
self._start_coords[0] - self.border_margin
|
||
|
)
|
||
|
|
||
|
if ver_growth == "down":
|
||
|
self.tar_y = self._start_coords[1] - self.target_height
|
||
|
else: # should always be "up"
|
||
|
self.tar_y = self._start_coords[1]
|
||
|
|
||
|
if hor_growth == "right":
|
||
|
self.tar_x = self._start_coords[0]
|
||
|
else: # should always be "left"
|
||
|
self.tar_x = self._start_coords[0] - self.target_width
|
||
|
self._calculate_complete = True
|
||
|
|
||
|
def open(self):
|
||
|
"""Animate the opening of a menu window."""
|
||
|
|
||
|
def open(interval):
|
||
|
if not self._calculate_complete:
|
||
|
return
|
||
|
if self.position == "auto":
|
||
|
self.menu.pos = self._start_coords
|
||
|
anim = Animation(
|
||
|
x=self.tar_x,
|
||
|
y=self.tar_y,
|
||
|
width=self.target_width,
|
||
|
height=self.target_height,
|
||
|
duration=self.opening_time,
|
||
|
opacity=1,
|
||
|
transition=self.opening_transition,
|
||
|
)
|
||
|
anim.start(self.menu)
|
||
|
else:
|
||
|
if self.position == "center":
|
||
|
self.menu.pos = (
|
||
|
self._start_coords[0] - self.target_width / 2,
|
||
|
self._start_coords[1] - self.target_height / 2,
|
||
|
)
|
||
|
elif self.position == "bottom":
|
||
|
self.menu.pos = (
|
||
|
self._start_coords[0] - self.target_width / 2,
|
||
|
self.caller.pos[1] - self.target_height,
|
||
|
)
|
||
|
anim = Animation(
|
||
|
width=self.target_width,
|
||
|
height=self.target_height,
|
||
|
duration=self.opening_time,
|
||
|
opacity=1,
|
||
|
transition=self.opening_transition,
|
||
|
)
|
||
|
anim.start(self.menu)
|
||
|
Window.add_widget(self)
|
||
|
Clock.unschedule(open)
|
||
|
self._calculate_process = False
|
||
|
|
||
|
self.set_menu_properties()
|
||
|
if not self._calculate_process:
|
||
|
self._calculate_process = True
|
||
|
Clock.schedule_interval(open, 0)
|
||
|
|
||
|
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 on_enter(self, instance):
|
||
|
"""Call when mouse enter the bbox of the item of menu."""
|
||
|
|
||
|
def on_leave(self, instance):
|
||
|
"""Call when the mouse exit the item of menu."""
|
||
|
|
||
|
def on_release(self, *args):
|
||
|
"""The method that will be called when you click menu items."""
|
||
|
|
||
|
def on_dismiss(self):
|
||
|
"""Called when the menu is closed."""
|
||
|
|
||
|
Window.remove_widget(self)
|
||
|
self.menu.width = 0
|
||
|
self.menu.height = 0
|
||
|
self.menu.opacity = 0
|
||
|
|
||
|
def dismiss(self):
|
||
|
"""Closes the menu."""
|
||
|
|
||
|
self.on_dismiss()
|