changeset 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 b0af45a92055
children 84ff5c917064
files cagou/core/behaviors.py cagou/core/common.py cagou/core/common_widgets.py cagou/core/menu.py cagou/core/utils.py cagou/kv/behaviors.kv cagou/kv/common.kv cagou/kv/menu.kv cagou/plugins/plugin_wid_chat.kv cagou/plugins/plugin_wid_chat.py cagou/plugins/plugin_wid_contact_list.py cagou/plugins/plugin_wid_file_sharing.py cagou/plugins/plugin_wid_remote.py
diffstat 13 files changed, 339 insertions(+), 276 deletions(-) [+]
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)
--- a/cagou/core/common.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/core/common.py	Wed Feb 12 20:02:58 2020 +0100
@@ -27,13 +27,15 @@
 from kivy.uix.label import Label
 from kivy.uix.behaviors import ButtonBehavior
 from kivy.uix.behaviors import ToggleButtonBehavior
+from kivy.uix.stacklayout import StackLayout
 from kivy.uix.boxlayout import BoxLayout
 from kivy.uix.scrollview import ScrollView
 from kivy.event import EventDispatcher
 from kivy.metrics import dp
 from kivy import properties
 from sat_frontends.quick_frontend import quick_chat
-from cagou.core.constants import Const as C
+from .constants import Const as C
+from .common_widgets import CategorySeparator
 from cagou import G
 
 log = logging.getLogger(__name__)
@@ -63,17 +65,26 @@
     base_width = dp(150)
     avatar_layout = properties.ObjectProperty()
     avatar = properties.ObjectProperty()
+    badge = properties.ObjectProperty(allownone=True)
+    badge_text = properties.StringProperty('')
     profile = properties.StringProperty()
     data = properties.DictProperty()
     jid = properties.StringProperty('')
 
-    def on_kv_post(self, __):
-        if self.data and self.data.get('notifs'):
-            notif = NotifLabel(
-                pos_hint={"right": 0.8, "y": 0},
-                text=str(len(self.data['notifs']))
-            )
-            self.avatar_layout.add_widget(notif)
+    def on_badge_text(self, wid, text):
+        if text:
+            if self.badge is not None:
+                self.badge.text = text
+            else:
+                self.badge = NotifLabel(
+                    pos_hint={"right": 0.8, "y": 0},
+                    text=text,
+                )
+                self.avatar_layout.add_widget(self.badge)
+        else:
+            if self.badge is not None:
+                self.avatar_layout.remove_widget(self.badge)
+                self.badge = None
 
 
 class ContactButton(ButtonBehavior, ContactItem):
@@ -174,12 +185,25 @@
             self.add_widget(icon_wid)
 
 
+class JidSelectorCategoryLayout(StackLayout):
+    pass
+
+
 class JidSelector(ScrollView, EventDispatcher):
     layout = properties.ObjectProperty(None)
+    # if item_class is changed, the properties must be the same as for ContactButton
+    # and ordering must be supported
+    item_class = properties.ObjectProperty(ContactButton)
+    add_separators = properties.ObjectProperty(True)
     # list of item to show, can be:
-    #    - a well-known string like:
-    #       * "roster": to show all roster jids
-    #       * "opened_chats": to show jids of all opened chat widgets
+    #    - a well-known string which can be:
+    #       * "roster": all roster jids
+    #       * "opened_chats": all opened chat widgets
+    #       * "bookmarks": MUC bookmarks
+    #       A layout will be created each time and stored in the attribute of the same
+    #       name.
+    #       If add_separators is True, a CategorySeparator will be added on top of each
+    #       layout.
     #    - a kivy Widget, which will be added to the layout (notable useful with
     #      common_widgets.CategorySeparator)
     #    - a callable, which must return an iterable of kwargs for ContactButton
@@ -190,6 +214,10 @@
 
     def __init__(self, **kwargs):
         self.register_event_type('on_select')
+        # list of layouts containing items
+        self.items_layouts = []
+        # jid to list of ContactButton instances map
+        self.items_map = {}
         super().__init__(**kwargs)
 
     def on_kv_post(self, wid):
@@ -210,6 +238,13 @@
         log.debug("onContactsFilled event received")
         self.update()
 
