view src/cagou/core/cagou_main.py @ 29:8b5827c43155

notes first draft: Implementation of XMLUI notes. There is a new header on top of root widget which display notifications, and notes are shown for a couple of seconds. A blue Cagou head appear when there are notes, and user can display 10 last when clicking on it. This header will probably not be present on platforms such as Android, because there is already a system-wide notifications handler which can be used instead (saving visual space).
author Goffi <goffi@goffi.org>
date Sun, 21 Aug 2016 15:15:25 +0200
parents 9f9532eb835f
children 4f9e701d76b4
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
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
from kivy import properties
import xmlui
from profile_manager import ProfileManager
from widgets_handler import WidgetsHandler
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.dropdown import DropDown
from cagou_widget import CagouWidget
from .common import IconButton
from importlib import import_module
import os.path
import glob
import cagou.plugins
import cagou.kv


class NotifIcon(IconButton):

    def __init__(self, callback, callback_args):
        self._callback = callback
        self._callback_args = callback_args
        super(NotifIcon, self).__init__()

    def on_release(self):
        self.parent.remove_widget(self)
        self._callback(*self._callback_args)


class Note(Label):
    title = properties.StringProperty()
    message = properties.StringProperty()
    level = properties.OptionProperty(C.XMLUI_DATA_LVL_DEFAULT, options=list(C.XMLUI_DATA_LVLS))


class NoteDrop(Note):
    pass


class NotesDrop(DropDown):
    clear_btn = properties.ObjectProperty()

    def __init__(self, notes):
        super(NotesDrop, self).__init__()
        self.notes = notes

    def open(self, widget):
        self.clear_widgets()
        for n in self.notes:
            self.add_widget(NoteDrop(title=n.title, message=n.message, level=n.level))
        self.add_widget(self.clear_btn)
        super(NotesDrop, self).open(widget)


class RootHeadWidget(BoxLayout):
    """Notifications widget"""
    manager = properties.ObjectProperty()
    notes = properties.ListProperty()

    def __init__(self):
        super(RootHeadWidget, self).__init__()
        self.notes_last = None
        self.notes_event = None
        self.notes_drop = NotesDrop(self.notes) # auto_with=False, width=100)

    def addNotif(self, callback, *args):
        icon = NotifIcon(callback, args)
        self.add_widget(icon)

    def addNote(self, title, message, level):
        note = Note(title=title, message=message, level=level)
        self.notes.append(note)
        if len(self.notes) > 10:
            del self.notes[:-10]
        if self.notes_event is None:
            self.notes_event = Clock.schedule_interval(self._displayNextNote, 5)
            self._displayNextNote()

    def _displayNextNote(self, dummy=None):
        screen = Screen()
        try:
            idx = self.notes.index(self.notes_last) + 1
        except ValueError:
            idx = 0
        try:
            note = self.notes_last = self.notes[idx]
        except IndexError:
            self.notes_event.cancel()
            self.notes_event = None
        else:
            screen.add_widget(note)
        self.manager.switch_to(screen)


class CagouRootWidget(BoxLayout):

    def __init__(self, main_widget):
        super(CagouRootWidget, self).__init__(orientation=("vertical"))
        # header
        self._head_widget = RootHeadWidget()
        self.add_widget(self._head_widget)

        # body
        self._manager = ScreenManager()
        main_screen = Screen(name='main')
        main_screen.add_widget(main_widget)
        self._manager.add_widget(main_screen)
        self.add_widget(self._manager)
        self.change_widget(main_widget)

    def change_widget(self, main_widget, screen="main"):
        """change main widget"""
        main_screen = self._manager.get_screen(screen)
        main_screen.clear_widgets()
        main_screen.add_widget(main_widget)

    def newAction(self, handler, action_data, id_, security_limit, profile):
        """Add a notification for an action"""
        self._head_widget.addNotif(handler, action_data, id_, security_limit, profile)

    def addNote(self, title, message, level):
        self._head_widget.addNote(title, message, level)


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

    def build(self):
        return CagouRootWidget(ProfileManager())

    def expand(self, path):
        """expand path and replace known values

        useful in kv. Values which can be used:
            - {media}: media dir
        """
        return os.path.expanduser(path).format(media=self.host.media_dir)


class Cagou(QuickApp):
    MB_HANDLE = False

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

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

    def _defaultFactory(self, 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=iter(self.profiles))

    ## plugins & kv import ##

    def _import_kv(self):
        """import all kv files in cagou.kv"""
        path = os.path.dirname(cagou.kv.__file__)
        for kv_path in glob.glob(os.path.join(path, "*.kv")):
            Builder.load_file(kv_path)
            log.debug(u"kv file {} loaded".format(kv_path))

    def _import_plugins(self):
        """import all plugins"""
        self.default_wid = None
        plugins_path = os.path.dirname(cagou.plugins.__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:
            plugin_path = 'cagou.plugins.' + plug
            mod = import_module(plugin_path)
            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)
            kv_path = os.path.join(plugins_path, plugin_info['kv_file'])
            Builder.load_file(kv_path)

            # 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

            # icons
            for size in ('small', 'medium'):
                key = u'icon_{}'.format(size)
                try:
                    path = plugin_info[key]
                except KeyError:
                    path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir)
                else:
                    path = path.format(media=self.media_dir)
                    if not os.path.isfile(path):
                        path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir)
                plugin_info[key] = path

            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
        """
        for plugin_data in self._plg_wids:
            if plugin_data['main'] == except_cls:
                continue
            yield plugin_data

    ## widgets handling

    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)

    ## misc ##

    def plugging_profiles(self):
        self.app.root.change_widget(WidgetsHandler())

    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 addNote(self, title, message, level):
        """add a note (message which disappear) to root widget's header"""
        self.app.root.addNote(title, message, level)

    ## signals handling ##

    def actionNewHandler(self, action_data, id_, security_limit, profile):
        handler = super(Cagou, self).actionNewHandler
        # FIXME: temporarily deactivated
        # if 'xmlui' in action_data:
        #     self.app.root.newAction(handler, action_data, id_, security_limit, profile)
        # else:
        #     handler(action_data, id_, security_limit, profile)
        handler(action_data, id_, security_limit, profile)