431 lines
14 KiB
Python
431 lines
14 KiB
Python
'''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()
|