changeset 222:a676cb07c1cb

core (menu): TouchMenuBehaviour: moved code showing ModernMenu on item from file sharing plugin to a generic behaviour, so it can be re-used elsewhere.
author Goffi <goffi@goffi.org>
date Tue, 26 Jun 2018 20:26:21 +0200
parents e1a385a791cc
children 9e5f9f0cee48
files cagou/core/cagou_widget.py cagou/core/menu.py cagou/kv/cagou_widget.kv cagou/kv/menu.kv cagou/plugins/plugin_wid_file_sharing.kv cagou/plugins/plugin_wid_file_sharing.py
diffstat 6 files changed, 157 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/cagou/core/cagou_widget.py	Tue Jun 26 07:36:11 2018 +0200
+++ b/cagou/core/cagou_widget.py	Tue Jun 26 20:26:21 2018 +0200
@@ -66,7 +66,7 @@
             if p['main'] == self.__class__:
                 self.plugin_info = p
                 break
-        BoxLayout.__init__(self, orientation="vertical")
+        BoxLayout.__init__(self)
         self.selector = HeaderWidgetSelector(self)
 
     def switchWidget(self, plugin_info):
--- a/cagou/core/menu.py	Tue Jun 26 07:36:11 2018 +0200
+++ b/cagou/core/menu.py	Tue Jun 26 20:26:21 2018 +0200
@@ -28,12 +28,14 @@
 from kivy.uix.popup import Popup
 from cagou.core.utils import FilterBehavior
 from kivy import properties
-from kivy.garden import contextmenu
+from kivy.garden import contextmenu, modernmenu
 from sat_frontends.quick_frontend import quick_menus
 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
 
 ABOUT_TITLE = _(u"About {}".format(C.APP_NAME))
@@ -78,7 +80,8 @@
         profile = None
         if selected is not None:
             try:
-                profile = selected.profile
+                # FIXME: handle multi-profiles
+                profile = next(iter(selected.profiles))
             except AttributeError:
                 pass
 
@@ -129,7 +132,8 @@
         else:
             context_menu = contextmenu.ContextMenu()
             caller.add_widget(context_menu)
-            # FIXME: next line is needed after parent is set to avoid a display bug in contextmenu
+            # FIXME: next line is needed after parent is set to avoid
+            #        a display bug in contextmenu
             # TODO: fix this upstream
             context_menu._on_visible(False)
 
@@ -266,8 +270,11 @@
     # callback will be called with path to file to transfer
     # profiles if set will be sent to transfer widget, may be used to get specific files
     profiles = properties.ObjectProperty()
-    transfer_txt = _(u"Beware! The file will be sent to your server and stay unencrypted there\nServer admin(s) can see the file, and they choose how, when and if it will be deleted")
-    send_txt = _(u"The file will be sent unencrypted directly to your contact (without transiting by the server), except in some cases")
+    transfer_txt = _(u"Beware! The file will be sent to your server and stay unencrypted "
+                     u"there\nServer admin(s) can see the file, and they choose how, "
+                     u"when and if it will be deleted")
+    send_txt = _(u"The file will be sent unencrypted directly to your contact "
+                 u"(without transiting by the server), except in some cases")
     items_layout = properties.ObjectProperty()
     size_hint_close = (1, 0)
     size_hint_open = (1, 0.5)
@@ -295,8 +302,12 @@
                 self.callback(
                     file_path,
                     cleaning_cb,
-                    transfer_type = C.TRANSFER_UPLOAD if self.ids['upload_btn'].state == "down" else C.TRANSFER_SEND)
-            wid = plug_info['factory'](plug_info, onTransferCb, self.cancel_cb, self.profiles)
+                    transfer_type = (C.TRANSFER_UPLOAD
+                        if self.ids['upload_btn'].state == "down" else C.TRANSFER_SEND))
+            wid = plug_info['factory'](plug_info,
+                                       onTransferCb,
+                                       self.cancel_cb,
+                                       self.profiles)
             if not external:
                 G.host.showExtraUI(wid)
 
@@ -336,3 +347,100 @@
                        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/kv/cagou_widget.kv	Tue Jun 26 07:36:11 2018 +0200
+++ b/cagou/kv/cagou_widget.kv	Tue Jun 26 20:26:21 2018 +0200
@@ -53,6 +53,7 @@
 <CagouWidget>:
     header_input: header_input
     header_box: header_box
+    orientation: "vertical"
     BoxLayout:
         id: header_box
         size_hint: 1, None
