Mercurial > libervia-desktop-kivy
changeset 323:5bd583d00594
backport: added a new "backport" module for using unreleased code from Kivy:
Carousel has been backported from Kivy 2.0, because a couple of bugs hitting Cagou are fixed
there (notably https://github.com/kivy/kivy/issues/6370). The issue was specially visible
when sliding chat widgets.
If a version >= 2.0 of kivy is used, a warning will be displayed to indicated that the
backport can be removed.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 06 Dec 2019 13:23:03 +0100 |
parents | e2b51663d8b8 |
children | 4374cb741eb5 |
files | cagou/backport/LICENSE cagou/backport/README cagou/backport/__init__.py cagou/backport/carousel.py cagou/core/cagou_main.py |
diffstat | 5 files changed, 755 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/backport/LICENSE Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,19 @@ +Copyright (c) 2010-2019 Kivy Team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/backport/README Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,9 @@ +This directory include files backported from unreleased versions of Kivy until +they are actually released. Note that the LICENSE in this directory apply only +to this directory, other files follow general Cagou license specified in root +directory's COPYING (AGPL v3+). + +The files here (except __init__.py) comes from Kivy repository (https://github.com/kivy/kivy) and are +copyrighted to their respective authors + +"__init__.py" is part of Cagou and licensed under the same license (see root COPYING).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/backport/__init__.py Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016-2019 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import kivy +from sat.core import log as logging + + +log = logging.getLogger(__name__) + + +def do_backport(): + version = tuple(kivy.parse_kivy_version(kivy.__version__)[0]) + if version < (2, 0, 0): + log.info("installing backport for Carousel") + import sys + from kivy.uix import carousel # NOQA + from . import carousel as carousel_backport + sys.modules['kivy.uix.carousel'] = carousel_backport + else: + log.info("Kivy >= 2.0.0 is used, Carousel backport is not needed anymore")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/backport/carousel.py Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,690 @@ +''' +Carousel +======== + +.. image:: images/carousel.gif + :align: right + +.. versionadded:: 1.4.0 + +The :class:`Carousel` widget provides the classic mobile-friendly carousel view +where you can swipe between slides. +You can add any content to the carousel and have it move horizontally or +vertically. The carousel can display pages in a sequence or a loop. + +Example:: + + from kivy.app import App + from kivy.uix.carousel import Carousel + from kivy.uix.image import AsyncImage + + + class CarouselApp(App): + def build(self): + carousel = Carousel(direction='right') + for i in range(10): + src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i + image = AsyncImage(source=src, allow_stretch=True) + carousel.add_widget(image) + return carousel + + + CarouselApp().run() + + +Kv Example:: + + Carousel: + direction: 'right' + AsyncImage: + source: 'http://placehold.it/480x270.png&text=slide-1.png' + AsyncImage: + source: 'http://placehold.it/480x270.png&text=slide-2.png' + AsyncImage: + source: 'http://placehold.it/480x270.png&text=slide-3.png' + AsyncImage: + source: 'http://placehold.it/480x270.png&text=slide-4.png' + + +.. versionchanged:: 1.5.0 + The carousel now supports active children, like the + :class:`~kivy.uix.scrollview.ScrollView`. It will detect a swipe gesture + according to the :attr:`Carousel.scroll_timeout` and + :attr:`Carousel.scroll_distance` properties. + + In addition, the slide container is no longer exposed by the API. + The impacted properties are + :attr:`Carousel.slides`, :attr:`Carousel.current_slide`, + :attr:`Carousel.previous_slide` and :attr:`Carousel.next_slide`. + +''' + +__all__ = ('Carousel', ) + +from functools import partial +from kivy.clock import Clock +from kivy.factory import Factory +from kivy.animation import Animation +from kivy.uix.stencilview import StencilView +from kivy.uix.relativelayout import RelativeLayout +from kivy.properties import BooleanProperty, OptionProperty, AliasProperty, \ + NumericProperty, ListProperty, ObjectProperty, StringProperty + + +class Carousel(StencilView): + '''Carousel class. See module documentation for more information. + ''' + + slides = ListProperty([]) + '''List of slides inside the Carousel. The slides are the + widgets added to the Carousel using the :attr:`add_widget` method. + + :attr:`slides` is a :class:`~kivy.properties.ListProperty` and is + read-only. + ''' + + def _get_slides_container(self): + return [x.parent for x in self.slides] + + slides_container = AliasProperty(_get_slides_container, bind=('slides',)) + + direction = OptionProperty('right', + options=('right', 'left', 'top', 'bottom')) + '''Specifies the direction in which the slides are ordered. This + corresponds to the direction from which the user swipes to go from one + slide to the next. It + can be `right`, `left`, `top`, or `bottom`. For example, with + the default value of `right`, the second slide is to the right + of the first and the user would swipe from the right towards the + left to get to the second slide. + + :attr:`direction` is an :class:`~kivy.properties.OptionProperty` and + defaults to 'right'. + ''' + + min_move = NumericProperty(0.2) + '''Defines the minimum distance to be covered before the touch is + considered a swipe gesture and the Carousel content changed. + This is a expressed as a fraction of the Carousel's width. + If the movement doesn't reach this minimum value, the movement is + cancelled and the content is restored to its original position. + + :attr:`min_move` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0.2. + ''' + + anim_move_duration = NumericProperty(0.5) + '''Defines the duration of the Carousel animation between pages. + + :attr:`anim_move_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to 0.5. + ''' + + anim_cancel_duration = NumericProperty(0.3) + '''Defines the duration of the animation when a swipe movement is not + accepted. This is generally when the user does not make a large enough + swipe. See :attr:`min_move`. + + :attr:`anim_cancel_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to 0.3. + ''' + + loop = BooleanProperty(False) + '''Allow the Carousel to loop infinitely. If True, when the user tries to + swipe beyond last page, it will return to the first. If False, it will + remain on the last page. + + :attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False. + ''' + + def _get_index(self): + if self.slides: + return self._index % len(self.slides) + return None + + def _set_index(self, value): + if self.slides: + self._index = value % len(self.slides) + else: + self._index = None + + index = AliasProperty(_get_index, _set_index, + bind=('_index', 'slides'), + cache=True) + '''Get/Set the current slide based on the index. + + :attr:`index` is an :class:`~kivy.properties.AliasProperty` and defaults + to 0 (the first item). + ''' + + def _prev_slide(self): + slides = self.slides + len_slides = len(slides) + index = self.index + if len_slides < 2: # None, or 1 slide + return None + if self.loop and index == 0: + return slides[-1] + if index > 0: + return slides[index - 1] + + previous_slide = AliasProperty(_prev_slide, + bind=('slides', 'index', 'loop'), + cache=True) + '''The previous slide in the Carousel. It is None if the current slide is + the first slide in the Carousel. This ordering reflects the order in which + the slides are added: their presentation varies according to the + :attr:`direction` property. + + :attr:`previous_slide` is an :class:`~kivy.properties.AliasProperty`. + + .. versionchanged:: 1.5.0 + This property no longer exposes the slides container. It returns + the widget you have added. + ''' + + def _curr_slide(self): + if len(self.slides): + return self.slides[self.index or 0] + + current_slide = AliasProperty(_curr_slide, + bind=('slides', 'index'), + cache=True) + '''The currently shown slide. + + :attr:`current_slide` is an :class:`~kivy.properties.AliasProperty`. + + .. versionchanged:: 1.5.0 + The property no longer exposes the slides container. It returns + the widget you have added. + ''' + + def _next_slide(self): + if len(self.slides) < 2: # None, or 1 slide + return None + if self.loop and self.index == len(self.slides) - 1: + return self.slides[0] + if self.index < len(self.slides) - 1: + return self.slides[self.index + 1] + + next_slide = AliasProperty(_next_slide, + bind=('slides', 'index', 'loop'), + cache=True) + '''The next slide in the Carousel. It is None if the current slide is + the last slide in the Carousel. This ordering reflects the order in which + the slides are added: their presentation varies according to the + :attr:`direction` property. + + :attr:`next_slide` is an :class:`~kivy.properties.AliasProperty`. + + .. versionchanged:: 1.5.0 + The property no longer exposes the slides container. + It returns the widget you have added. + ''' + + scroll_timeout = NumericProperty(200) + '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. + If the user has not moved :attr:`scroll_distance` within the timeout, + no scrolling will occur and the touch event will go to the children. + + :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and + defaults to 200 (milliseconds) + + .. versionadded:: 1.5.0 + ''' + + scroll_distance = NumericProperty('20dp') + '''Distance to move before scrolling the :class:`Carousel` in pixels. As + soon as the distance has been traveled, the :class:`Carousel` will start + to scroll, and no touch event will go to children. + It is advisable that you base this value on the dpi of your target device's + screen. + + :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and + defaults to 20dp. + + .. versionadded:: 1.5.0 + ''' + + anim_type = StringProperty('out_quad') + '''Type of animation to use while animating to the next/previous slide. + This should be the name of an + :class:`~kivy.animation.AnimationTransition` function. + + :attr:`anim_type` is a :class:`~kivy.properties.StringProperty` and + defaults to 'out_quad'. + + .. versionadded:: 1.8.0 + ''' + + ignore_perpendicular_swipes = BooleanProperty(False) + '''Ignore swipes on axis perpendicular to direction. + + :attr:`ignore_perpendicular_swipes` is a + :class:`~kivy.properties.BooleanProperty` and defaults to False. + + .. versionadded:: 1.10.0 + ''' + + # private properties, for internal use only ### + _index = NumericProperty(0, allownone=True) + _prev = ObjectProperty(None, allownone=True) + _current = ObjectProperty(None, allownone=True) + _next = ObjectProperty(None, allownone=True) + _offset = NumericProperty(0) + _touch = ObjectProperty(None, allownone=True) + + _change_touch_mode_ev = None + + def __init__(self, **kwargs): + self._trigger_position_visible_slides = Clock.create_trigger( + self._position_visible_slides, -1) + super(Carousel, self).__init__(**kwargs) + self._skip_slide = None + self.touch_mode_change = False + self._prioritize_next = False + self.fbind('loop', lambda *args: self._insert_visible_slides()) + + def load_slide(self, slide): + '''Animate to the slide that is passed as the argument. + + .. versionchanged:: 1.8.0 + ''' + slides = self.slides + start, stop = slides.index(self.current_slide), slides.index(slide) + if start == stop: + return + + self._skip_slide = stop + if stop > start: + self._prioritize_next = True + self._insert_visible_slides(_next_slide=slide) + self.load_next() + else: + self._prioritize_next = False + self._insert_visible_slides(_prev_slide=slide) + self.load_previous() + + def load_previous(self): + '''Animate to the previous slide. + + .. versionadded:: 1.7.0 + ''' + self.load_next(mode='prev') + + def load_next(self, mode='next'): + '''Animate to the next slide. + + .. versionadded:: 1.7.0 + ''' + if self.index is not None: + w, h = self.size + _direction = { + 'top': -h / 2, + 'bottom': h / 2, + 'left': w / 2, + 'right': -w / 2} + _offset = _direction[self.direction] + if mode == 'prev': + _offset = -_offset + + self._start_animation(min_move=0, offset=_offset) + + def get_slide_container(self, slide): + return slide.parent + + @property + def _prev_equals_next(self): + return self.loop and len(self.slides) == 2 + + def _insert_visible_slides(self, _next_slide=None, _prev_slide=None): + get_slide_container = self.get_slide_container + + previous_slide = _prev_slide if _prev_slide else self.previous_slide + if previous_slide: + self._prev = get_slide_container(previous_slide) + else: + self._prev = None + + current_slide = self.current_slide + if current_slide: + self._current = get_slide_container(current_slide) + else: + self._current = None + + next_slide = _next_slide if _next_slide else self.next_slide + if next_slide: + self._next = get_slide_container(next_slide) + else: + self._next = None + + if self._prev_equals_next: + setattr(self, '_prev' if self._prioritize_next else '_next', None) + + super_remove = super(Carousel, self).remove_widget + for container in self.slides_container: + super_remove(container) + + if self._prev and self._prev.parent is not self: + super(Carousel, self).add_widget(self._prev) + if self._next and self._next.parent is not self: + super(Carousel, self).add_widget(self._next) + if self._current: + super(Carousel, self).add_widget(self._current) + + def _position_visible_slides(self, *args): + slides, index = self.slides, self.index + no_of_slides = len(slides) - 1 + if not slides: + return + x, y, width, height = self.x, self.y, self.width, self.height + _offset, direction = self._offset, self.direction[0] + _prev, _next, _current = self._prev, self._next, self._current + get_slide_container = self.get_slide_container + last_slide = get_slide_container(slides[-1]) + first_slide = get_slide_container(slides[0]) + skip_next = False + _loop = self.loop + + if direction in 'rl': + xoff = x + _offset + x_prev = {'l': xoff + width, 'r': xoff - width} + x_next = {'l': xoff - width, 'r': xoff + width} + if _prev: + _prev.pos = (x_prev[direction], y) + elif _loop and _next and index == 0: + # if first slide is moving to right with direction set to right + # or toward left with direction set to left + if ((_offset > 0 and direction == 'r') or + (_offset < 0 and direction == 'l')): + # put last_slide before first slide + last_slide.pos = (x_prev[direction], y) + skip_next = True + if _current: + _current.pos = (xoff, y) + if skip_next: + return + if _next: + _next.pos = (x_next[direction], y) + elif _loop and _prev and index == no_of_slides: + if ((_offset < 0 and direction == 'r') or + (_offset > 0 and direction == 'l')): + first_slide.pos = (x_next[direction], y) + if direction in 'tb': + yoff = y + _offset + y_prev = {'t': yoff - height, 'b': yoff + height} + y_next = {'t': yoff + height, 'b': yoff - height} + if _prev: + _prev.pos = (x, y_prev[direction]) + elif _loop and _next and index == 0: + if ((_offset > 0 and direction == 't') or + (_offset < 0 and direction == 'b')): + last_slide.pos = (x, y_prev[direction]) + skip_next = True + if _current: + _current.pos = (x, yoff) + if skip_next: + return + if _next: + _next.pos = (x, y_next[direction]) + elif _loop and _prev and index == no_of_slides: + if ((_offset < 0 and direction == 't') or + (_offset > 0 and direction == 'b')): + first_slide.pos = (x, y_next[direction]) + + def on_size(self, *args): + size = self.size + for slide in self.slides_container: + slide.size = size + self._trigger_position_visible_slides() + + def on_pos(self, *args): + self._trigger_position_visible_slides() + + def on_index(self, *args): + self._insert_visible_slides() + self._trigger_position_visible_slides() + self._offset = 0 + + def on_slides(self, *args): + if self.slides: + self.index = self.index % len(self.slides) + self._insert_visible_slides() + self._trigger_position_visible_slides() + + def on__offset(self, *args): + self._trigger_position_visible_slides() + # if reached full offset, switch index to next or prev + direction = self.direction[0] + _offset = self._offset + width = self.width + height = self.height + index = self.index + if self._skip_slide is not None or index is None: + return + + # Move to next slide? + if (direction == 'r' and _offset <= -width) or \ + (direction == 'l' and _offset >= width) or \ + (direction == 't' and _offset <= - height) or \ + (direction == 'b' and _offset >= height): + if self.next_slide: + self.index += 1 + + # Move to previous slide? + elif (direction == 'r' and _offset >= width) or \ + (direction == 'l' and _offset <= -width) or \ + (direction == 't' and _offset >= height) or \ + (direction == 'b' and _offset <= -height): + if self.previous_slide: + self.index -= 1 + + elif self._prev_equals_next: + new_value = (_offset < 0) is (direction in 'rt') + if self._prioritize_next is not new_value: + self._prioritize_next = new_value + if new_value is (self._next is None): + self._prev, self._next = self._next, self._prev + + def _start_animation(self, *args, **kwargs): + # compute target offset for ease back, next or prev + new_offset = 0 + direction = kwargs.get('direction', self.direction)[0] + is_horizontal = direction in 'rl' + extent = self.width if is_horizontal else self.height + min_move = kwargs.get('min_move', self.min_move) + _offset = kwargs.get('offset', self._offset) + + if _offset < min_move * -extent: + new_offset = -extent + elif _offset > min_move * extent: + new_offset = extent + + # if new_offset is 0, it wasnt enough to go next/prev + dur = self.anim_move_duration + if new_offset == 0: + dur = self.anim_cancel_duration + + # detect edge cases if not looping + len_slides = len(self.slides) + index = self.index + if not self.loop or len_slides == 1: + is_first = (index == 0) + is_last = (index == len_slides - 1) + if direction in 'rt': + towards_prev = (new_offset > 0) + towards_next = (new_offset < 0) + else: + towards_prev = (new_offset < 0) + towards_next = (new_offset > 0) + if (is_first and towards_prev) or (is_last and towards_next): + new_offset = 0 + + anim = Animation(_offset=new_offset, d=dur, t=self.anim_type) + anim.cancel_all(self) + + def _cmp(*l): + if self._skip_slide is not None: + self.index = self._skip_slide + self._skip_slide = None + + anim.bind(on_complete=_cmp) + anim.start(self) + + def _get_uid(self, prefix='sv'): + return '{0}.{1}'.format(prefix, self.uid) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + touch.ud[self._get_uid('cavoid')] = True + return + if self.disabled: + return True + if self._touch: + return super(Carousel, self).on_touch_down(touch) + Animation.cancel_all(self) + self._touch = touch + uid = self._get_uid() + touch.grab(self) + touch.ud[uid] = { + 'mode': 'unknown', + 'time': touch.time_start} + self._change_touch_mode_ev = Clock.schedule_once( + self._change_touch_mode, self.scroll_timeout / 1000.) + self.touch_mode_change = False + return True + + def on_touch_move(self, touch): + if not self.touch_mode_change: + if self.ignore_perpendicular_swipes and \ + self.direction in ('top', 'bottom'): + if abs(touch.oy - touch.y) < self.scroll_distance: + if abs(touch.ox - touch.x) > self.scroll_distance: + self._change_touch_mode() + self.touch_mode_change = True + elif self.ignore_perpendicular_swipes and \ + self.direction in ('right', 'left'): + if abs(touch.ox - touch.x) < self.scroll_distance: + if abs(touch.oy - touch.y) > self.scroll_distance: + self._change_touch_mode() + self.touch_mode_change = True + + if self._get_uid('cavoid') in touch.ud: + return + if self._touch is not touch: + super(Carousel, self).on_touch_move(touch) + return self._get_uid() in touch.ud + if touch.grab_current is not self: + return True + ud = touch.ud[self._get_uid()] + direction = self.direction[0] + if ud['mode'] == 'unknown': + if direction in 'rl': + distance = abs(touch.ox - touch.x) + else: + distance = abs(touch.oy - touch.y) + if distance > self.scroll_distance: + ev = self._change_touch_mode_ev + if ev is not None: + ev.cancel() + ud['mode'] = 'scroll' + else: + if direction in 'rl': + self._offset += touch.dx + if direction in 'tb': + self._offset += touch.dy + return True + + def on_touch_up(self, touch): + if self._get_uid('cavoid') in touch.ud: + return + if self in [x() for x in touch.grab_list]: + touch.ungrab(self) + self._touch = None + ud = touch.ud[self._get_uid()] + if ud['mode'] == 'unknown': + ev = self._change_touch_mode_ev + if ev is not None: + ev.cancel() + super(Carousel, self).on_touch_down(touch) + Clock.schedule_once(partial(self._do_touch_up, touch), .1) + else: + self._start_animation() + + else: + if self._touch is not touch and self.uid not in touch.ud: + super(Carousel, self).on_touch_up(touch) + return self._get_uid() in touch.ud + + def _do_touch_up(self, touch, *largs): + super(Carousel, self).on_touch_up(touch) + # don't forget about grab event! + for x in touch.grab_list[:]: + touch.grab_list.remove(x) + x = x() + if not x: + continue + touch.grab_current = x + super(Carousel, self).on_touch_up(touch) + touch.grab_current = None + + def _change_touch_mode(self, *largs): + if not self._touch: + return + self._start_animation() + uid = self._get_uid() + touch = self._touch + ud = touch.ud[uid] + if ud['mode'] == 'unknown': + touch.ungrab(self) + self._touch = None + super(Carousel, self).on_touch_down(touch) + return + + def add_widget(self, widget, index=0, canvas=None): + container = RelativeLayout( + size=self.size, x=self.x - self.width, y=self.y) + container.add_widget(widget) + super(Carousel, self).add_widget(container, index, canvas) + if index != 0: + self.slides.insert(index - len(self.slides), widget) + else: + self.slides.append(widget) + + def remove_widget(self, widget, *args, **kwargs): + # XXX be careful, the widget.parent refer to the RelativeLayout + # added in add_widget(). But it will break if RelativeLayout + # implementation change. + # if we passed the real widget + slides = self.slides + if widget in slides: + if self.index >= slides.index(widget): + self.index = max(0, self.index - 1) + container = widget.parent + slides.remove(widget) + super(Carousel, self).remove_widget(container) + return container.remove_widget(widget, *args, **kwargs) + return super(Carousel, self).remove_widget(widget, *args, **kwargs) + + def clear_widgets(self): + for slide in self.slides[:]: + self.remove_widget(slide) + super(Carousel, self).clear_widgets() + + +if __name__ == '__main__': + from kivy.app import App + + class Example1(App): + + def build(self): + carousel = Carousel(direction='left', + loop=True) + for i in range(4): + src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i + image = Factory.AsyncImage(source=src, allow_stretch=True) + carousel.add_widget(image) + return carousel + + Example1().run()
--- a/cagou/core/cagou_main.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/core/cagou_main.py Fri Dec 06 13:23:03 2019 +0100 @@ -23,6 +23,8 @@ from sat.core.i18n import _ from . import kivy_hack kivy_hack.do_hack() +from cagou.backport import do_backport +do_backport() from .constants import Const as C from sat.core import log as logging from sat.core import exceptions