# HG changeset patch # User Goffi # Date 1530037581 -7200 # Node ID a676cb07c1cbd77c26a28250376ddf49a9bd2ee3 # Parent e1a385a791cc8d29205f89113028d5ee1c54c0ad core (menu): TouchMenuBehaviour: moved code showing ModernMenu on item from file sharing plugin to a generic behaviour, so it can be re-used elsewhere. diff -r e1a385a791cc -r a676cb07c1cb cagou/core/cagou_widget.py --- 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): diff -r e1a385a791cc -r a676cb07c1cb cagou/core/menu.py --- 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) diff -r e1a385a791cc -r a676cb07c1cb cagou/kv/cagou_widget.kv --- 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 @@ : header_input: header_input header_box: header_box + orientation: "vertical" BoxLayout: id: header_box size_hint: 1, None diff -r e1a385a791cc -r a676cb07c1cb cagou/kv/menu.kv --- 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) + + +: + 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) + +: + bg_color: app.c_sec[:3] + [0.9] + padding: dp(5), dp(5) + radius: dp(100) diff -r e1a385a791cc -r a676cb07c1cb cagou/plugins/plugin_wid_file_sharing.kv --- 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 @@ : - shared: root.filepath in root.sharing_wid.shared_paths + shared: root.filepath in root.main_wid.shared_paths : @@ -89,17 +89,3 @@ : size_hint: 1, None height: sp(20) - - -: - 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) - -: - bg_color: app.c_sec[:3] + [0.9] - padding: dp(5), dp(5) - radius: dp(100) diff -r e1a385a791cc -r a676cb07c1cb cagou/plugins/plugin_wid_file_sharing.py --- 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):