--- a/cagou/kv/menu.kv	Tue Jun 26 07:36:11 2018 +0200
+++ b/cagou/kv/menu.kv	Tue Jun 26 20:26:21 2018 +0200
@@ -131,3 +131,17 @@
             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_file_sharing.kv	Tue Jun 26 07:36:11 2018 +0200
+++ b/cagou/plugins/plugin_wid_file_sharing.kv	Tue Jun 26 20:26:21 2018 +0200
@@ -66,7 +66,7 @@
 
 
 <LocalPathWidget>:
-    shared: root.filepath in root.sharing_wid.shared_paths
+    shared: root.filepath in root.main_wid.shared_paths
 
 
 <DeviceWidget>:
@@ -89,17 +89,3 @@
 <CategorySeparator>:
     size_hint: 1, None
     height: sp(20)
-
-
-<Menu>:
-    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_file_sharing.py	Tue Jun 26 07:36:11 2018 +0200
+++ b/cagou/plugins/plugin_wid_file_sharing.py	Tue Jun 26 20:26:21 2018 +0200
@@ -27,15 +27,14 @@
 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
+from cagou.core.menu import (EntitiesSelectorMenu, TouchMenuBehaviour,
+                             TouchMenuItemBehaviour)
 from cagou.core.utils import FilterBehavior
 from cagou import G
 from kivy import properties
 from kivy.uix.label import Label
 from kivy.uix.button import Button
 from kivy.uix.boxlayout import BoxLayout
-from kivy.garden import modernmenu
-from kivy.clock import Clock
 from kivy.metrics import dp
 from kivy import utils as kivy_utils
 from functools import partial
@@ -106,53 +105,19 @@
         return self.identities.values()[0].values()[0][0]
 
 
-class ItemWidget(BoxLayout):
-    click_timeout = properties.NumericProperty(0.4)
+class ItemWidget(TouchMenuItemBehaviour, BoxLayout):
+    name = properties.StringProperty()
     base_width = properties.NumericProperty(dp(100))
 
-    def __init__(self, sharing_wid, name):
-        self.sharing_wid = sharing_wid
-        self.name = name
-        super(ItemWidget, self).__init__()
-
-    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(ItemWidget, 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.sharing_wid.menu is None:
-                self.do_item_action(touch)
-        return super(ItemWidget, self).on_touch_up(touch)
-
-    def open_menu(self, touch, dt):
-        self.sharing_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 PathWidget(ItemWidget):
 
-    def __init__(self, sharing_wid, filepath):
+    def __init__(self, filepath, main_wid, **kw):
         name = os.path.basename(filepath)
         self.filepath = os.path.normpath(filepath)
         if self.filepath == u'.':
             self.filepath = u''
-        super(PathWidget, self).__init__(sharing_wid, name)
+        super(PathWidget, self).__init__(name=name, main_wid=main_wid, **kw)
 
     @property
     def is_dir(self):
@@ -160,7 +125,7 @@
 
     def do_item_action(self, touch):
         if self.is_dir:
-            self.sharing_wid.current_dir = self.filepath
+            self.main_wid.current_dir = self.filepath
 
     def open_menu(self, touch, dt):
         log.debug(_(u"opening menu for {path}").format(path=self.filepath))
@@ -178,19 +143,19 @@
         if self.shared:
             choices.append(dict(text=_(u'unshare'),
                                 index=len(choices)+1,
-                                callback=self.sharing_wid.unshare))
+                                callback=self.main_wid.unshare))
         else:
             choices.append(dict(text=_(u'share'),
                                 index=len(choices)+1,
-                                callback=self.sharing_wid.share))
+                                callback=self.main_wid.share))
         return choices
 
 
 class RemotePathWidget(PathWidget):
 
-    def __init__(self, sharing_wid, filepath, type_):
+    def __init__(self, main_wid, filepath, type_, **kw):
         self.type_ = type_
-        super(RemotePathWidget, self).__init__(sharing_wid, filepath)
+        super(RemotePathWidget, self).__init__(filepath, main_wid=main_wid, **kw)
 
     @property
     def is_dir(self):
@@ -199,17 +164,17 @@
     def do_item_action(self, touch):
         if self.is_dir:
             if self.filepath == u'..':
-                self.sharing_wid.remote_entity = u''
+                self.main_wid.remote_entity = u''
             else:
                 super(RemotePathWidget, self).do_item_action(touch)
         else:
-            self.sharing_wid.request_item(self)
+            self.main_wid.request_item(self)
             return True
 
 
 class DeviceWidget(ItemWidget):
 
