'''Splitter
======

.. versionadded:: 1.5.0

.. image:: images/splitter.jpg
    :align: right

The :class:`Splitter` is a widget that helps you re-size its child
widget/layout by letting you re-size it via dragging the boundary or
double tapping the boundary. This widget is similar to the
:class:`~kivy.uix.scrollview.ScrollView` in that it allows only one
child widget.

Usage::

    splitter = Splitter(sizable_from = 'right')
    splitter.add_widget(layout_or_widget_instance)
    splitter.min_size = 100
    splitter.max_size = 250

To change the size of the strip/border used for resizing::

    splitter.strip_size = '10pt'

To change its appearance::

    splitter.strip_cls = your_custom_class

You can also change the appearance of the `strip_cls`, which defaults to
:class:`SplitterStrip`, by overriding the `kv` rule in your app:

.. code-block:: kv

    <SplitterStrip>:
        horizontal: True if self.parent and self.parent.sizable_from[0] \
in ('t', 'b') else False
        background_normal: 'path to normal horizontal image' \
if self.horizontal else 'path to vertical normal image'
        background_down: 'path to pressed horizontal image' \
if self.horizontal else 'path to vertical pressed image'

'''


__all__ = ('Splitter', )

from kivy.factory import Factory
from kivy.uix.button import Button
from kivy.properties import (OptionProperty, NumericProperty, ObjectProperty,
                             ListProperty, BooleanProperty)
from kivy.uix.boxlayout import BoxLayout


class SplitterStrip(Button):
    '''Class used for the graphical representation of a
    :class:`kivy.uix.splitter.SplitterStripe`.
    '''
    pass


