'''
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
    <CustomDropDown>:
        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)