+
+    def _createItem(self, **kwargs):
+        item = self.item_class(**kwargs)
+        jid = kwargs['jid']
+        self.items_map.setdefault(jid, []).append(item)
+        return item
+
     def update(self):
         log.debug("starting update")
         self.layout.clear_widgets()
@@ -228,52 +263,69 @@
             elif callable(item):
                 items_kwargs = item()
                 for item_kwargs in items_kwargs:
-                    item = ContactButton(**item_kwargs)
+                    item = self._createItem(**items_kwargs)
                     item.bind(on_press=partial(self.dispatch, 'on_select'))
                     self.layout.add_widget(item)
             else:
                 log.error(f"unmanaged to_show item type: {item!r}")
 
-    def addOpenedChatsItems(self):
-        opened_chats = G.host.widgets.getWidgets(
-            quick_chat.QuickChat,
-            profiles = G.host.profiles)
+    def addCategoryLayout(self, label=None):
+        category_layout = JidSelectorCategoryLayout()
+
+        if label and self.add_separators:
+            category_layout.add_widget(CategorySeparator(text=label))
+
+        self.layout.add_widget(category_layout)
+        self.items_layouts.append(category_layout)
+        return category_layout
 
-        for wid in opened_chats:
-            contact_list = G.host.contact_lists[wid.profile]
-            data=contact_list.getItem(wid.target)
-            notifs = list(G.host.getNotifs(wid.target, profile=wid.profile))
-            if notifs:
-                # we shallow copy the dict to have the notification displayed only with
-                # opened chats (otherwise, the counter would appear on each other
-                # instance of ContactButton for this entity, i.e. in roster too).
-                data = data.copy()
-                data['notifs'] = notifs
-            try:
-                item = ContactButton(
-                    jid=wid.target,
-                    data=data,
-                    profile=wid.profile,
-                )
-            except Exception as e:
-                log.warning(f"Can't add contact {wid.target}: {e}")
+    def getItemFromWid(self, wid):
+        """create JidSelector item from QuickChat widget"""
+        contact_list = G.host.contact_lists[wid.profile]
+        data=contact_list.getItem(wid.target)
+        try:
+            item = self._createItem(
+                jid=wid.target,
+                data=data,
+                profile=wid.profile,
+            )
+        except Exception as e:
+            log.warning(f"Can't add contact {wid.target}: {e}")
+            return
+        notifs = list(G.host.getNotifs(wid.target, profile=wid.profile))
+        if notifs:
+            item.badge_text = str(len(notifs))
+        item.bind(on_press=partial(self.dispatch, 'on_select'))
+        return item
+
+    def addOpenedChatsItems(self):
+        self.opened_chats = category_layout = self.addCategoryLayout(_("Opened chats"))
+        widgets = sorted(G.host.widgets.getWidgets(
+            quick_chat.QuickChat,
+            profiles = G.host.profiles,
+            with_duplicates=False))
+
+        for wid in widgets:
+            item = self.getItemFromWid(wid)
+            if item is None:
                 continue
-            item.bind(on_press=partial(self.dispatch, 'on_select'))
-            self.layout.add_widget(item)
+            category_layout.add_widget(item)
 
     def addRosterItems(self):
+        self.roster = category_layout = self.addCategoryLayout(_("Your contacts"))
         for profile in G.host.profiles:
             contact_list = G.host.contact_lists[profile]
             for entity_jid in sorted(contact_list.roster):