class Splitter(BoxLayout):
    '''See module documentation.

    :Events:
        `on_press`:
            Fired when the splitter is pressed.
        `on_release`:
            Fired when the splitter is released.

    .. versionchanged:: 1.6.0
        Added `on_press` and `on_release` events.

    '''

    border = ListProperty([4, 4, 4, 4])
    '''Border used for the
    :class:`~kivy.graphics.vertex_instructions.BorderImage`
    graphics instruction.

    This must be a list of four values: (bottom, right, top, left).
    Read the BorderImage instructions for more information about how
    to use it.

    :attr:`border` is a :class:`~kivy.properties.ListProperty` and
    defaults to (4, 4, 4, 4).
    '''

    strip_cls = ObjectProperty(SplitterStrip)
    '''Specifies the class of the resize Strip.

    :attr:`strip_cls` is an :class:`kivy.properties.ObjectProperty` and
    defaults to :class:`~kivy.uix.splitter.SplitterStrip`, which is of type
    :class:`~kivy.uix.button.Button`.

    .. versionchanged:: 1.8.0
        If you set a string, the :class:`~kivy.factory.Factory` will be used to
        resolve the class.

    '''

    sizable_from = OptionProperty('left', options=(
        'left', 'right', 'top', 'bottom'))
    '''Specifies whether the widget is resizable. Options are:
    `left`, `right`, `top` or `bottom`

    :attr:`sizable_from` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `left`.
    '''

    strip_size = NumericProperty('10pt')
    '''Specifies the size of resize strip

    :attr:`strp_size` is a :class:`~kivy.properties.NumericProperty`
    defaults to `10pt`
    '''

    min_size = NumericProperty('100pt')
    '''Specifies the minimum size beyond which the widget is not resizable.

    :attr:`min_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to `100pt`.
    '''

    max_size = NumericProperty('500pt')
    '''Specifies the maximum size beyond which the widget is not resizable.

    :attr:`max_size` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `500pt`.
    '''

    _parent_proportion = NumericProperty(0.)
    '''(internal) Specifies the distance that the slider has travelled
    across its parent, used to automatically maintain a sensible
    position if the parent is resized.

    :attr:`_parent_proportion` is a
    :class:`~kivy.properties.NumericProperty` and defaults to 0.

    .. versionadded:: 1.9.0
    '''

    _bound_parent = ObjectProperty(None, allownone=True)
    '''(internal) References the widget whose size is currently being
    tracked by :attr:`_parent_proportion`.

    :attr:`_bound_parent` is a
    :class:`~kivy.properties.ObjectProperty` and defaults to None.

    .. versionadded:: 1.9.0
    '''

    keep_within_parent = BooleanProperty(False)
    '''If True, will limit the splitter to stay within its parent widget.

    :attr:`keep_within_parent` is a
    :class:`~kivy.properties.BooleanProperty` and defaults to False.

    .. versionadded:: 1.9.0
    '''

    rescale_with_parent = BooleanProperty(False)
    '''If True, will automatically change size to take up the same
    proportion of the parent widget when it is resized, while
    staying within :attr:`min_size` and :attr:`max_size`. As long as
    these attributes can be satisfied, this stops the
    :class:`Splitter` from exceeding the parent size during rescaling.

    :attr:`rescale_with_parent` is a
    :class:`~kivy.properties.BooleanProperty` and defaults to False.

    .. versionadded:: 1.9.0
    '''

    __events__ = ('on_press', 'on_release')

    def __init__(self, **kwargs):
        self._container = None
        self._strip = None
        super(Splitter, self).__init__(**kwargs)

        do_size = self._do_size
        fbind = self.fbind
        fbind('max_size', do_size)
        fbind('min_size', do_size)
        fbind('parent', self._rebind_parent)

    def on_sizable_from(self, instance, sizable_from):
        if not instance._container:
            return

        sup = super(Splitter, instance)
        _strp = instance._strip
        if _strp:
            # remove any previous binds
            _strp.unbind(on_touch_down=instance.strip_down)
            _strp.unbind(on_touch_move=instance.strip_move)
            _strp.unbind(on_touch_up=instance.strip_up)
            self.unbind(disabled=_strp.setter('disabled'))

            sup.remove_widget(instance._strip)

        cls = instance.strip_cls
        if not isinstance(_strp, cls):
            if isinstance(cls, str):
                cls = Factory.get(cls)
            instance._strip = _strp = cls()

        sz_frm = instance.sizable_from[0]
        if sz_frm in ('l', 'r'):
            _strp.size_hint = None, 1
            _strp.width = instance.strip_size
            instance.orientation = 'horizontal'
            instance.unbind(strip_size=_strp.setter('width'))
            instance.bind(strip_size=_strp.setter('width'))
        else:
            _strp.size_hint = 1, None
            _strp.height = instance.strip_size
            instance.orientation = 'vertical'
            instance.unbind(strip_size=_strp.setter('height'))
            instance.bind(strip_size=_strp.setter('height'))

        index = 1
        if sz_frm in ('r', 'b'):
            index = 0
        sup.add_widget(_strp, index)

        _strp.bind(on_touch_down=instance.strip_down)
        _strp.bind(on_touch_move=instance.strip_move)
        _strp.bind(on_touch_up=instance.strip_up)
        _strp.disabled = self.disabled
        self.bind(disabled=_strp.setter('disabled'))

    def add_widget(self, widget, index=0, *args, **kwargs):
        if self._container or not widget:
            return Exception('Splitter accepts only one Child')
        self._container = widget
        sz_frm = self.sizable_from[0]
        if sz_frm in ('l', 'r'):
            widget.size_hint_x = 1
        else:
            widget.size_hint_y = 1

        index = 0
        if sz_frm in ('r', 'b'):
            index = 1
        super(Splitter, self).add_widget(widget, index, *args, **kwargs)
        self.on_sizable_from(self, self.sizable_from)

    def remove_widget(self, widget, *args, **kwargs):
        super(Splitter, self).remove_widget(widget, *args, **kwargs)
        if widget == self._container:
            self._container = None

    def clear_widgets(self, *args, **kwargs):
        self.remove_widget(self._container)

    def strip_down(self, instance, touch):
        if not instance.collide_point(*touch.pos):
            return False
        touch.grab(self)
        self.dispatch('on_press')

    def on_press(self):
        pass

    def _rebind_parent(self, instance, new_parent):
        if self._bound_parent is not None:
            self._bound_parent.unbind(size=self.rescale_parent_proportion)
        if self.parent is not None:
            new_parent.bind(size=self.rescale_parent_proportion)
        self._bound_parent = new_parent
        self.rescale_parent_proportion()

    def rescale_parent_proportion(self, *args):
        if not self.parent:
            return
        if self.rescale_with_parent:
            parent_proportion = self._parent_proportion
            if self.sizable_from in ('top', 'bottom'):
                new_height = parent_proportion * self.parent.height
                self.height = max(self.min_size,
                                 min(new_height, self.max_size))
            else:
                new_width = parent_proportion * self.parent.width
                self.width = max(self.min_size, min(new_width, self.max_size))

    def _do_size(self, instance, value):
        if self.sizable_from[0] in ('l', 'r'):
            self.width = max(self.min_size, min(self.width, self.max_size))
        else:
            self.height = max(self.min_size, min(self.height, self.max_size))

    @staticmethod
    def _is_moving(sz_frm, diff, pos, minpos, maxpos):
        if sz_frm in ('l', 'b'):
            cmp = minpos
        else:
            cmp = maxpos
        if diff == 0:
            return False
        elif diff > 0 and pos <= cmp:
            return False
        elif diff < 0 and pos >= cmp:
            return False
        return True

    def strip_move(self, instance, touch):
        if touch.grab_current is not instance:
            return False
        max_size = self.max_size
        min_size = self.min_size
        sz_frm = self.sizable_from[0]

        if sz_frm in ('t', 'b'):
            diff_y = (touch.dy)
            self_y = self.y
            self_top = self.top
            if not self._is_moving(sz_frm, diff_y, touch.y, self_y, self_top):
                return
            if self.keep_within_parent:
                if sz_frm == 't' and (self_top + diff_y) > self.parent.top:
                    diff_y = self.parent.top - self_top
                elif sz_frm == 'b' and (self_y + diff_y) < self.parent.y:
                    diff_y = self.parent.y - self_y
            if sz_frm == 'b':
                diff_y *= -1
            if self.size_hint_y:
                self.size_hint_y = None
            if self.height > 0:
                self.height += diff_y
            else:
                self.height = 1

            height = self.height
            self.height = max(min_size, min(height, max_size))

            self._parent_proportion = self.height / self.parent.height
        else:
            diff_x = (touch.dx)
            self_x = self.x
            self_right = self.right
            if not self._is_moving(sz_frm, diff_x, touch.x, self_x, self_right):
                return
            if self.keep_within_parent:
                if sz_frm == 'l' and (self_x + diff_x) < self.parent.x:
                    diff_x = self.parent.x - self_x
                elif (sz_frm == 'r' and
                      (self_right + diff_x) > self.parent.right):
                    diff_x = self.parent.right - self_right
            if sz_frm == 'l':
                diff_x *= -1
            if self.size_hint_x:
                self.size_hint_x = None
            if self.width > 0:
                self.width += diff_x
            else:
                self.width = 1

            width = self.width
            self.width = max(min_size, min(width, max_size))

            self._parent_proportion = self.width / self.parent.width

    def strip_up(self, instance, touch):
        if touch.grab_current is not instance:
            return

        if touch.is_double_tap:
            max_size = self.max_size
            min_size = self.min_size
            sz_frm = self.sizable_from[0]
            s = self.size

            if sz_frm in ('t', 'b'):
                if self.size_hint_y:
                    self.size_hint_y = None
                if s[1] - min_size <= max_size - s[1]:
                    self.height = max_size
                else:
                    self.height = min_size
            else:
                if self.size_hint_x:
                    self.size_hint_x = None
                if s[0] - min_size <= max_size - s[0]:
                    self.width = max_size
                else:
                    self.width = min_size
        touch.ungrab(instance)
        self.dispatch('on_release')

    def on_release(self):
        pass


if __name__ == '__main__':
    from kivy.app import App
    from kivy.uix.button import Button
    from kivy.uix.floatlayout import FloatLayout

    class SplitterApp(App):

        def build(self):
            root = FloatLayout()
            bx = BoxLayout()
            bx.add_widget(Button())
            bx.add_widget(Button())
            bx2 = BoxLayout()
            bx2.add_widget(Button())
            bx2.add_widget(Button())
            bx2.add_widget(Button())
            spl = Splitter(
                size_hint=(1, .25),
                pos_hint={'top': 1},
                sizable_from='bottom')
            spl1 = Splitter(
                sizable_from='left',
                size_hint=(None, 1), width=90)
            spl1.add_widget(Button())
            bx.add_widget(spl1)
            spl.add_widget(bx)

            spl2 = Splitter(size_hint=(.25, 1))
            spl2.add_widget(bx2)
            spl2.sizable_from = 'right'
            root.add_widget(spl)
            root.add_widget(spl2)
            return root

    SplitterApp().run()
