diff cagou/core/behaviors.py @ 404:f7476818f9fb

core (common): JidSelector + behaviors various improvments: - renamed *Behaviour => *Behavior to be consistent with Kivy + moved to new "core.behaviors" modules - use a dedicated property in ContactItem for notification counter (which is now named "badge") - in JidSelector, well-known strings now create use a dedicated layout, add separator (except if new `add_separators` property is set to False), and are added to attribute of the same name - a new `item_class` property is now used to indicate the class to instanciate for items (by default it's a ContactItem) - FilterBahavior.do_filter now expect the parent layout instead of directly the children, this is to allow a FilterBahavior to manage several children layout at once (used with JidSelector) - core.utils has been removed, as the behavior there has been moved to core.behaviors
author Goffi <goffi@goffi.org>
date Wed, 12 Feb 2020 20:02:58 +0100
parents
children 3c9ba4a694ef
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/behaviors.py	Wed Feb 12 20:02:58 2020 +0100
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2020 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/>.
+
+
+from kivy import properties
+from kivy.animation import Animation
+from kivy.clock import Clock
+from kivy_garden import modernmenu
+from functools import partial
+
+
+class TouchMenu(modernmenu.ModernMenu):
+    pass
+
+
+class TouchMenuItemBehavior:
+    """Class to use on every item where a menu may appear
+
+    main_wid attribute must be set to the class inheriting from TouchMenuBehavior
+    do_item_action is the method called on simple click
+    getMenuChoices must return a list of menus for long press
+        menus there are dict as expected by ModernMenu
+        (translated text, index and callback)
+    """
+    main_wid = properties.ObjectProperty()
+    click_timeout = properties.NumericProperty(0.4)
+
+    def on_touch_down(self, touch):
+        if not self.collide_point(*touch.pos):
+            return
+        t = partial(self.open_menu, touch)
+        touch.ud['menu_timeout'] = t
+        Clock.schedule_once(t, self.click_timeout)
+        return super(TouchMenuItemBehavior, self).on_touch_down(touch)
+
+    def do_item_action(self, touch):
+        pass
+
+    def on_touch_up(self, touch):
+        if touch.ud.get('menu_timeout'):
+            Clock.unschedule(touch.ud['menu_timeout'])
+            if self.collide_point(*touch.pos) and self.main_wid.menu is None:
+                self.do_item_action(touch)
+        return super(TouchMenuItemBehavior, self).on_touch_up(touch)
+
+    def open_menu(self, touch, dt):
+        self.main_wid.open_menu(self, touch)
+        del touch.ud['menu_timeout']
+
+    def getMenuChoices(self):
+        """return choice adapted to selected item
+
+        @return (list[dict]): choices ad expected by ModernMenu
+        """
+        return []
+
+
+class TouchMenuBehavior:
+    """Class to handle a menu appearing on long press on items
+
+    classes using this behaviour need to have a float_layout property
+    pointing the main FloatLayout.
+    """
+    float_layout = properties.ObjectProperty()
+
+    def __init__(self, *args, **kwargs):
+        super(TouchMenuBehavior, self).__init__(*args, **kwargs)
+        self.menu = None
+        self.menu_item = None
+
+    ## menu methods ##
+
+    def clean_fl_children(self, layout, children):
+        """insure that self.menu and self.menu_item are None when menu is dimissed"""
+        if self.menu is not None and self.menu not in children:
+            self.menu = self.menu_item = None
+
+    def clear_menu(self):
+        """remove menu if there is one"""
+        if self.menu is not None:
+            self.menu.dismiss()
+            self.menu = None
+            self.menu_item = None
+
+    def open_menu(self, item, touch):
+        """open menu for item
+
+        @param item(PathWidget): item when the menu has been requested
+        @param touch(kivy.input.MotionEvent): touch data
+        """
+        if self.menu_item == item:
+            return
+        self.clear_menu()
+        pos = self.to_widget(*touch.pos)
+        choices = item.getMenuChoices()
+        if not choices:
+            return
+        self.menu = TouchMenu(choices=choices,
+                              center=pos,
+                              size_hint=(None, None))
+        self.float_layout.add_widget(self.menu)
+        self.menu.start_display(touch)
+        self.menu_item = item
+
+    def on_float_layout(self, wid, float_layout):
+        float_layout.bind(children=self.clean_fl_children)
+
+
+class FilterBehavior(object):
+    """class to handle items filtering with animation"""
+
+    def __init__(self, *args, **kwargs):
+        super(FilterBehavior, self).__init__(*args, **kwargs)
+        self._filter_last = {}
+        self._filter_anim = Animation(width = 0,
+                                      height = 0,
+                                      opacity = 0,
+                                      d = 0.5)
+
+    def do_filter(self, parent, text, get_child_text, width_cb, height_cb,
+                  continue_tests=None):
+        """filter the children
+
+        filtered children will have a animation to set width, height and opacity to 0
+        @param parent(kivy.uix.widget.Widget): parent layout of the widgets to filter
+        @param text(unicode): filter text (if this text is not present in a child,
+            the child is filtered out)
+        @param get_child_text(callable): must retrieve child text
+            child is used as sole argument
+        @param width_cb(callable, int, None): method to retrieve width when opened
+            child is used as sole argument, int can be used instead of callable
+        @param height_cb(callable, int, None): method to retrieve height when opened
+            child is used as sole argument, int can be used instead of callable
+        @param continue_tests(list[callable]): list of test to skip the item
+            all callables take child as sole argument.
+            if any of the callable return True, the child is skipped (i.e. not filtered)
+        """
+        text = text.strip().lower()
+        filtering = len(text)>len(self._filter_last.get(parent, ''))
+        self._filter_last[parent] = text
+        for child in parent.children:
+            if continue_tests is not None and any((t(child) for t in continue_tests)):
+                continue
+            if text in get_child_text(child).lower():
+                self._filter_anim.cancel(child)
+                for key, method in (('width', width_cb),
+                                    ('height', height_cb),
+                                    ('opacity', lambda c: 1)):
+                    try:
+                        setattr(child, key, method(child))
+                    except TypeError:
+                        # method is not a callable, must be an int
+                        setattr(child, key, method)
+            elif (filtering
+                  and child.opacity > 0
+                  and not self._filter_anim.have_properties_to_animate(child)):
+                self._filter_anim.start(child)