From 3a60614ae665e600730f09091ce4e9a3fd0b4412 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Fri, 2 Sep 2011 18:37:51 +0200 Subject: [PATCH] accordion: add new accordion widget with one little example. --- doc/sources/images/accordion.jpg | Bin 0 -> 11425 bytes examples/widgets/accordion_1.py | 15 ++ kivy/data/style.kv | 52 ++++ kivy/factory_registers.py | 2 + kivy/uix/accordion.py | 440 +++++++++++++++++++++++++++++++ 5 files changed, 509 insertions(+) create mode 100644 doc/sources/images/accordion.jpg create mode 100644 examples/widgets/accordion_1.py create mode 100644 kivy/uix/accordion.py diff --git a/doc/sources/images/accordion.jpg b/doc/sources/images/accordion.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16426edde5b9089fe793b3360162bd501620f9fc GIT binary patch literal 11425 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 literal 0 HcmV?d00001 diff --git a/examples/widgets/accordion_1.py b/examples/widgets/accordion_1.py new file mode 100644 index 000000000..8118ab205 --- /dev/null +++ b/examples/widgets/accordion_1.py @@ -0,0 +1,15 @@ +from kivy.uix.accordion import Accordion, AccordionItem +from kivy.uix.label import Label +from kivy.app import App + +class AccordionApp(App): + def build(self): + root = Accordion() + for x in xrange(5): + item = AccordionItem(title='Title %d' % x) + item.add_widget(Label(text='Very big content\n' * 10)) + root.add_widget(item) + return root + +if __name__ == '__main__': + AccordionApp().run() diff --git a/kivy/data/style.kv b/kivy/data/style.kv index bfc8e21d8..0f5c48d67 100644 --- a/kivy/data/style.kv +++ b/kivy/data/style.kv @@ -333,6 +333,57 @@ id: container +# ============================================================================= +# Accordion widget +# ============================================================================= + +[AccordionItemTitle@Label]: + text: ctx.title + canvas.before: + Color: + rgb: 1, 1, 1 + BorderImage: + source: + ctx.item.background_normal \ + if ctx.item.collapse \ + else ctx.item.background_selected + pos: self.pos + size: self.size + PushMatrix + Translate: + xy: self.center_x, self.center_y + Rotate: + angle: 90 if ctx.item.orientation == 'horizontal' else 0 + axis: 0, 0, 1 + Translate: + xy: -self.center_x, -self.center_y + canvas.after: + PopMatrix + + +: + container: container + container_title: container_title + + BoxLayout: + orientation: root.orientation + pos: root.pos + BoxLayout: + size_hint_x: None if root.orientation == 'horizontal' else 1 + size_hint_y: None if root.orientation == 'vertical' else 1 + width: root.min_space if root.orientation == 'horizontal' else 100 + height: root.min_space if root.orientation == 'vertical' else 100 + id: container_title + + StencilView: + id: sv + + BoxLayout: + id: container + pos: sv.pos + size: root.content_size + + # ============================================================================= # Settings # ============================================================================= @@ -491,3 +542,4 @@ size_hint_y: None id: content orientation: 'vertical' + diff --git a/kivy/factory_registers.py b/kivy/factory_registers.py index 251ca1290..b8f85c87c 100644 --- a/kivy/factory_registers.py +++ b/kivy/factory_registers.py @@ -74,6 +74,8 @@ r('ShapeRect', module='kivy.input.shape') r('AnchorLayout', module='kivy.uix.anchorlayout') r('BoxLayout', module='kivy.uix.boxlayout') r('GridLayout', module='kivy.uix.gridlayout') +r('Accordion', module='kivy.uix.accordion') +r('AccordionItem', module='kivy.uix.accordion') r('Button', module='kivy.uix.button') r('Camera', module='kivy.uix.camera') r('FloatLayout', module='kivy.uix.floatlayout') diff --git a/kivy/uix/accordion.py b/kivy/uix/accordion.py new file mode 100644 index 000000000..beafca04d --- /dev/null +++ b/kivy/uix/accordion.py @@ -0,0 +1,440 @@ +''' +Accordion +========= + +.. versionadded:: 1.0.8 + +.. warning:: + + This widget is still experimental, and his API is subject to change in a + future version. + +.. image:: images/accordion.jpg + :align: right + +The Accordion widget is a form of menu where the options are stacked either +vertically or horizontally, and the item in focus/when touched opens up +displaying his content. + +The :class:`Accordion` will contain one or many :class:`AccordionItem`, that +will contain one root content widget. You'll have a Tree like this: + +- Accordion + + - AccordionItem + + - YourContent + + - AccordionItem + + - BoxLayout + + - Another user content 1 + + - Another user content 2 + + - AccordionItem + + - Another user content + + +The current implementation divide the :class:`AccordionItem` in 2: + +#. One container for the title bar +#. One container for the content + +The title bar is made from a Kv template. We'll see how to create a new template +to customize the design of the title bar. + +.. warning:: + + If you see message like:: + + [WARNING] [Accordion] not have enough space for displaying all childrens + [WARNING] [Accordion] need 440px, got 100px + [WARNING] [Accordion] layout aborted. + + That's mean you have too many children, and they are no more space to + display any content. This is "normal", and nothing will be done. Try to + increase the space for the accordion, and reduce the number of children. You + can also reduce the :attr:`Accordion.min_space`. + +Simple example +-------------- + +.. include:: ../../examples/widgets/accordion_1.py + :literal: + +Customize the accordion +----------------------- + +You can increase the default size of the title bar:: + + root = Accordion(min_space=60) + +Or change the orientation to vertical:: + + root = Accordion(orientation='vertical') + +The item is more configurable, and you can set your own title background when +the item is collapsed or opened like:: + + item = AccordionItem(background_normal='image_when_collapsed.png', + background_selected='image_when_selected.png') + +''' + +__all__ = ('Accordion', 'AccordionItem', 'AccordionException') + +from kivy.animation import Animation +from kivy.uix.floatlayout import FloatLayout +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import ObjectProperty, StringProperty, \ + BooleanProperty, NumericProperty, ListProperty, OptionProperty, \ + DictProperty +from kivy.uix.widget import Widget +from kivy.logger import Logger + + +class AccordionException(Exception): + '''AccordionException class, that can be throwed anytime the accordion is + doing something bad. + ''' + pass + + +class AccordionItem(FloatLayout): + '''AccordionItem class, that must be used in conjunction with + :class:`Accordion` class. See module documentation for more information. + ''' + + title = StringProperty('') + '''Title string of the item. The title might be used with conjuction of the + `AccordionItemTitle` that use it. + If you are using a custom template, you can use that property as a text + entry, or not. By default, it's used for the title text. + + + :data:`title` is a :class:`~kivy.properties.StringProperty`, default to '' + ''' + + title_template = StringProperty('AccordionItemTitle') + '''Template to use for creating the title part of the accordion item. The + default template is a simple Label, not customizable (except the text) that + support vertical and horizontal orientation, and different background for + collapse and selected mode. + + It's better to create and use your own template if you want to do that is + not supported by the default template. + + :data:`title` is a :class:`~kivy.properties.StringProperty`, default to + 'AccordionItemTitle'. The current default template live in the + `kivy/data/style.kv` file. + + Here is the code if you want to start over to build your own template:: + + [AccordionItemTitle@Label]: + text: ctx.title + canvas.before: + Color: + rgb: 1, 1, 1 + BorderImage: + source: + ctx.item.background_normal \ + if ctx.item.collapse \ + else ctx.item.background_selected + pos: self.pos + size: self.size + PushMatrix + Translate: + xy: self.center_x, self.center_y + Rotate: + angle: 90 if ctx.item.orientation == 'horizontal' else 0 + axis: 0, 0, 1 + Translate: + xy: -self.center_x, -self.center_y + canvas.after: + PopMatrix + + + ''' + + title_args = DictProperty({}) + '''Default arguments that will be pass to the + :meth:`kivy.lang.Builder.template` method. + + :data:`title_args` is a :class:`~kivy.properties.DictProperty`, default to + {} + ''' + + collapse = BooleanProperty(True) + '''Boolean indicate if the current item is collapsed or not. + + :data:`collapse` is a :class:`~kivy.properties.BooleanProperty`, default to + True + ''' + + collapse_alpha = NumericProperty(1.) + '''Value between 0 and 1 indicate how much the item is collasped (1) or + selected (0). It's mostly used for animation. + + :data:`collapse_alpha` is a :class:`~kivy.properties.NumericProperty`, + default to 1. + ''' + + accordion = ObjectProperty(None) + '''Instance of the :class:`Accordion` that the item belong to. + + :data:`accordion` is an :class:`~kivy.properties.ObjectProperty`, default to + None. + ''' + + background_normal = StringProperty('data/images/button.png') + '''Background image of the accordion item used for default graphical + representation, when the item is collapsed. + + :data:`background_normal` is an :class:`~kivy.properties.StringProperty`, + default to 'data/images/button.png' + ''' + + background_selected = StringProperty('data/images/button_pressed.png') + '''Background image of the accordion item used for default graphical + representation, when the item is selected (not collapsed). + + :data:`background_normal` is an :class:`~kivy.properties.StringProperty`, + default to 'data/images/button_pressed.png' + ''' + + orientation = OptionProperty('vertical', options=( + 'horizontal', 'vertical')) + '''Link to the :attr:`Accordion.orientation` property. + ''' + + min_space = NumericProperty(44) + '''Link to the :attr:`Accordion.min_space` property. + ''' + + content_size = ListProperty([100, 100]) + '''(internal) Set by the :class:`Accordion` to the size allocated for the + content + ''' + + container = ObjectProperty(None) + '''(internal) Property that will be set to the container of children, inside + the AccordionItem representation. + ''' + + container_title = ObjectProperty(None) + '''(internal) Property that will be set to the container of title, inside + the AccordionItem representation. + ''' + + def __init__(self, **kwargs): + self._trigger_title = Clock.create_trigger(self._update_title, -1) + self._anim_collapse = None + super(AccordionItem, self).__init__(**kwargs) + self.bind(title=self._trigger_title, + title_template=self._trigger_title, + title_args=self._trigger_title) + self._trigger_title() + + def add_widget(self, widget): + if self.container is None: + return super(AccordionItem, self).add_widget(widget) + return self.container.add_widget(widget) + + def remove_widget(self, widget): + if self.container: + self.container.remove_widget(widget) + super(AccordionItem, self).remove_widget(widget) + + def on_collapse(self, instance, value): + accordion = self.accordion + if accordion is None: + return + if not value: + self.accordion.select(self) + collapse_alpha = float(value) + if self._anim_collapse: + self._anim_collapse.stop() + self._anim_collapse = None + if self.collapse_alpha != collapse_alpha: + self._anim_collapse = Animation( + collapse_alpha=collapse_alpha, + t=accordion.anim_func, + d=accordion.anim_duration).start(self) + + def on_collapse_alpha(self, instance, value): + self.accordion._trigger_layout() + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + return + if self.collapse: + self.collapse = False + return True + else: + return super(AccordionItem, self).on_touch_down(touch) + + def _update_title(self, dt): + if not self.container_title: + self._trigger_title() + return + c = self.container_title + c.clear_widgets() + instance = Builder.template(self.title_template, + title=self.title, item=self, **self.title_args) + c.add_widget(instance) + + +class Accordion(Widget): + '''Accordion class, see module documentation for more information. + ''' + + orientation = OptionProperty('horizontal', options=( + 'horizontal', 'vertical')) + '''Orientation of the layout. + + :data:`orientation` is an :class:`~kivy.properties.OptionProperty`, default + to 'horizontal'. Can take a value of 'vertical' or 'horizontal'. + ''' + + anim_duration = NumericProperty(.25) + '''Duration of the animation is second, when a new accordion item is + selected. + + :data:`anim_duration` is a :class:`~kivy.properties.NumericProperty`, + default to .25 (250ms) + ''' + + anim_func = ObjectProperty('out_expo') + '''Easing function to use for the animation. Check + :class:`kivy.animation.AnimationTransition` for more information about + available animation functions. + + :data:`anim_func` is a :class:`~kivy.properties.ObjectProperty`, + default to 'out_expo'. You can set a string or a function to use as an + easing function. + ''' + + min_space = NumericProperty(44) + '''Minimum space to use for title of each item. This value is automatically + set on each children, each time the layout happen. + + :data:`min_space` is a :class:`~kivy.properties.NumericProperty`, default to + 44 (px). + ''' + + def __init__(self, **kwargs): + super(Accordion, self).__init__(**kwargs) + self._trigger_layout = Clock.create_trigger(self._do_layout, -1) + self.bind( + orientation = self._trigger_layout, + children = self._trigger_layout, + size = self._trigger_layout, + pos = self._trigger_layout, + min_space = self._trigger_layout) + + def add_widget(self, widget, *largs): + if not isinstance(widget, AccordionItem): + raise AccordionException('Accordion accept only AccordionItem') + widget.accordion = self + ret = super(Accordion, self).add_widget(widget, *largs) + all_collapsed = \ + list(set(([x.collapse for x in self.children]))) == [True] + if all_collapsed: + widget.collapse = False + return ret + + def select(self, instance): + if instance not in self.children: + raise AccordionException( + 'Accordion: instance not found in children') + for widget in self.children: + if widget == instance: + continue + widget.collapse = True + self._trigger_layout() + + def _do_layout(self, dt): + children = self.children + orientation = self.orientation + min_space = self.min_space + min_space_total = len(children) * self.min_space + w, h = self.size + x, y = self.pos + if orientation == 'horizontal': + display_space = self.width - min_space_total + else: + display_space = self.height - min_space_total + + if display_space <= 0: + Logger.warning('Accordion: not have enough space ' + 'for displaying all childrens') + Logger.warning('Accordion: need %dpx, got %dpx' % ( + min_space_total, min_space_total + display_space)) + Logger.warning('Accordion: layout aborted.') + return + + if orientation == 'horizontal': + children = reversed(children) + for child in children: + child_space = min_space + child_space += display_space * (1 - child.collapse_alpha) + child._min_space = min_space + child.x = x + child.y = y + child.orientation = self.orientation + if orientation == 'horizontal': + child.content_size = display_space, h + child.width = child_space + child.height = h + x += child_space + else: + child.content_size = w, display_space + child.width = w + child.height = child_space + y += child_space + +if __name__ == '__main__': + from kivy.base import runTouchApp + from kivy.uix.button import Button + from kivy.uix.boxlayout import BoxLayout + from kivy.uix.label import Label + + acc = Accordion() + for x in xrange(10): + item = AccordionItem(title='Title %d' % x) + if x == 0: + item.add_widget(Button(text='Content %d' % x)) + elif x == 1: + l = BoxLayout(orientation='vertical') + l.add_widget(Button(text=str(x), size_hint_y=None, height=35)) + l.add_widget(Label(text='Content %d' % x)) + item.add_widget(l) + else: + item.add_widget(Label(text='This is a big content\n' * 20)) + acc.add_widget(item) + + def toggle_layout(*l): + o = acc.orientation + acc.orientation = 'vertical' if o == 'horizontal' else 'horizontal' + btn = Button(text='Toggle layout') + btn.bind(on_release=toggle_layout) + + from kivy.uix.slider import Slider + slider = Slider() + + def update_min_space(instance, value): + acc.min_space = value + + slider.bind(value=update_min_space) + + root = BoxLayout(spacing=20, padding=20) + controls = BoxLayout(orientation='vertical', size_hint_x=.3) + controls.add_widget(btn) + controls.add_widget(slider) + root.add_widget(controls) + root.add_widget(acc) + runTouchApp(root)