# HG changeset patch # User Goffi # Date 1482680481 -3600 # Node ID c711be670ecdebb1f7a90908041d2aa8a5218302 # Parent c2a7234d13d2d79bca4a2e09f78d9a26f1ce9f8c core, chat: upload plugin system: - extented plugin system so it's not only used with main widget. It is also used for upload widgets and can be extended more - plugin file name is used to detect the type: plugin_wid_* for main widgets, plugin_upload_* for upload widget plugins - a new UploadMenu class allows to easily add an upload button which will use loaded plugins - plugin_info can now specify a list of allowed platforms in "platforms" key - file upload in chat has been moved to a plugin diff -r c2a7234d13d2 -r c711be670ecd src/cagou/core/cagou_main.py --- a/src/cagou/core/cagou_main.py Sat Dec 24 14:20:49 2016 +0100 +++ b/src/cagou/core/cagou_main.py Sun Dec 25 16:41:21 2016 +0100 @@ -56,7 +56,7 @@ from cagou_widget import CagouWidget from . import widgets_handler from .common import IconButton -from .menu import MenusWidget +from . import menu from importlib import import_module import os.path import glob @@ -152,7 +152,7 @@ self.manager.switch_to(screen) -class RootMenus(MenusWidget): +class RootMenus(menu.MenusWidget): pass @@ -292,7 +292,8 @@ self.app.host = self self.media_dir = self.app.media_dir = config.getConfig(main_config, '', 'media_dir') self.app.default_avatar = os.path.join(self.media_dir, "misc/default_avatar.png") - self._plg_wids = [] # widget plugins + self._plg_wids = [] # main widgets plugins + self._plg_wids_upload = [] # upload widgets plugins self._import_plugins() self._visible_widgets = {} # visible widgets by classes @@ -343,11 +344,30 @@ del self.app._profile_manager super(Cagou, self).postInit(profile_manager) - def _defaultFactory(self, plugin_info, target, profiles): - """factory used to create widget instance when PLUGIN_INFO["factory"] is not set""" + def _defaultFactoryMain(self, plugin_info, target, profiles): + """default factory used to create main widgets instances + + used when PLUGIN_INFO["factory"] is not set + @param plugin_info(dict): plugin datas + @param target: QuickWidget target + @param profiles(iterable): list of profiles + """ main_cls = plugin_info['main'] return self.widgets.getOrCreateWidget(main_cls, target, on_new_widget=None, profiles=iter(self.profiles)) + def _defaultFactoryUpload(self, plugin_info, callback, cancel_cb, profiles): + """default factory used to create upload widgets instances + + @param plugin_info(dict): plugin datas + @param callback(callable): method to call with path to file to upload + @param cancel_cb(callable): call when upload is cancelled + upload widget must be used as first argument + @param profiles(iterable): list of profiles + None if not specified + """ + main_cls = plugin_info['main'] + return main_cls(callback=callback, cancel_cb=cancel_cb) + ## plugins & kv import ## def _import_kv(self): @@ -369,29 +389,65 @@ plugin_glob = "plugin*.py" plug_lst = [os.path.splitext(p)[0] for p in map(os.path.basename, glob.glob(os.path.join(plugins_path, plugin_glob)))] - imported_names = set() # use to avoid loading 2 times plugin with same import name + imported_names_main = set() # used to avoid loading 2 times plugin with same import name + imported_names_upload = set() for plug in plug_lst: plugin_path = 'cagou.plugins.' + plug + + # we get type from plugin name + suff = plug[7:] + if u'_' not in suff: + log.error(u"invalid plugin name: {}, skipping".format(plug)) + continue + plugin_type = suff[:suff.find(u'_')] + + # and select the variable to use according to type + if plugin_type == C.PLUG_TYPE_WID: + imported_names = imported_names_main + default_factory = self._defaultFactoryMain + elif plugin_type == C.PLUG_TYPE_UPLOAD: + imported_names = imported_names_upload + default_factory = self._defaultFactoryUpload + else: + log.error(u"unknown plugin type {type_} for plugin {file_}, skipping".format( + type_ = plugin_type, + file_ = plug + )) + continue + plugins_set = self._getPluginsSet(plugin_type) + mod = import_module(plugin_path) try: plugin_info = mod.PLUGIN_INFO except AttributeError: plugin_info = {} + plugin_info['plugin_file'] = plug + plugin_info['plugin_type'] = plugin_type + + if 'platforms' in plugin_info: + if sys.platform not in plugin_info['platforms']: + log.info(u"{plugin_file} is not used on this platform, skipping".format(**plugin_info)) + continue + # import name is used to differentiate plugins if 'import_name' not in plugin_info: plugin_info['import_name'] = plug - if 'import_name' in imported_names: + if plugin_info['import_name'] in imported_names: log.warning(_(u"there is already a plugin named {}, ignoring new one").format(plugin_info['import_name'])) continue if plugin_info['import_name'] == C.WID_SELECTOR: + if plugin_type != C.PLUG_TYPE_WID: + log.error(u"{import_name} import name can only be used with {type_} type, skipping {name}".format(type_=C.PLUG_TYPE_WID, **plugin_info)) + continue # if WidgetSelector exists, it will be our default widget self.default_wid = plugin_info # we want everything optional, so we use plugin file name # if actual name is not found if 'name' not in plugin_info: - plugin_info['name'] = plug[plug.rfind('_')+1:] + name_start = 8 + len(plugin_type) + plugin_info['name'] = plug[name_start:] # we need to load the kv file if 'kv_file' not in plugin_info: @@ -406,7 +462,7 @@ # factory is used to create the instance # if not found, we use a defaut one with getOrCreateWidget if 'factory' not in plugin_info: - plugin_info['factory'] = self._defaultFactory + plugin_info['factory'] = default_factory # icons for size in ('small', 'medium'): @@ -421,38 +477,53 @@ path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir) plugin_info[key] = path - self._plg_wids.append(plugin_info) + plugins_set.append(plugin_info) if not self._plg_wids: log.error(_(u"no widget plugin found")) return # we want widgets sorted by names self._plg_wids.sort(key=lambda p: p['name'].lower()) + self._plg_wids_upload.sort(key=lambda p: p['name'].lower()) if self.default_wid is None: # we have no selector widget, we use the first widget as default self.default_wid = self._plg_wids[0] - def getPluggedWidgets(self, except_cls=None): + def _getPluginsSet(self, type_): + if type_ == C.PLUG_TYPE_WID: + return self._plg_wids + elif type_ == C.PLUG_TYPE_UPLOAD: + return self._plg_wids_upload + else: + raise KeyError(u"{} plugin type is unknown".format(type_)) + + def getPluggedWidgets(self, type_=C.PLUG_TYPE_WID, except_cls=None): """get available widgets plugin infos + @param type_(unicode): type of widgets to get + one of C.PLUG_TYPE_* constant @param except_cls(None, class): if not None, widgets from this class will be excluded @return (iter[dict]): available widgets plugin infos """ - for plugin_data in self._plg_wids: + plugins_set = self._getPluginsSet(type_) + for plugin_data in plugins_set: if plugin_data['main'] == except_cls: continue yield plugin_data - def getPluginInfo(self, **kwargs): + def getPluginInfo(self, type_=C.PLUG_TYPE_WID, **kwargs): """get first plugin info corresponding to filters + @param type_(unicode): type of widgets to get + one of C.PLUG_TYPE_* constant @param **kwargs: filter(s) to use, each key present here must also exist and be of the same value in requested plugin info @return (dict, None): found plugin info or None """ - for plugin_info in self._plg_wids: + plugins_set = self._getPluginsSet(type_) + for plugin_info in plugins_set: for k, w in kwargs.iteritems(): try: if plugin_info[k] != w: diff -r c2a7234d13d2 -r c711be670ecd src/cagou/core/constants.py --- a/src/cagou/core/constants.py Sat Dec 24 14:20:49 2016 +0100 +++ b/src/cagou/core/constants.py Sun Dec 25 16:41:21 2016 +0100 @@ -21,9 +21,12 @@ class Const(constants.Const): - APP_NAME = "Cagou" + APP_NAME = u"Cagou" LOG_OPT_SECTION = APP_NAME.lower() CONFIG_SECTION = APP_NAME.lower() - WID_SELECTOR = 'selector' - ICON_SIZES = ('small', 'medium') # small = 32, medium = 44 + WID_SELECTOR = u'selector' + ICON_SIZES = (u'small', u'medium') # small = 32, medium = 44 DEFAULT_WIDGET_ICON = u'{media}/misc/black.png' + + PLUG_TYPE_WID = u'wid' + PLUG_TYPE_UPLOAD = u'upload' diff -r c2a7234d13d2 -r c711be670ecd src/cagou/core/menu.py --- a/src/cagou/core/menu.py Sat Dec 24 14:20:49 2016 +0100 +++ b/src/cagou/core/menu.py Sun Dec 25 16:41:21 2016 +0100 @@ -157,3 +157,51 @@ about.title = ABOUT_TITLE about.content = AboutContent(text=ABOUT_CONTENT, markup=True) about.open() + + +class UploadMenu(contextmenu.ContextMenu): + # callback will be called with path to file to upload + callback = properties.ObjectProperty() + # cancel callback need to remove the widget for UI + # will be called with the widget to remove as argument + cancel_cb = properties.ObjectProperty() + # profiles if set will be sent to upload widget, may be used to get specific files + profiles = properties.ObjectProperty() + + def __init__(self, **kwargs): + super(UploadMenu, self).__init__(**kwargs) + if self.cancel_cb is None: + self.cancel_cb = self.onUploadCancelled + if self.profiles is None: + self.profiles = iter(G.host.profiles) + for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_UPLOAD): + item = contextmenu.ContextMenuTextItem( + text = plug_info['name'], + on_release = lambda dummy, plug_info=plug_info: self.do_callback(plug_info) + ) + self.add_widget(item) + + def show(self, caller_wid=None): + if caller_wid is not None: + pos = caller_wid.x, caller_wid.top + self.get_height() + else: + pos = G.host.app.root_window.mouse_pos + super(UploadMenu, self).show(*pos) + + def _closeUI(self, wid): + G.host.closeUI() + + def onUploadCancelled(self, wid): + self._closeUI(wid) + + def do_callback(self, plug_info): + self.hide() + if self.callback is None: + log.warning(u"UploadMenu callback is not set") + else: + wid = None + def onUploadCb(file_path): + self._closeUI(wid) + self.callback(file_path) + wid = plug_info['factory'](plug_info, onUploadCb, self.cancel_cb, self.profiles) + G.host.showExtraUI(wid) diff -r c2a7234d13d2 -r c711be670ecd src/cagou/kv/menu.kv --- a/src/cagou/kv/menu.kv Sat Dec 24 14:20:49 2016 +0100 +++ b/src/cagou/kv/menu.kv Sun Dec 25 16:41:21 2016 +0100 @@ -33,3 +33,6 @@ : cancel_handler_widget: self.parent + +: + cancel_handler_widget: self.parent if self.parent else self.orig_parent diff -r c2a7234d13d2 -r c711be670ecd src/cagou/plugins/plugin_upload_file.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_upload_file.py Sun Dec 25 16:41:21 2016 +0100 @@ -0,0 +1,42 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + + +from sat.core import log as logging +log = logging.getLogger(__name__) +from sat.core.i18n import _ +from kivy.uix.boxlayout import BoxLayout +from kivy import properties + + +PLUGIN_INFO = { + "name": _(u"file"), + "main": "FileUploader", + "description": _(u"upload a local file"), +} + + +class FileUploader(BoxLayout): + callback = properties.ObjectProperty() + cancel_cb = properties.ObjectProperty() + + def onUploadOK(self, filechooser): + if filechooser.selection: + file_path = filechooser.selection[0] + self.callback(file_path) diff -r c2a7234d13d2 -r c711be670ecd src/cagou/plugins/plugin_wid_chat.kv --- a/src/cagou/plugins/plugin_wid_chat.kv Sat Dec 24 14:20:49 2016 +0100 +++ b/src/cagou/plugins/plugin_wid_chat.kv Sun Dec 25 16:41:21 2016 +0100 @@ -14,8 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -#:import expanduser os.path.expanduser -#:import platform kivy.utils.platform : size_hint: None, None @@ -83,34 +81,31 @@ padding: root.mess_padding bold: True if root.mess_data.type == "info" else False -: - size_hint: 1, None - height: dp(40) - message_input: message_input - MessageInputWidget: - id: message_input - size_hint: 1, 1 - hint_text: "Enter your message here" - on_text_validate: root.parent.onSend(args[0]) - IconButton - # upload button - source: app.expand("{media}/icons/tango/actions/32/list-add.png") - allow_stretch: True - size_hint: None, 1 - width: max(self.texture_size[0], dp(40)) - on_release: root.parent.onUploadButton() -: - FileChooserListView: - id: filechooser - rootpath: "/" if platform == 'android' else expanduser('~') - Button: - text: "upload" +: + messages_widget: messages_widget + ScrollView: + size_hint: 1, 0.8 + scroll_y: 0 + do_scroll_x: False + MessagesWidget: + id: messages_widget + MessageInputBox: size_hint: 1, None - height: dp(50) - on_release: root.parent_chat.onUploadOK(filechooser) - Button: - text: "cancel" - size_hint: 1, None - height: dp(50) - on_release: root.parent_chat.onUploadCancel(filechooser) + height: dp(40) + message_input: message_input + MessageInputWidget: + id: message_input + size_hint: 1, 1 + hint_text: "Enter your message here" + on_text_validate: root.onSend(args[0]) + IconButton + # upload button + source: app.expand("{media}/icons/tango/actions/32/list-add.png") + allow_stretch: True + size_hint: None, 1 + width: max(self.texture_size[0], dp(40)) + on_release: upload_menu.show(self) + UploadMenu: + id: upload_menu + callback: root.onUploadOK diff -r c2a7234d13d2 -r c711be670ecd src/cagou/plugins/plugin_wid_chat.py --- a/src/cagou/plugins/plugin_wid_chat.py Sat Dec 24 14:20:49 2016 +0100 +++ b/src/cagou/plugins/plugin_wid_chat.py Sun Dec 25 16:41:21 2016 +0100 @@ -25,7 +25,6 @@ from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.stacklayout import StackLayout -from kivy.uix.scrollview import ScrollView from kivy.uix.textinput import TextInput from kivy.uix.label import Label from kivy.uix.image import AsyncImage @@ -548,25 +547,14 @@ pass -class FileUploader(BoxLayout): - - def __init__(self, parent_chat, **kwargs): - self.parent_chat = parent_chat - super(FileUploader, self).__init__(orientation='vertical', **kwargs) - - class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget): message_input = properties.ObjectProperty() + messages_widget = properties.ObjectProperty() def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): quick_chat.QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles) cagou_widget.CagouWidget.__init__(self) self.header_input.hint_text = u"{}".format(target) - scroll_view = ScrollView(size_hint=(1,0.8), scroll_y=0, do_scroll_x=False) - self.messages_widget = MessagesWidget() - scroll_view.add_widget(self.messages_widget) - self.add_widget(scroll_view) - self.add_widget(MessageInputBox()) self.host.addListener('progressError', self.onProgressError, profiles) self.host.addListener('progressFinished', self.onProgressFinished, profiles) self._waiting_pids = {} # waiting progress ids @@ -621,9 +609,6 @@ # TODO: display message to user log.warning(u"Can't upload file: {}".format(err_msg)) - def onUploadButton(self): - G.host.showExtraUI(FileUploader(self)) - def fileUploadDone(self, metadata, profile): log.debug("file uploaded: {}".format(metadata)) G.host.messageSend( @@ -642,21 +627,15 @@ else: self._waiting_pids[progress_id] = self.fileUploadDone - def onUploadOK(self, file_chooser): - if file_chooser.selection: - file_path = file_chooser.selection[0] - G.host.bridge.fileUpload( - file_path, - "", - "", - {"ignore_tls_errors": C.BOOL_TRUE}, # FIXME: should not be the default - self.profile, - callback = self.fileUploadCb - ) - G.host.closeUI() - - def onUploadCancel(self, file_chooser): - G.host.closeUI() + def onUploadOK(self, file_path): + G.host.bridge.fileUpload( + file_path, + "", + "", + {"ignore_tls_errors": C.BOOL_TRUE}, # FIXME: should not be the default + self.profile, + callback = self.fileUploadCb + ) def _mucJoinCb(self, joined_data): joined, room_jid_s, occupants, user_nick, subject, profile = joined_data