view src/cagou.py @ 14:21a432afd06d

plugin system, first draft: - widgets are now handled with plugins, ContactList and WidgetSelector have been changed to be pluggins - everything in PLUGIN_INFO (including PLUGIN_INFO itself) is optional. Best guess is used if a value is missing - WidgetsHandler use default widget from plugins, which is WidgetSelector for now - PLUGIN_INFO is used in the same way as for backed plugins, with (for now) following possible keys: - "name": human readable name - "description": long description of what widget do - "import_name": unique short name used as reference for import - "main": main class name - "factory": callback used to instanciate a widget (see Cagou._defaultFactory) - "kv_file": path to the kv language file to load (same filename with ".kv" will be used if key is not found) other data should be added quickly, as a path to a file used as icon - host.getPluggedWidgets can be used to find loaded widgets. except_cls can be used to excluse a class (self.__class__ usually) - fixed host.switchWidget when old widget is already a CagouWidget - CagouWidget header new display the available widgets
author Goffi <goffi@goffi.org>
date Sat, 09 Jul 2016 16:02:44 +0200
parents 12a189fbb9ba
children 56838ad5c84b
line wrap: on
line source

#!/usr//bin/env python2
# -*- 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 <http://www.gnu.org/licenses/>.


from sat.core.i18n import _
import logging_setter
logging_setter.set_logging()
from constants import Const as C
from sat.core import log as logging
log = logging.getLogger(__name__)
from sat_frontends.quick_frontend.quick_app import QuickApp
from sat_frontends.bridge.DBus import DBusBridgeFrontend
# from sat_frontends.quick_frontend import quick_utils
import kivy
kivy.require('1.9.1')
import kivy.support
kivy.support.install_gobject_iteration()
from kivy.app import App
from kivy.lang import Builder
import xmlui
from profile_manager import ProfileManager
from widgets_handler import WidgetsHandler
from kivy.uix.boxlayout import BoxLayout
from cagou_widget import CagouWidget
from importlib import import_module
import os.path
import glob


class CagouRootWidget(BoxLayout):

    def __init__(self, widgets):
        super(CagouRootWidget, self).__init__(orientation=("vertical"))
        for wid in widgets:
            self.add_widget(wid)

    def change_widgets(self, widgets):
        self.clear_widgets()
        for wid in widgets:
            self.add_widget(wid)


class CagouApp(App):
    """Kivy App for Cagou"""

    def build(self):
        return CagouRootWidget([ProfileManager(self.host)])


class Cagou(QuickApp):
    MB_HANDLE = False

    def __init__(self):
        super(Cagou, self).__init__(create_bridge=DBusBridgeFrontend, xmlui=xmlui)
        self.app = CagouApp()
        self.app.host = self
        media_dir = self.app.media_dir = self.bridge.getConfig("", "media_dir")
        self.app.default_avatar = os.path.join(media_dir, "misc/default_avatar.png")
        self._plg_wids = []  # widget plugins
        self._import_plugins()

    def run(self):
        self.app.run()

    def _defaultFactory(self, host, plugin_info, target, profiles):
        """factory used to create widget instance when PLUGIN_INFO["factory"] is not set"""
        main_cls = plugin_info['main']
        return self.widgets.getOrCreateWidget(main_cls, target, on_new_widget=None, profiles=profiles)

    def _import_plugins(self):
        """Import all plugins"""
        self.default_wid = None
        plugins_path = os.path.dirname(__file__)
        plug_lst = [os.path.splitext(p)[0] for p in map(os.path.basename, glob.glob(os.path.join(plugins_path, "plugin*.py")))]
        imported_names = set()  # use to avoid loading 2 times plugin with same import name
        for plug in plug_lst:
            mod = import_module(plug)
            try:
                plugin_info = mod.PLUGIN_INFO
            except AttributeError:
                plugin_info = {}

            # 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:
                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 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:]

            # we need to load the kv file
            if 'kv_file' not in plugin_info:
                plugin_info['kv_file'] = u'{}.kv'.format(plug)
            Builder.load_file(plugin_info['kv_file'])

            # what is the main class ?
            main_cls = getattr(mod, plugin_info['main'])
            plugin_info['main'] = main_cls

            # 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

            self._plg_wids.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())

        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):
        """get available widgets plugin infos

        @param except_cls(None, class): if not None,
            widgets from this class will be excluded
        @return (list[dict]): available widgets plugin infos
        """
        lst = self._plg_wids[:]
        if except_cls is not None:
            for plugin_info in lst:
                if plugin_info['main'] == except_cls:
                    lst.remove(plugin_info)
                    break
        return lst

    def plugging_profiles(self):
        self.app.root.change_widgets([WidgetsHandler(self)])

    def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE):
        log.info(u"Profile presence status set to {show}/{status}".format(show=show, status=status))

    def switchWidget(self, old, new):
        """Replace old widget by new one

        old(CagouWidget): CagouWidget instance or a child
        new(CagouWidget): new widget instance
        """
        to_change = None
        if isinstance(old, CagouWidget):
            to_change = old
        else:
            for w in old.walk_reverse():
                if isinstance(w, CagouWidget):
                    to_change = w
                    break

        if to_change is None:
            log.error(u"no CagouWidget found when trying to switch widget")
        else:
            parent = to_change.parent
            idx = parent.children.index(to_change)
            parent.remove_widget(to_change)
            parent.add_widget(new, index=idx)


if __name__ == '__main__':
    Cagou().run()