-    def __init__(self, sharing_wid, entity_jid, identities):
+    def __init__(self, main_wid, entity_jid, identities, **kw):
         self.entity_jid = entity_jid
         self.identities = identities
         own_jid = next(G.host.profiles.itervalues()).whoami
@@ -223,7 +188,7 @@
         else:
             name = _(u"sharing component")
 
-        super(DeviceWidget, self).__init__(sharing_wid, name)
+        super(DeviceWidget, self).__init__(name=name, main_wid=main_wid, **kw)
 
     def getSymbol(self):
         if self.identities.type == 'desktop':
@@ -238,21 +203,17 @@
             return 'desktop'
 
     def do_item_action(self, touch):
-        self.sharing_wid.remote_entity = self.entity_jid
-        self.sharing_wid.remote_dir = u''
+        self.main_wid.remote_entity = self.entity_jid
+        self.main_wid.remote_dir = u''
 
 
 class CategorySeparator(Label):
     pass
 
 
-class Menu(modernmenu.ModernMenu):
-    pass
-
-
-class FileSharing(quick_widgets.QuickWidget, cagou_widget.CagouWidget, FilterBehavior):
+class FileSharing(quick_widgets.QuickWidget, cagou_widget.CagouWidget, FilterBehavior,
+                  TouchMenuBehaviour):
     SINGLE=False
-    float_layout = properties.ObjectProperty()
     layout = properties.ObjectProperty()
     mode = properties.OptionProperty(MODE_LOCAL, options=[MODE_VIEW, MODE_LOCAL])
     local_dir = properties.StringProperty(expanduser(u'~'))
@@ -265,6 +226,7 @@
         quick_widgets.QuickWidget.__init__(self, host, target, profiles)
         cagou_widget.CagouWidget.__init__(self)
         FilterBehavior.__init__(self)
+        TouchMenuBehaviour.__init__(self)
         self.mode_btn = ModeBtn(self)
         self.mode_btn.bind(on_release=self.change_mode)
         self.headerInputAddExtra(self.mode_btn)
@@ -272,9 +234,6 @@
                   remote_dir=self.update_view,
                   remote_entity=self.update_view)
         self.update_view()
-        self.menu = None
-        self.menu_item = None
-        self.float_layout.bind(children=self.clean_fl_children)
         if not FileSharing.signals_registered:
             # FIXME: we use this hack (registering the signal for the whole class) now
             #        as there is currently no unregisterSignal available in bridges
@@ -375,8 +334,8 @@
         for file_data in files_data:
             filepath = os.path.join(self.current_dir, file_data[u'name'])
             item = RemotePathWidget(
-                self,
                 filepath=filepath,
+                main_wid=self,
                 type_=file_data[u'type'])
             self.layout.add_widget(item)
 
@@ -400,7 +359,7 @@
 
         if self.mode == MODE_LOCAL:
             filepath = os.path.join(self.local_dir, u'..')
-            self.layout.add_widget(LocalPathWidget(sharing_wid=self, filepath=filepath))
+            self.layout.add_widget(LocalPathWidget(filepath=filepath, main_wid=self))
             try:
                 files = sorted(os.listdir(self.local_dir))
             except OSError as e:
@@ -415,8 +374,8 @@
                 return
             for f in files:
                 filepath = os.path.join(self.local_dir, f)
-                self.layout.add_widget(LocalPathWidget(sharing_wid=self,
-                                                       filepath=filepath))
+                self.layout.add_widget(LocalPathWidget(filepath=filepath,
+                                                       main_wid=self))
         elif self.mode == MODE_VIEW:
             if not self.remote_entity:
                 self.discover_devices()
@@ -425,8 +384,8 @@
                 # so user can return to previous list even in case of error
                 parent_path = os.path.join(self.remote_dir, u'..')
                 item = RemotePathWidget(
-                    self,
                     filepath = parent_path,
+                    main_wid=self,
                     type_ = C.FILE_TYPE_DIRECTORY)
                 self.layout.add_widget(item)
                 self.host.bridge.FISList(
@@ -437,40 +396,6 @@
                     callback=self.FISListCb,
                     errback=self.FISListEb)
 
-    ## 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 = Menu(choices=choices,
-                         center=pos,
-                         size_hint=(None, None))
-        self.float_layout.add_widget(self.menu)
-        self.menu.start_display(touch)
-        self.menu_item = item
-
     ## Share methods ##
 
     def do_share(self, entities_jids, item):