Mercurial > libervia-desktop-kivy
diff cagou/core/widgets_handler.py @ 353:19422bbd9c8e
core (widgets handler): refactoring:
- CagouWidget now has class properties (to be overridden when needed) which indicate how
if the widget handle must add a wrapping ScreenManager (global_screen_manager) or show
all instances of the class in a Carousel (collection_carousel). If none of those
options is used, a ScrollView will be wrapping the widget, to be sure that the widget
will be resized correctly when necessary (without it, the widget could still be
drawn in the backround when the size is too small and overflow on the WidgetWrapper,
this would be the case with WidgetSelector)
- some helper methods/properties have been added to CagouWidget. Check docstrings for
details
- better handling of (in)visible widget in WidgetsHandler
- thanks to the new wrapping ScrollView, WidgetSelect will show scroll bars if the
available space is too small.
- bugs fixes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 17 Jan 2020 18:44:35 +0100 |
parents | 772c170b47a9 |
children | 8b6621cc142c |
line wrap: on
line diff
--- a/cagou/core/widgets_handler.py Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/core/widgets_handler.py Fri Jan 17 18:44:35 2020 +0100 @@ -1,5 +1,4 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client # Copyright (C) 2016-2019 Jérôme Poisson (goffi@goffi.org) @@ -19,17 +18,22 @@ from sat.core import log as logging -log = logging.getLogger(__name__) from sat.core import exceptions from sat_frontends.quick_frontend import quick_widgets from kivy.graphics import Color, Ellipse from kivy.uix.layout import Layout from kivy.uix.boxlayout import BoxLayout from kivy.uix.stencilview import StencilView +from kivy.uix.carousel import Carousel +from kivy.uix.screenmanager import ScreenManager, Screen +from kivy.uix.scrollview import ScrollView from kivy.metrics import dp from kivy import properties from cagou import G -from cagou.core.constants import Const as C +from .constants import Const as C +from . import cagou_widget + +log = logging.getLogger(__name__) REMOVE_WID_LIMIT = dp(50) @@ -37,7 +41,9 @@ class WHWrapper(BoxLayout): - carousel = properties.ObjectProperty(None) + main_container = properties.ObjectProperty(None) + screen_manager = properties.ObjectProperty(None, allownone=True) + carousel = properties.ObjectProperty(None, allownone=True) split_size = properties.NumericProperty(dp(1)) split_margin = properties.NumericProperty(dp(2)) split_color = properties.ListProperty([0.8, 0.8, 0.8, 1]) @@ -54,13 +60,14 @@ idx = kwargs.pop('_wid_idx') self._wid_idx = idx super(WHWrapper, self).__init__(**kwargs) - self._former_slide = None - self.carousel.bind(current_slide=self.onSlideChange) - self._slides_update_lock = False self._left_wids = set() self._top_wids = set() self._right_wids = set() self._bottom_wids = set() + self._clear_attributes() + + def _clear_attributes(self): + self._former_slide = None def __repr__(self): return "WHWrapper_{idx}".format(idx=self._wid_idx) @@ -98,7 +105,46 @@ @property def current_slide(self): - return self.carousel.current_slide + if (self.carousel is not None + and (self.screen_manager is None or self.screen_manager.current == '')): + return self.carousel.current_slide + elif self.screen_manager is not None: + # we should have exactly one children in current_screen, else there is a bug + return self.screen_manager.current_screen.children[0] + else: + try: + return self.main_container.children[0] + except IndexError: + log.error("No child found, this should not happen") + return None + + @property + def carousel_active(self): + """Return True if Carousel is used and active""" + if self.carousel is None: + return False + if self.screen_manager is not None and self.screen_manager.current != '': + return False + return True + + @property + def former_screen_wid(self): + """Return widget currently active for former screen""" + if self.screen_manager is None: + raise exceptions.InternalError( + "former_screen_wid can only be used if ScreenManager is used") + if self._former_screen_name is None: + return None + return self.getScreenWidget(self._former_screen_name) + + def getScreenWidget(self, screen_name): + """Return screen main widget, handling carousel if necessary""" + if self.carousel is not None and screen_name == '': + return self.carousel.current_slide + try: + return self.screen_manager.get_screen(screen_name).children[0] + except IndexError: + return None def _draw_ellipse(self): """draw split ellipse""" @@ -158,8 +204,8 @@ touch.ud['ori_width'] = self.width self._draw_ellipse() else: - if len(self.carousel.slides) == 1: - # we don't want swipe if there is only one slide + if self.carousel_active and len(self.carousel.slides) <= 1: + # we don't want swipe of carousel if there is only one slide return StencilView.on_touch_down(self.carousel, touch) else: return super(WHWrapper, self).on_touch_down(touch) @@ -302,50 +348,143 @@ self.canvas.after.remove(self.ellipse) del self.ellipse + def clear_widgets(self): + current_slide = self.current_slide + if current_slide is not None: + G.host._removeVisibleWidget(current_slide) + + super().clear_widgets() + + self.screen_manager = None + self.carousel = None + self._clear_attributes() + def set_widget(self, wid, index=0): - self.carousel.add_widget(wid, index) + assert len(self.children) == 0 + + if wid.collection_carousel or wid.global_screen_manager: + self.main_container = self + else: + self.main_container = ScrollView() + self.add_widget(self.main_container) + + if self.carousel is not None: + return self.carousel.add_widget(wid, index) + + if wid.global_screen_manager: + if self.screen_manager is None: + self.screen_manager = ScreenManager() + self.main_container.add_widget(self.screen_manager) + parent = Screen() + self.screen_manager.add_widget(parent) + self._former_screen_name = '' + self.screen_manager.bind(current=self.onScreenChange) + wid.screenManagerInit(self.screen_manager) + else: + parent = self.main_container + + if wid.collection_carousel: + # a Carousel is requested, and this is the first widget that we add + # so we need to create the carousel + self.carousel = Carousel( + direction = "right", + ignore_perpendicular_swipes = True, + loop = True, + ) + self._slides_update_lock = 0 + self.carousel.bind(current_slide=self.onSlideChange) + parent.add_widget(self.carousel) + self.carousel.add_widget(wid, index) + else: + # no Carousel requested, we add the widget as a direct child + parent.add_widget(wid) + G.host._addVisibleWidget(wid) def changeWidget(self, new_widget): """Change currently displayed widget slides widgets will be updated """ - # slides update need to be blocked to avoid the update in onSlideChange - # which would mess the removal of current widgets - self._slides_update_lock = True - current = self.carousel.current_slide - for w in self.carousel.slides: - if w == current or w == new_widget: - continue - if isinstance(w, quick_widgets.QuickWidget): - G.host.widgets.deleteWidget(w) - self.carousel.clear_widgets() - self.carousel.add_widget(G.host.getOrClone(new_widget)) - self._slides_update_lock = False - self.updateHiddenSlides() + if (self.carousel is not None + and self.carousel.current_slide.__class__ == new_widget.__class__): + # we have the same class, we reuse carousel and screen manager setting + + if self.carousel.current_slide != new_widget: + # slides update need to be blocked to avoid the update in onSlideChange + # which would mess the removal of current widgets + self._slides_update_lock += 1 + new_wid = None + for w in self.carousel.slides[:]: + if w.widget_hash == new_widget.widget_hash: + new_wid = w + continue + self.carousel.remove_widget(w) + if isinstance(w, quick_widgets.QuickWidget): + G.host.widgets.deleteWidget(w) + if new_wid is None: + new_wid = G.host.getOrClone(new_widget) + self.carousel.add_widget(new_wid) + self._updateHiddenSlides() + self._slides_update_lock -= 1 + + if self.screen_manager is not None: + self.screen_manager.clear_widgets([ + s for s in self.screen_manager.screens if s.name != '']) + new_wid.screenManagerInit(self.screen_manager) + else: + # else, we restart fresh + self.clear_widgets() + self.set_widget(G.host.getOrClone(new_widget)) + + def onScreenChange(self, screen_manager, new_screen): + try: + new_screen_wid = self.current_slide + except IndexError: + new_screen_wid = None + log.warning("Switching to a screen without children") + if new_screen == '' and self.carousel is not None: + # carousel may have been changed in the background, so we update slides + self._updateHiddenSlides() + former_screen_wid = self.former_screen_wid + if isinstance(former_screen_wid, cagou_widget.CagouWidget): + G.host._removeVisibleWidget(former_screen_wid) + if isinstance(new_screen_wid, cagou_widget.CagouWidget): + G.host._addVisibleWidget(new_screen_wid) + self._former_screen_name = new_screen + G.host.selected_widget = new_screen_wid def onSlideChange(self, handler, new_slide): + if self._former_slide is new_slide: + # FIXME: workaround for Kivy a95d67f (and above?), Carousel.current_slide + # binding now calls onSlideChange twice with the same widget (here + # "new_slide"). To be checked with Kivy team. + return + log.debug(f"Slide change: new_slide = {new_slide}") if self._former_slide is not None: - if self._former_slide is new_slide: - # FIXME: workaround for Kivy a95d67f (and above?), Carousel.current_slide - # binding now calls onSlideChange twice with the same widget (here - # "new_slide"). To be checked with Kivy team. - return - G.host._removeVisibleWidget(self._former_slide) + G.host._removeVisibleWidget(self._former_slide, ignore_missing=True) self._former_slide = new_slide - if new_slide is not None: - G.host._addVisibleWidget(new_slide) - self.updateHiddenSlides() + if self.carousel_active: + G.host.selected_widget = new_slide + if new_slide is not None: + G.host._addVisibleWidget(new_slide) + self._updateHiddenSlides() - def hiddenList(self, visible_list): - """return widgets of same class as holded one which are hidden + def hiddenList(self, visible_list, ignore=None): + """return widgets of same class as carousel current one, if they are hidden @param visible_list(list[QuickWidget]): widgets visible + @param ignore(QuickWidget, None): do no return this widget @return (iter[QuickWidget]): widgets hidden """ - added = [(w.targets, w.profiles) for w in visible_list] # we want to avoid recreated widgets - for w in G.host.widgets.getWidgets(self.current_slide.__class__, profiles=self.current_slide.profiles): - if w in visible_list or (w.targets, w.profiles) in added: + # we want to avoid recreated widgets + added = [w.widget_hash for w in visible_list] + current_slide = self.carousel.current_slide + for w in G.host.widgets.getWidgets(current_slide.__class__, + profiles=current_slide.profiles): + wid_hash = w.widget_hash + if w in visible_list or wid_hash in added: + continue + if wid_hash == ignore.widget_hash: continue yield w @@ -361,24 +500,27 @@ except AttributeError: return str(list(widget.targets)[0]).lower() - def updateHiddenSlides(self): + def _updateHiddenSlides(self): """adjust carousel slides according to visible widgets""" - if self._slides_update_lock: + if self._slides_update_lock or not self.carousel_active: return - if not isinstance(self.carousel.current_slide, quick_widgets.QuickWidget): + current_slide = self.carousel.current_slide + if not isinstance(current_slide, quick_widgets.QuickWidget): return # lock must be used here to avoid recursions - self._slides_update_lock = True - visible_list = G.host.getVisibleList(self.current_slide.__class__) - hidden = list(self.hiddenList(visible_list)) - slides_sorted = sorted(hidden + [self.carousel.current_slide], key=self.widgets_sort) - to_remove = set(self.carousel.slides).difference({self.carousel.current_slide}) + self._slides_update_lock += 1 + visible_list = G.host.getVisibleList(current_slide.__class__) + # we ignore current_slide as it may not be visible yet (e.g. if an other + # screen is shown + hidden = list(self.hiddenList(visible_list, ignore=current_slide)) + slides_sorted = sorted(set(hidden + [current_slide]), key=self.widgets_sort) + to_remove = set(self.carousel.slides).difference({current_slide}) for w in to_remove: self.carousel.remove_widget(w) if hidden: # no need to add more than two widgets (next and previous), # as the list will be updated on each new visible widget - current_idx = slides_sorted.index(self.current_slide) + current_idx = slides_sorted.index(current_slide) try: next_slide = slides_sorted[current_idx+1] except IndexError: @@ -388,7 +530,7 @@ previous_slide = slides_sorted[current_idx-1] self.carousel.add_widget(G.host.getOrClone(previous_slide)) - self._slides_update_lock = False + self._slides_update_lock -= 1 class WidgetsHandlerLayout(Layout): @@ -476,9 +618,4 @@ def __init__(self, **kw): super(WidgetsHandler, self).__init__(**kw) - self.wrapper = self.add_widget() - - @property - def cagou_widget(self): - """get holded CagouWidget""" - return self.wrapper.current_slide + self.add_widget()