''' Drop-Down List ============== .. image:: images/dropdown.gif :align: right .. versionadded:: 1.4.0 A versatile drop-down list that can be used with custom widgets. It allows you to display a list of widgets under a displayed widget. Unlike other toolkits, the list of widgets can contain any type of widget: simple buttons, images etc. The positioning of the drop-down list is fully automatic: we will always try to place the dropdown list in a way that the user can select an item in the list. Basic example ------------- A button with a dropdown list of 10 possible values. All the buttons within the dropdown list will trigger the dropdown :meth:`DropDown.select` method. After being called, the main button text will display the selection of the dropdown. :: from kivy.uix.dropdown import DropDown from kivy.uix.button import Button from kivy.base import runTouchApp # create a dropdown with 10 buttons dropdown = DropDown() for index in range(10): # When adding widgets, we need to specify the height manually # (disabling the size_hint_y) so the dropdown can calculate # the area it needs. btn = Button(text='Value %d' % index, size_hint_y=None, height=44) # for each button, attach a callback that will call the select() method # on the dropdown. We'll pass the text of the button as the data of the # selection. btn.bind(on_release=lambda btn: dropdown.select(btn.text)) # then add the button inside the dropdown dropdown.add_widget(btn) # create a big main button mainbutton = Button(text='Hello', size_hint=(None, None)) # show the dropdown menu when the main button is released # note: all the bind() calls pass the instance of the caller (here, the # mainbutton instance) as the first argument of the callback (here, # dropdown.open.). mainbutton.bind(on_release=dropdown.open) # one last thing, listen for the selection in the dropdown list and # assign the data to the button text. dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x)) runTouchApp(mainbutton) Extending dropdown in Kv ------------------------ You could create a dropdown directly from your kv:: #:kivy 1.4.0 : Button: text: 'My first Item' size_hint_y: None height: 44 on_release: root.select('item1') Label: text: 'Unselectable item' size_hint_y: None height: 44 Button: text: 'My second Item' size_hint_y: None height: 44 on_release: root.select('item2') And then, create the associated python class and use it:: class CustomDropDown(DropDown): pass dropdown = CustomDropDown() mainbutton = Button(text='Hello', size_hint=(None, None)) mainbutton.bind(on_release=dropdown.open) dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x)) ''' __all__ = ('DropDown', ) from kivy.uix.scrollview import ScrollView from kivy.properties import ObjectProperty, NumericProperty, BooleanProperty from kivy.core.window import Window from kivy.lang import Builder from kivy.clock import Clock from kivy.config import Config _grid_kv = ''' GridLayout: size_hint_y: None height: self.minimum_size[1] cols: 1 ''' class DropDownException(Exception): '''DropDownException class. ''' pass class DropDown(ScrollView): '''DropDown class. See module documentation for more information. :Events: `on_select`: data Fired when a selection is done. The data of the selection is passed in as the first argument and is what you pass in the :meth:`select` method as the first argument. `on_dismiss`: .. versionadded:: 1.8.0 Fired when the DropDown is dismissed, either on selection or on touching outside the widget. ''' auto_width = BooleanProperty(True) '''By default, the width of the dropdown will be the same as the width of the attached widget. Set to False if you want to provide your own width. :attr:`auto_width` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' max_height = NumericProperty(None, allownone=True) '''Indicate the maximum height that the dropdown can take. If None, it will take the maximum height available until the top or bottom of the screen is reached. :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and defaults to None. ''' dismiss_on_select = BooleanProperty(True) '''By default, the dropdown will be automatically dismissed when a selection has been done. Set to False to prevent the dismiss. :attr:`dismiss_on_select` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' auto_dismiss = BooleanProperty(True) '''By default, the dropdown will be automatically dismissed when a touch happens outside of it, this option allows to disable this feature :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. .. versionadded:: 1.8.0 ''' min_state_time = NumericProperty(0) '''Minimum time before the :class:`~kivy.uix.DropDown` is dismissed. This is used to allow for the widget inside the dropdown to display a down state or for the :class:`~kivy.uix.DropDown` itself to display a animation for closing. :attr:`min_state_time` is a :class:`~kivy.properties.NumericProperty` and defaults to the `Config` value `min_state_time`. .. versionadded:: 1.10.0 ''' attach_to = ObjectProperty(allownone=True) '''(internal) Property that will be set to the widget to which the drop down list is attached. The :meth:`open` method will automatically set this property whilst :meth:`dismiss` will set it back to None. ''' container = ObjectProperty() '''(internal) Property that will be set to the container of the dropdown list. It is a :class:`~kivy.uix.gridlayout.GridLayout` by default. ''' _touch_started_inside = None __events__ = ('on_select', 'on_dismiss') def __init__(self, **kwargs): self._win = None if 'min_state_time' not in kwargs: self.min_state_time = float( Config.get('graphics', 'min_state_time')) if 'container' not in kwargs: c = self.container = Builder.load_string(_grid_kv) else: c = None if 'do_scroll_x' not in kwargs: self.do_scroll_x = False if 'size_hint' not in kwargs: if 'size_hint_x' not in kwargs: self.size_hint_x = None if 'size_hint_y' not in kwargs: self.size_hint_y = None super(DropDown, self).__init__(**kwargs) if c is not None: super(DropDown, self).add_widget(c) self.on_container(self, c) Window.bind( on_key_down=self.on_key_down, size=self._reposition) self.fbind('size', self._reposition) def on_key_down(self, instance, key, scancode, codepoint, modifiers): if key == 27 and self.get_parent_window(): self.dismiss() return True def on_container(self, instance, value): if value is not None: self.container.bind(minimum_size=self._reposition) def open(self, widget): '''Open the dropdown list and attach it to a specific widget. Depending on the position of the widget within the window and the height of the dropdown, the dropdown might be above or below that widget. ''' # ensure we are not already attached if self.attach_to is not None: self.dismiss() # we will attach ourself to the main window, so ensure the # widget we are looking for have a window self._win = widget.get_parent_window() if self._win is None: raise DropDownException( 'Cannot open a dropdown list on a hidden widget') self.attach_to = widget widget.bind(pos=self._reposition, size=self._reposition) self._reposition() # attach ourself to the main window self._win.add_widget(self) def dismiss(self, *largs): '''Remove the dropdown widget from the window and detach it from the attached widget. ''' Clock.schedule_once(self._real_dismiss, self.min_state_time) def _real_dismiss(self, *largs): if self.parent: self.parent.remove_widget(self) if self.attach_to: self.attach_to.unbind(pos=self._reposition, size=self._reposition) self.attach_to = None self.dispatch('on_dismiss') def on_dismiss(self): pass def select(self, data): '''Call this method to trigger the `on_select` event with the `data` selection. The `data` can be anything you want. ''' self.dispatch('on_select', data) if self.dismiss_on_select: self.dismiss() def on_select(self, data): pass def add_widget(self, *args, **kwargs): if self.container: return self.container.add_widget(*args, **kwargs) return super(DropDown, self).add_widget(*args, **kwargs) def remove_widget(self, *args, **kwargs): if self.container: return self.container.remove_widget(*args, **kwargs) return super(DropDown, self).remove_widget(*args, **kwargs) def clear_widgets(self, *args, **kwargs): if self.container: return self.container.clear_widgets(*args, **kwargs) return super(DropDown, self).clear_widgets(*args, **kwargs) def on_motion(self, etype, me): super().on_motion(etype, me) return True def on_touch_down(self, touch): self._touch_started_inside = self.collide_point(*touch.pos) if not self.auto_dismiss or self._touch_started_inside: super(DropDown, self).on_touch_down(touch) return True def on_touch_move(self, touch): if not self.auto_dismiss or self._touch_started_inside: super(DropDown, self).on_touch_move(touch) return True def on_touch_up(self, touch): # Explicitly test for False as None occurs when shown by on_touch_down if self.auto_dismiss and self._touch_started_inside is False: self.dismiss() else: super(DropDown, self).on_touch_up(touch) self._touch_started_inside = None return True def _reposition(self, *largs): # calculate the coordinate of the attached widget in the window # coordinate system win = self._win if not win: return widget = self.attach_to if not widget or not widget.get_parent_window(): return wx, wy = widget.to_window(*widget.pos) wright, wtop = widget.to_window(widget.right, widget.top) if self.auto_width: self.width = wright - wx # ensure the dropdown list doesn't get out on the X axis, with a # preference to 0 in case the list is too wide. x = wx if x + self.width > win.width: x = win.width - self.width if x < 0: x = 0 self.x = x # determine if we display the dropdown upper or lower to the widget if self.max_height is not None: height = min(self.max_height, self.container.minimum_height) else: height = self.container.minimum_height h_bottom = wy - height h_top = win.height - (wtop + height) if h_bottom > 0: self.top = wy self.height = height elif h_top > 0: self.y = wtop self.height = height else: # none of both top/bottom have enough place to display the # widget at the current size. Take the best side, and fit to # it. if h_top < h_bottom: self.top = self.height = wy else: self.y = wtop self.height = win.height - wtop if __name__ == '__main__': from kivy.uix.button import Button from kivy.base import runTouchApp def show_dropdown(button, *largs): dp = DropDown() dp.bind(on_select=lambda instance, x: setattr(button, 'text', x)) for i in range(10): item = Button(text='hello %d' % i, size_hint_y=None, height=44) item.bind(on_release=lambda btn: dp.select(btn.text)) dp.add_widget(item) dp.open(button) def touch_move(instance, touch): instance.center = touch.pos btn = Button(text='SHOW', size_hint=(None, None), pos=(300, 200)) btn.bind(on_release=show_dropdown, on_touch_move=touch_move) runTouchApp(btn)