-                item = ContactButton(
+                item = self._createItem(
                     jid=entity_jid,
                     data=contact_list.getItem(entity_jid),
                     profile=profile,
                 )
                 item.bind(on_press=partial(self.dispatch, 'on_select'))
-                self.layout.add_widget(item)
+                category_layout.add_widget(item)
 
     def addBookmarksItems(self):
+        self.bookmarks = category_layout = self.addCategoryLayout(_("Your chat rooms"))
         for profile in G.host.profiles:
             profile_manager = G.host.profiles[profile]
             try:
@@ -288,10 +340,10 @@
                     cache = contact_list.getItem(entity_jid)
                 except KeyError:
                     cache = {}
-                item = ContactButton(
+                item = self._createItem(
                     jid=entity_jid,
                     data=cache,
                     profile=profile,
                 )
                 item.bind(on_press=partial(self.dispatch, 'on_select'))
-                self.layout.add_widget(item)
+                category_layout.add_widget(item)
--- a/cagou/core/common_widgets.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/core/common_widgets.py	Wed Feb 12 20:02:58 2020 +0100
@@ -22,7 +22,7 @@
 from sat.core.i18n import _
 from kivy.uix.label import Label
 from kivy.uix.boxlayout import BoxLayout
-from cagou.core.menu import TouchMenuItemBehaviour
+from .behaviors import TouchMenuItemBehavior
 from kivy import properties
 from kivy.metrics import dp
 from kivy.clock import Clock
@@ -82,7 +82,7 @@
         return names[0]
 
 
-class ItemWidget(TouchMenuItemBehaviour, BoxLayout):
+class ItemWidget(TouchMenuItemBehavior, BoxLayout):
     name = properties.StringProperty()
     base_width = properties.NumericProperty(dp(100))
 
--- a/cagou/core/menu.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/core/menu.py	Wed Feb 12 20:02:58 2020 +0100
@@ -26,13 +26,11 @@
 from kivy.uix.label import Label
 from kivy.uix.button import Button
 from kivy.uix.popup import Popup
-from cagou.core.utils import FilterBehavior
+from .behaviors import FilterBehavior
 from kivy import properties
-from kivy_garden import modernmenu
 from kivy.core.window import Window
 from kivy.animation import Animation
 from kivy.metrics import dp
-from kivy.clock import Clock
 from cagou import G
 from functools import partial
 import webbrowser
@@ -295,105 +293,8 @@
 
     def do_filter_input(self, filter_input, text):
         self.layout.spacing = 0 if text else dp(5)
-        self.do_filter(self.layout.children,
+        self.do_filter(self.layout,
                        text,
                        lambda c: c.jid,
                        width_cb=lambda c: c.width,
                        height_cb=lambda c: dp(70))
-
-
-class TouchMenu(modernmenu.ModernMenu):
-    pass
-
-
-class TouchMenuItemBehaviour(object):
-    """Class to use on every item where a menu may appear
-
-    main_wid attribute must be set to the class inheriting from TouchMenuBehaviour
-    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(TouchMenuItemBehaviour, 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(TouchMenuItemBehaviour, 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 TouchMenuBehaviour(object):
-    """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(TouchMenuBehaviour, 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)
--- a/cagou/core/utils.py	Wed Feb 12 20:02:58 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-#!/usr//bin/env python2
-
-
-# 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/>.
-
-"""misc utils/behaviors"""
-
-
-from kivy.animation import Animation
-
-
-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, children, 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 children(kivy.uix.widget.Widget): 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)
-        self._filter_last = text
-        for child in 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/behaviors.kv	Wed Feb 12 20:02:58 2020 +0100
@@ -0,0 +1,28 @@
+# 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/>.
+
+<TouchMenu>:
+    creation_direction: -1
+    radius: dp(25)
+    creation_timeout: .4
+    cancel_color: app.c_sec_light[:3] + [0.3]
+    color: app.c_sec
+    line_width: dp(2)
+
+<ModernMenuLabel>:
+    bg_color: app.c_sec[:3] + [0.9]
+    padding: dp(5), dp(5)
+    radius: dp(100)
--- a/cagou/kv/common.kv	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/kv/common.kv	Wed Feb 12 20:02:58 2020 +0100
@@ -136,6 +136,11 @@
     color: app.c_sec_light
 
 
+<JidSelectorCategoryLayout>:
+    size_hint: 1, None
+    height: self.minimum_height
+    spacing: 0
+
 <JidSelector>:
     layout: layout
     StackLayout:
--- a/cagou/kv/menu.kv	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/kv/menu.kv	Wed Feb 12 20:02:58 2020 +0100
@@ -131,17 +131,3 @@
             size_hint: 1, None
             height: self.minimum_height
             spacing: dp(5)
-
-
-<TouchMenu>:
-    creation_direction: -1
-    radius: dp(25)
-    creation_timeout: .4
-    cancel_color: app.c_sec_light[:3] + [0.3]
-    color: app.c_sec
-    line_width: dp(2)
-
-<ModernMenuLabel>:
-    bg_color: app.c_sec[:3] + [0.9]
-    padding: dp(5), dp(5)
-    radius: dp(100)
--- a/cagou/plugins/plugin_wid_chat.kv	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/plugins/plugin_wid_chat.kv	Wed Feb 12 20:02:58 2020 +0100
@@ -224,13 +224,5 @@
         # we call update() explicitly in on_pre_enter
         implicit_update: False
         on_select: root.on_select(args[1])
-        to_show:
-            [
-            CategorySeparator(text=_("Opened chats")),
-            "opened_chats",
-            CategorySeparator(text=_("Your contacts")),
-            "roster",
-            CategorySeparator(text=_("Your chat rooms")),
-            "bookmarks",
-            ]
+        to_show: ["opened_chats", "roster", "bookmarks"]
 
--- a/cagou/plugins/plugin_wid_chat.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/plugins/plugin_wid_chat.py	Wed Feb 12 20:02:58 2020 +0100
@@ -38,14 +38,14 @@
 from sat_frontends.quick_frontend import quick_chat
 from sat_frontends.tools import jid
 from cagou import G
-from cagou.core.constants import Const as C
-from cagou.core import cagou_widget
-from cagou.core import xmlui
-from cagou.core.image import Image
-from cagou.core.common import SymbolButton, JidButton
-from cagou.core.utils import FilterBehavior
-from cagou.core import menu
-from cagou.core.common import ContactButton
+from ..core.constants import Const as C
+from ..core import cagou_widget
+from ..core import xmlui
+from ..core.image import Image
+from ..core.common import SymbolButton, JidButton
+from ..core.behaviors import FilterBehavior
+from ..core import menu
+from ..core.common import ContactButton
 
 log = logging.getLogger(__name__)
 
@@ -949,14 +949,15 @@
 
     def onHeaderInputComplete(self, wid, text, **kwargs):
         """we filter items when text is entered in input box"""
-        self.do_filter(
-            self.jid_selector.children[0].children,
-            text,
-            # we append nick to jid to filter on both
-            lambda c: c.jid + c.data.get('nick', ''),
-            width_cb=lambda c: c.base_width,
-            height_cb=lambda c: c.minimum_height,
-            continue_tests=[lambda c: not isinstance(c, ContactButton)])
+        for layout in self.jid_selector.items_layouts:
+            self.do_filter(
+                layout,
+                text,
+                # we append nick to jid to filter on both
+                lambda c: c.jid + c.data.get('nick', ''),
+                width_cb=lambda c: c.base_width,
+                height_cb=lambda c: c.minimum_height,
+                continue_tests=[lambda c: not isinstance(c, ContactButton)])
 
 
 PLUGIN_INFO["factory"] = Chat.factory
--- a/cagou/plugins/plugin_wid_contact_list.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/plugins/plugin_wid_contact_list.py	Wed Feb 12 20:02:58 2020 +0100
@@ -30,8 +30,8 @@
 from ..core import cagou_widget
 from ..core.constants import Const as C
 from ..core.common import ContactItem
-from ..core.utils import FilterBehavior
-from ..core.menu import SideMenu, TouchMenuBehaviour, TouchMenuItemBehaviour
+from ..core.behaviors import FilterBehavior, TouchMenuBehavior, TouchMenuItemBehavior
+from ..core.menu import SideMenu
 
 
 log = logging.getLogger(__name__)
@@ -99,7 +99,7 @@
             message=_("error while trying to remove contact: {msg}")))
 
 
