""" Components/Menu =============== .. seealso:: `Material Design spec, Menus `_ .. 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 = ''' 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 = ''' 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} 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()