Mercurial > libervia-desktop-kivy
changeset 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 | 434f770fe55b |
children | aa860c10acfc |
files | cagou/core/cagou_main.py cagou/core/cagou_widget.py cagou/core/widgets_handler.py cagou/kv/widgets_handler.kv cagou/plugins/plugin_wid_chat.py cagou/plugins/plugin_wid_widget_selector.kv cagou/plugins/plugin_wid_widget_selector.py |
diffstat | 7 files changed, 289 insertions(+), 89 deletions(-) [+] |
line wrap: on
line diff
--- a/cagou/core/cagou_main.py Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/core/cagou_main.py Fri Jan 17 18:44:35 2020 +0100 @@ -753,14 +753,7 @@ raise exceptions.InternalError("no CagouWidget found when " "trying to switch widget") - wrapper = to_change.parent - while wrapper is not None and not(isinstance(wrapper, widgets_handler.WHWrapper)): - wrapper = wrapper.parent - - if wrapper is None: - raise exceptions.InternalError("no wrapper found") - - wrapper.changeWidget(new) + to_change.whwrapper.changeWidget(new) self.selected_widget = new def _addVisibleWidget(self, widget): @@ -811,31 +804,35 @@ return set() def deleteUnusedWidgetInstances(self, widget): - """Delete instance of this widget without parent + """Delete instance of this widget which are not attached to a WHWrapper @param widget(quick_widgets.QuickWidget): reference widget other instance of this widget will be deleted if they have no parent """ - # FIXME: unused for now to_delete = [] if isinstance(widget, quick_widgets.QuickWidget): for w in self.widgets.getWidgetInstances(widget): - if w.parent is None and w != widget: + if w.whwrapper is None and w != widget: to_delete.append(w) for w in to_delete: log.debug("cleaning widget: {wid}".format(wid=w)) self.widgets.deleteWidget(w) - def getOrClone(self, widget): - """Get a QuickWidget if it has not parent set else clone it + def getOrClone(self, widget, **kwargs): + """Get a QuickWidget if it is not in a WHWrapper, else clone it - if an other instance of this widget exist without parent, it will be used. + if an other instance of this widget exist without being in a WHWrapper + (i.e. if it is not already in use) it will be used. """ - if widget.parent is None: + if widget.whwrapper is None: + if widget.parent is not None: + widget.parent.remove_widget(widget) self.deleteUnusedWidgetInstances(widget) return widget for w in self.widgets.getWidgetInstances(widget): - if w.parent is None: + if w.whwrapper is None: + if w.parent is not None: + w.parent.remove_widget(w) self.deleteUnusedWidgetInstances(w) return w targets = list(widget.targets) @@ -843,7 +840,8 @@ targets[0], on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, - profiles=widget.profiles) + profiles=widget.profiles, + **kwargs) for t in targets[1:]: w.addTarget(t) return w @@ -927,7 +925,8 @@ ## misc ## def plugging_profiles(self): - self.app.root.changeWidget(widgets_handler.WidgetsHandler()) + self._widgets_handler = widgets_handler.WidgetsHandler() + self.app.root.changeWidget(self._widgets_handler) self.bridge.menusGet("", C.NO_SECURITY_LIMIT, callback=self._menusGetCb) def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE): @@ -1023,6 +1022,23 @@ msg = e)) notification = None + def getParentWHWrapper(self, wid): + """Retrieve parent WHWrapper instance managing a widget + + @param wid(Widget): widget to check + @return (WHWrapper, None): found instance if any, else None + """ + wh = self.getAncestorWidget(wid, widgets_handler.WHWrapper) + if wh is None: + # we may have a screen + try: + sm = wid.screen_manager + except (exceptions.InternalError, exceptions.NotFound): + return None + else: + wh = self.getAncestorWidget(sm, widgets_handler.WHWrapper) + return wh + def getAncestorWidget(self, wid, cls): """Retrieve an ancestor of given class
--- a/cagou/core/cagou_widget.py Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/core/cagou_widget.py Fri Jan 17 18:44:35 2020 +0100 @@ -19,13 +19,17 @@ from sat.core import log as logging -log = logging.getLogger(__name__) +from sat.core import exceptions from kivy.uix.behaviors import ButtonBehavior from kivy.uix.boxlayout import BoxLayout from kivy.uix.dropdown import DropDown +from kivy.uix.screenmanager import Screen from kivy import properties from cagou import G -from cagou.core.common import ActionIcon +from .common import ActionIcon + + +log = logging.getLogger(__name__) class HeaderWidgetChoice(ButtonBehavior, BoxLayout): @@ -44,7 +48,8 @@ def __init__(self, cagou_widget): super(HeaderWidgetSelector, self).__init__() - for plugin_info in G.host.getPluggedWidgets(except_cls=cagou_widget.__class__): + plg_info_cls = cagou_widget.plugin_info_class or cagou_widget.__class__ + for plugin_info in G.host.getPluggedWidgets(except_cls=plg_info_cls): choice = HeaderWidgetChoice(cagou_widget, plugin_info) self.add_widget(choice) @@ -58,17 +63,60 @@ class CagouWidget(BoxLayout): + main_container = properties.ObjectProperty(None) header_input = properties.ObjectProperty(None) header_box = properties.ObjectProperty(None) + # set to True if you want to be able to switch between visible widgets of this + # class using a carousel + collection_carousel = False + # set to True if you a global ScreenManager global to all widgets of this class. + # The screen manager is created in WHWrapper + global_screen_manager = False + # override this if a specific class (i.e. not self.__class__) must be used for + # plugin info. Useful when a CagouWidget is used with global_screen_manager. + plugin_info_class = None - def __init__(self): + def __init__(self, **kwargs): + plg_info_cls = self.plugin_info_class or self.__class__ for p in G.host.getPluggedWidgets(): - if p['main'] == self.__class__: + if p['main'] == plg_info_cls: self.plugin_info = p break - BoxLayout.__init__(self) + super().__init__(**kwargs) self.selector = HeaderWidgetSelector(self) + @property + def screen_manager(self): + if ((not self.global_screen_manager + and not (self.plugin_info_class is not None + and self.plugin_info_class.global_screen_manager))): + raise exceptions.InternalError( + "screen_manager property can't be used if global_screen_manager is not " + "set") + screen = self.getAncestor(Screen) + if screen is None: + raise exceptions.NotFound("Can't find parent Screen") + if screen.manager is None: + raise exceptions.NotFound("Can't find parent ScreenManager") + return screen.manager + + @property + def whwrapper(self): + """Retrieve parent widget handler""" + return G.host.getParentWHWrapper(self) + + def screenManagerInit(self, screen_manager): + """Override this method to do init when ScreenManager is instantiated + + This is only called once even if collection_carousel is used. + """ + if not self.global_screen_manager: + raise exceptions.InternalError("screenManagerInit should not be called") + + def getAncestor(self, cls): + """Helper method to use host.getAncestorWidget with self""" + return G.host.getAncestorWidget(self, cls) + def switchWidget(self, plugin_info): self.selector.dismiss() factory = plugin_info["factory"]
--- 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()
--- a/cagou/kv/widgets_handler.kv Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/kv/widgets_handler.kv Fri Jan 17 18:44:35 2020 +0100 @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. <WHWrapper>: - carousel: carousel _sp_top_y: self.y + self.height - self.sp_size padding: self.split_size + self.split_margin, self.split_size + self.split_margin, 0, 0 @@ -42,9 +41,3 @@ # top points: self.x + self.width / 2 - self.sp_size - self.sp_space, root._sp_top_y, self.x + self.width / 2, root._sp_top_y, self.x + self.width / 2 + self.sp_size + self.sp_space, root._sp_top_y pointsize: self.sp_size - - Carousel: - id: carousel - direction: 'right' - ignore_perpendicular_swipes: True - loop: True
--- a/cagou/plugins/plugin_wid_chat.py Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/plugins/plugin_wid_chat.py Fri Jan 17 18:44:35 2020 +0100 @@ -380,9 +380,12 @@ message_input = properties.ObjectProperty() messages_widget = properties.ObjectProperty() history_scroll = properties.ObjectProperty() + collection_carousel = True def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, statuses=None, profiles=None): + if statuses is None: + statuses = {} quick_chat.QuickChat.__init__( self, host, target, type_, nick, occupants, subject, statuses, profiles=profiles) @@ -404,9 +407,11 @@ self.headerInputAddExtra(extra_btn) self.header_input.hint_text = target self._history_prepend_lock = False - Clock.schedule_once(lambda dt: self.postInit(), 0) self.history_count = 0 + def on_kv_post(self, __): + self.postInit() + def __str__(self): return "Chat({})".format(self.target)
--- a/cagou/plugins/plugin_wid_widget_selector.kv Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/plugins/plugin_wid_widget_selector.kv Fri Jan 17 18:44:35 2020 +0100 @@ -39,3 +39,5 @@ <WidgetSelector>: spacing: dp(10) + size_hint: 1, None + height: self.minimum_height
--- a/cagou/plugins/plugin_wid_widget_selector.py Fri Jan 17 18:44:35 2020 +0100 +++ b/cagou/plugins/plugin_wid_widget_selector.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)