-class CLContactItem(TouchMenuItemBehaviour, ContactItem):
+class CLContactItem(TouchMenuItemBehavior, ContactItem):
 
     def do_item_action(self, touch):
         assert self.profile
@@ -116,7 +116,7 @@
 
 
 class ContactList(QuickContactList, cagou_widget.CagouWidget, FilterBehavior,
-                  TouchMenuBehaviour):
+                  TouchMenuBehavior):
     float_layout = properties.ObjectProperty()
     layout = properties.ObjectProperty()
     use_header_input = True
@@ -143,7 +143,7 @@
         DelContactMenu(contact_item=item).show()
 
     def onHeaderInputComplete(self, wid, text):
-        self.do_filter(self.layout.children,
+        self.do_filter(self.layout,
                        text,
                        lambda c: c.jid,
                        width_cb=lambda c: c.base_width,
--- a/cagou/plugins/plugin_wid_file_sharing.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/plugins/plugin_wid_file_sharing.py	Wed Feb 12 20:02:58 2020 +0100
@@ -26,11 +26,11 @@
 from sat.tools.common import files_utils
 from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.tools import jid
-from cagou.core.constants import Const as C
-from cagou.core import cagou_widget
-from cagou.core.menu import EntitiesSelectorMenu, TouchMenuBehaviour
-from cagou.core.utils import FilterBehavior
-from cagou.core.common_widgets import (Identities, ItemWidget, DeviceWidget,
+from ..core.constants import Const as C
+from ..core import cagou_widget
+from ..core.menu import EntitiesSelectorMenu
+from ..core.behaviors import TouchMenuBehavior, FilterBehavior
+from ..core.common_widgets import (Identities, ItemWidget, DeviceWidget,
                                        CategorySeparator)
 from cagou import G
 from kivy import properties
@@ -148,7 +148,7 @@
 
 
 class FileSharing(quick_widgets.QuickWidget, cagou_widget.CagouWidget, FilterBehavior,
-                  TouchMenuBehaviour):
+                  TouchMenuBehavior):
     SINGLE=False
     layout = properties.ObjectProperty()
     mode = properties.OptionProperty(MODE_VIEW, options=[MODE_VIEW, MODE_LOCAL])
@@ -163,7 +163,7 @@
         quick_widgets.QuickWidget.__init__(self, host, target, profiles)
         cagou_widget.CagouWidget.__init__(self)
         FilterBehavior.__init__(self)
-        TouchMenuBehaviour.__init__(self)
+        TouchMenuBehavior.__init__(self)
         self.mode_btn = ModeBtn(self)
         self.mode_btn.bind(on_release=self.change_mode)
         self.headerInputAddExtra(self.mode_btn)
@@ -216,7 +216,7 @@
         """we filter items when text is entered in input box"""
         if '/' in text:
             return
-        self.do_filter(self.layout.children,
+        self.do_filter(self.layout,
                        text,
                        lambda c: c.name,
                        width_cb=lambda c: c.base_width,
--- a/cagou/plugins/plugin_wid_remote.py	Wed Feb 12 20:02:58 2020 +0100
+++ b/cagou/plugins/plugin_wid_remote.py	Wed Feb 12 20:02:58 2020 +0100
@@ -21,11 +21,10 @@
 from sat.core import log as logging
 from sat.core.i18n import _
 from sat_frontends.quick_frontend import quick_widgets
-from cagou.core import cagou_widget
-from cagou.core.constants import Const as C
-from cagou.core.menu import TouchMenuBehaviour
-from cagou.core.utils import FilterBehavior
-from cagou.core.common_widgets import (Identities, ItemWidget, DeviceWidget,
+from ..core import cagou_widget
+from ..core.constants import Const as C
+from ..core.behaviors import TouchMenuBehavior, FilterBehavior
+from ..core.common_widgets import (Identities, ItemWidget, DeviceWidget,
                                        CategorySeparator)
 from sat.tools.common import template_xmlui
 from cagou.core import xmlui
@@ -177,7 +176,7 @@
 
 
 class RemoteControl(quick_widgets.QuickWidget, cagou_widget.CagouWidget, FilterBehavior,
-                  TouchMenuBehaviour):
+                  TouchMenuBehavior):
     SINGLE=False
     layout = properties.ObjectProperty()
 
@@ -185,7 +184,7 @@
         quick_widgets.QuickWidget.__init__(self, host, target, profiles)
         cagou_widget.CagouWidget.__init__(self)
         FilterBehavior.__init__(self)
-        TouchMenuBehaviour.__init__(self)
+        TouchMenuBehavior.__init__(self)
         self.stack_layout = None
         self.showRootWidget()