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()