Sideband/sbapp/kivymd/uix/menu/menu.py
2022-10-08 17:17:59 +02:00

1135 lines
32 KiB
Python
Executable File

"""
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 = '''
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}",
"viewclass": "OneLineListItem",
"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,
width_mult=4,
)
def menu_callback(self, text_item):
print(text_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()
Customization of menu item
--------------------------
Menu items are created in the same way as items for the
:class:`~kivy.uix.recycleview.RecycleView` class.
.. 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
user_font_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": f"R+{i}",
"right_icon": "apple-keyboard-command",
"left_icon": "git",
"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,
width_mult=4,
)
def menu_callback(self, text_item):
print(text_item)
def build(self):
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right.gif
: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>
orientation: "vertical"
adaptive_size: True
padding: "4dp"
MDBoxLayout:
spacing: "12dp"
adaptive_size: 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}",
"viewclass": "OneLineListItem",
"height": dp(56),
"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,
width_mult=4,
)
def menu_callback(self, text_item):
print(text_item)
def build(self):
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)`.
.. note:: This example uses drop down menus for both the righthand and
lefthand menus (i.e both the 'triple bar' and 'triple dot' menus) to
illustrate that it is possible. A better solution for the 'triple bar' menu
would probably have been :class:`~kivymd.uix.MDNavigationDrawer`.
.. 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):
menu_items = [
{
"viewclass": "OneLineListItem",
"text": f"Item {i}",
"height": dp(56),
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
self.menu = MDDropdownMenu(
items=menu_items,
width_mult=4,
)
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.gif
:align: center
.. Position:
Position
========
Bottom position
---------------
.. seealso::
:attr:`~MDDropdownMenu.position`
.. code-block:: python
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import StringProperty
from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
<IconListItem>
IconLeftWidget:
icon: root.icon
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 IconListItem(OneLineIconListItem):
icon = StringProperty()
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"viewclass": "IconListItem",
"icon": "git",
"height": dp(56),
"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",
width_mult=4,
)
def set_item(self, text__item):
self.screen.ids.field.text = text__item
self.menu.dismiss()
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 kivy.metrics import dp
from kivy.properties import StringProperty
from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
<IconListItem>
IconLeftWidget:
icon: root.icon
MDScreen
MDDropDownItem:
id: drop_item
pos_hint: {'center_x': .5, 'center_y': .5}
text: 'Item 0'
on_release: app.menu.open()
'''
class IconListItem(OneLineIconListItem):
icon = StringProperty()
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
"viewclass": "IconListItem",
"icon": "git",
"text": f"Item {i}",
"height": dp(56),
"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",
width_mult=4,
)
self.menu.bind()
def set_item(self, text_item):
self.screen.ids.drop_item.set_item(text_item)
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",)
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
from kivy.properties import (
ColorProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
VariableListProperty,
)
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.recycleview import RecycleView
import kivymd.material_resources as m_res
from kivymd import uix_path
from kivymd.theming import ThemableBehavior
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`.
"""
drop_cls = ObjectProperty()
"""
See :class:`~MDDropdownMenu` class.
"""
class MDDropdownMenu(ThemableBehavior, FloatLayout):
"""
: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.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header-cls.png
:align: center
:attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
items = ListProperty()
"""
See :attr:`~kivy.uix.recycleview.RecycleView.data`.
.. code-block:: python
items = [
{
"viewclass": "OneLineListItem",
"height": dp(56),
"text": f"Item {i}",
}
for i in range(5)
]
self.menu = MDDropdownMenu(
items=items,
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-items.png
:align: center
: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.
.. code-block:: python
self.menu = MDDropdownMenu(
width_mult=4,
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-4.png
:align: center
.. code-block:: python
self.menu = MDDropdownMenu(
width_mult=8,
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-8.png
:align: center
: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.
.. code-block:: python
self.menu = MDDropdownMenu(
max_height=dp(112),
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-112.png
:align: center
.. code-block:: python
self.menu = MDDropdownMenu(
max_height=dp(224),
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-224.png
:align: center
: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(4),
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-border-margin-4.png
:align: center
.. 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.gif
: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.gif
: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.gif
: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.gif
:align: center
:attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
background_color = ColorProperty(None)
"""
Color of the background of the menu.
.. code-block:: python
self.menu = MDDropdownMenu(
background_color=self.theme_cls.primary_light,
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-background-color.png
:align: center
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
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 and you can set it to 0
if you don't want animation of menu opening.
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
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'`, `'center'`, `'bottom'`.
See Position_ for more information.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png
:align: center
:attr:`position` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'auto'`.
"""
radius = VariableListProperty([dp(7)])
"""
Menu radius.
.. code-block:: python
self.menu = MDDropdownMenu(
radius=[24, 0, 24, 0],
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-radius.png
:align: center
:attr:`radius` is a :class:`~kivy.properties.VariableListProperty`
and defaults to `'[dp(7)]'`.
"""
elevation = NumericProperty(4)
"""
Elevation value of menu dialog.
.. versionadded:: 1.0.0
.. code-block:: python
self.menu = MDDropdownMenu(
elevation=4,
...,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-elevation.png
:align: center
:attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
and defaults to `4`.
"""
_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)
Clock.schedule_once(self.ajust_radius)
self.register_event_type("on_dismiss")
self.menu = self.ids.md_menu
self.target_height = 0
def check_position_caller(
self, instance_window: WindowSDL, width: int, height: int
) -> None:
"""Called when the application root window is resized."""
# FIXME: Menu position is not recalculated when changing the size of
# the root application window.
self.set_menu_properties(0)
def set_menu_properties(self, interval: Union[int, float] = 0) -> None:
"""Sets the size and position for the menu window."""
if self.caller:
self.ids.md_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_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.ids.md_menu.data:
self.target_height += item.get("height", dp(72))
# 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 ajust_radius(self, interval: Union[int, float]) -> None:
"""
Adjusts the radius of the first and last items in the menu list
according to the radius that is set for the menu.
"""
if self.items:
radius_for_firt_item = self.radius[:2]
radius_for_last_item = self.radius[2:]
firt_data_item = self.items[0]
last_data_item = self.items[-1]
firt_data_item["radius"] = radius_for_firt_item + [0, 0]
last_data_item["radius"] = [0, 0] + radius_for_last_item
last_data_item["divider"] = None
self.items[0] = firt_data_item
self.items[-1] = last_data_item
# For all other elements of the list, except for the first and
# last, we set the value of the radius to `0`.
for i, data_item in enumerate(self.items):
if "radius" not in data_item:
data_item["radius"] = 0
self.items[i] = data_item
def adjust_position(self) -> str:
"""
Returns value 'auto' for the menu position if the menu position is out
of screen.
"""
target_width = self.target_width
target_height = self.target_height
caller = self.caller
position = self.position
if (
caller.x < target_width
or caller.x + target_width > Window.width
or caller.y + target_height > Window.height
or (caller.y < target_height and position == "center")
):
position = "auto"
if self.hor_growth or self.ver_growth:
self.hor_growth = None
self.ver_growth = None
self.set_menu_properties()
return position
def open(self) -> None:
"""Animate the opening of a menu window."""
def open(interval):
if not self._calculate_complete:
return
position = self.adjust_position()
if position == "auto":
self.menu.pos = self._start_coords
anim = Animation(
x=self.tar_x,
y=self.tar_y
- (self.header_cls.height if self.header_cls else 0),
width=self.target_width,
height=self.target_height,
duration=self.opening_time,
opacity=1,
transition=self.opening_transition,
)
anim.start(self.menu)
else:
if position == "center":
self.menu.pos = (
self._start_coords[0] - self.target_width / 2,
self._start_coords[1] - self.target_height / 2,
)
elif position == "bottom":
self.menu.pos = (
self._start_coords[0] - self.target_width / 2,
self.caller.pos[1] - self.target_height,
)
elif position == "top":
self.menu.pos = (
self._start_coords[0] - self.target_width / 2,
self.caller.pos[1] + self.caller.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_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 on_dismiss(self) -> None:
"""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, *args) -> None:
"""Closes the menu."""
self.on_dismiss()
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 = [
{
"viewclass": "OneLineListItem",
"height": dp(56),
"text": f"Item {i}",
}
for i in range(5)
]
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()