changeset 126:cd99f70ea592

global file reorganisation: - follow common convention by puttin cagou in "cagou" instead of "src/cagou" - added VERSION in cagou with current version - updated dates - moved main executable in /bin - moved buildozer files in root directory - temporary moved platform to assets/platform
author Goffi <goffi@goffi.org>
date Thu, 05 Apr 2018 17:11:21 +0200
parents b6e6afb0dc46
children 9d3d700a65b7
files assets/platform/android/sat.conf bin/cagou buildozer.spec cagou/VERSION cagou/__init__.py cagou/core/__init__.py cagou/core/cagou_main.py cagou/core/cagou_widget.py cagou/core/common.py cagou/core/config.py cagou/core/constants.py cagou/core/image.py cagou/core/kivy_hack.py cagou/core/menu.py cagou/core/profile_manager.py cagou/core/simple_xhtml.py cagou/core/widgets_handler.py cagou/core/xmlui.py cagou/images/button.png cagou/images/button_selected.png cagou/kv/__init__.py cagou/kv/cagou_widget.kv cagou/kv/common.kv cagou/kv/menu.kv cagou/kv/profile_manager.kv cagou/kv/root_widget.kv cagou/kv/simple_xhtml.kv cagou/kv/widgets_handler.kv cagou/kv/xmlui.kv cagou/plugins/__init__.py cagou/plugins/plugin_transfer_android_gallery.py cagou/plugins/plugin_transfer_android_photo.py cagou/plugins/plugin_transfer_android_video.py cagou/plugins/plugin_transfer_file.kv cagou/plugins/plugin_transfer_file.py cagou/plugins/plugin_transfer_voice.kv cagou/plugins/plugin_transfer_voice.py cagou/plugins/plugin_wid_chat.kv cagou/plugins/plugin_wid_chat.py cagou/plugins/plugin_wid_contact_list.kv cagou/plugins/plugin_wid_contact_list.py cagou/plugins/plugin_wid_settings.kv cagou/plugins/plugin_wid_settings.py cagou/plugins/plugin_wid_widget_selector.kv cagou/plugins/plugin_wid_widget_selector.py service/main.py src/buildozer.spec src/cagou/__init__.py src/cagou/core/__init__.py src/cagou/core/cagou_main.py src/cagou/core/cagou_widget.py src/cagou/core/common.py src/cagou/core/config.py src/cagou/core/constants.py src/cagou/core/image.py src/cagou/core/kivy_hack.py src/cagou/core/menu.py src/cagou/core/profile_manager.py src/cagou/core/simple_xhtml.py src/cagou/core/widgets_handler.py src/cagou/core/xmlui.py src/cagou/images/button.png src/cagou/images/button_selected.png src/cagou/kv/__init__.py src/cagou/kv/cagou_widget.kv src/cagou/kv/common.kv src/cagou/kv/menu.kv src/cagou/kv/profile_manager.kv src/cagou/kv/root_widget.kv src/cagou/kv/simple_xhtml.kv src/cagou/kv/widgets_handler.kv src/cagou/kv/xmlui.kv src/cagou/plugins/__init__.py src/cagou/plugins/plugin_transfer_android_gallery.py src/cagou/plugins/plugin_transfer_android_photo.py src/cagou/plugins/plugin_transfer_android_video.py src/cagou/plugins/plugin_transfer_file.kv src/cagou/plugins/plugin_transfer_file.py src/cagou/plugins/plugin_transfer_voice.kv src/cagou/plugins/plugin_transfer_voice.py src/cagou/plugins/plugin_wid_chat.kv src/cagou/plugins/plugin_wid_chat.py src/cagou/plugins/plugin_wid_contact_list.kv src/cagou/plugins/plugin_wid_contact_list.py src/cagou/plugins/plugin_wid_settings.kv src/cagou/plugins/plugin_wid_settings.py src/cagou/plugins/plugin_wid_widget_selector.kv src/cagou/plugins/plugin_wid_widget_selector.py src/libs/garden/README src/libs/garden/garden.contextmenu/.gitignore src/libs/garden/garden.contextmenu/LICENSE src/libs/garden/garden.contextmenu/README.md src/libs/garden/garden.contextmenu/__init__.py src/libs/garden/garden.contextmenu/app_menu.kv src/libs/garden/garden.contextmenu/app_menu.py src/libs/garden/garden.contextmenu/context_menu.kv src/libs/garden/garden.contextmenu/context_menu.py src/libs/garden/garden.contextmenu/doc/app-menu-01.png src/libs/garden/garden.contextmenu/doc/context-menu-01.png src/libs/garden/garden.contextmenu/doc/menu-divider-01.png src/libs/garden/garden.contextmenu/doc/menu-divider-02.png src/libs/garden/garden.contextmenu/examples/simple_app_menu.py src/libs/garden/garden.contextmenu/examples/simple_context_menu.py src/main.py src/platform/android/sat.conf src/service/main.py
diffstat 100 files changed, 5120 insertions(+), 6139 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/assets/platform/android/sat.conf	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,8 @@
+[DEFAULT]
+bridge = pb
+log_level = debug
+log_fmt = #[%%(levelname)s][%%(name)s] %%(message)s
+
+[cagou]
+log_level = debug
+log_fmt = [%%(levelname)s][%%(name)s] %%(message)s
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/cagou	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,23 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+import cagou
+
+if __name__ == "__main__":
+    cagou.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildozer.spec	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,236 @@
+[app]
+
+# (str) Title of your application
+title = Cagou
+
+# (str) Package name
+package.name = cagou
+
+# (str) Package domain (needed for android/ios packaging)
+package.domain = org.goffi.cagou
+
+# (str) Source code where the main.py live
+source.dir = .
+
+# (list) Source files to include (let empty to include all the files)
+source.include_exts = py,png,jpg,kv,atlas,conf
+# FIXME: check if we can do sat.conf only, without every .conf
+
+# (list) List of inclusions using pattern matching
+#source.include_patterns = assets/*,images/*.png
+source.include_patterns = media
+
+# (list) Source files to exclude (let empty to not exclude anything)
+#source.exclude_exts = spec
+
+# (list) List of directory to exclude (let empty to not exclude anything)
+#source.exclude_dirs = tests, bin
+
+# (list) List of exclusions using pattern matching
+#source.exclude_patterns = license,images/*/*.jpg
+
+# (str) Application versioning (method 1)
+version = 0.1
+
+# (str) Application versioning (method 2)
+# version.regex = __version__ = ['"](.*)['"]
+# version.filename = %(source.dir)s/main.py
+
+# (list) Application requirements
+# comma seperated e.g. requirements = sqlite3,kivy
+requirements = kivy, sqlite3, twisted, wokkel, pil, lxml, pyxdg, markdown, html2text, python-dateutil, pycrypto, pyopenssl, plyer, potr
+
+# (str) Custom source folders for requirements
+# Sets custom source for any requirements with recipes
+# requirements.source.kivy = ../../kivy
+
+# (list) Garden requirements
+#garden_requirements =
+
+# (str) Presplash of the application
+presplash.filename = %(source.dir)s/media/icons/muchoslava/png/cagou_profil_bleu_512.png
+
+# (str) Icon of the application
+icon.filename = %(source.dir)s/media/icons/muchoslava/png/cagou_profil_bleu_96.png
+
+# (str) Supported orientation (one of landscape, portrait or all)
+orientation = portrait
+
+# (list) List of service to declare
+#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
+
+#
+# OSX Specific
+#
+
+#
+# author = © Copyright Info
+
+#
+# Android specific
+#
+
+# (bool) Indicate if the application should be fullscreen or not
+fullscreen = 1
+
+# (list) Permissions
+android.permissions = INTERNET, ACCESS_NETWORK_STATE, VIBRATE, RECORD_AUDIO
+
+# (int) Android API to use
+#android.api = 19
+
+# (int) Minimum API required
+#android.minapi = 9
+
+# (int) Android SDK version to use
+#android.sdk = 20
+
+# (str) Android NDK version to use
+#android.ndk = 9c
+
+# (bool) Use --private data storage (True) or --dir public storage (False)
+#android.private_storage = True
+
+# (str) Android NDK directory (if empty, it will be automatically downloaded.)
+#android.ndk_path =
+
+# (str) Android SDK directory (if empty, it will be automatically downloaded.)
+#android.sdk_path =
+
+# (str) ANT directory (if empty, it will be automatically downloaded.)
+#android.ant_path =
+
+# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
+# we use our own p4a and mount in root dir on docker image
+android.p4a_dir = /python-for-android-old
+
+# (str) The directory in which python-for-android should look for your own build recipes (if any)
+#p4a.local_recipes =
+
+# (list) python-for-android whitelist
+#android.p4a_whitelist =
+
+# (bool) If True, then skip trying to update the Android sdk
+# This can be useful to avoid excess Internet downloads or save time
+# when an update is due and you just want to test/build your package
+# android.skip_update = False
+
+# (str) Bootstrap to use for android builds (android_new only)
+# android.bootstrap = sdl2
+
+# (str) Android entry point, default is ok for Kivy-based app
+#android.entrypoint = org.renpy.android.PythonActivity
+
+# (list) List of Java .jar files to add to the libs so that pyjnius can access
+# their classes. Don't add jars that you do not need, since extra jars can slow
+# down the build process. Allows wildcards matching, for example:
+# OUYA-ODK/libs/*.jar
+#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
+
+# (list) List of Java files to add to the android project (can be java or a
+# directory containing the files)
+#android.add_src =
+
+# (str) python-for-android branch to use, if not master, useful to try
+# not yet merged features.
+#android.branch = master
+
+# (str) OUYA Console category. Should be one of GAME or APP
+# If you leave this blank, OUYA support will not be enabled
+#android.ouya.category = GAME
+
+# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
+#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
+
+# (str) XML file to include as an intent filters in <activity> tag
+#android.manifest.intent_filters =
+
+# (list) Android additionnal libraries to copy into libs/armeabi
+#android.add_libs_armeabi = libs/android/*.so
+#android.add_libs_armeabi_v7a = libs/android-v7/*.so
+#android.add_libs_x86 = libs/android-x86/*.so
+#android.add_libs_mips = libs/android-mips/*.so
+
+# (bool) Indicate whether the screen should stay on
+# Don't forget to add the WAKE_LOCK permission if you set this to True
+#android.wakelock = False
+
+# (list) Android application meta-data to set (key=value format)
+#android.meta_data =
+
+# (list) Android library project to add (will be added in the
+# project.properties automatically.)
+#android.library_references =
+
+# (str) Android logcat filters to use
+#android.logcat_filters = *:S python:D
+
+# (bool) Copy library instead of making a libpymodules.so
+#android.copy_libs = 1
+
+#
+# iOS specific
+#
+
+# (str) Path to a custom kivy-ios folder
+#ios.kivy_ios_dir = ../kivy-ios
+
+# (str) Name of the certificate to use for signing the debug version
+# Get a list of available identities: buildozer ios list_identities
+#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
+
+# (str) Name of the certificate to use for signing the release version
+#ios.codesign.release = %(ios.codesign.debug)s
+
+
+[buildozer]
+
+# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
+log_level = 1
+
+# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
+warn_on_root = 1
+
+# (str) Path to build artifact storage, absolute or relative to spec file
+# build_dir = ./.buildozer
+
+# (str) Path to build output (i.e. .apk, .ipa) storage
+# bin_dir = ./bin
+
+#    -----------------------------------------------------------------------------
+#    List as sections
+#
+#    You can define all the "list" as [section:key].
+#    Each line will be considered as a option to the list.
+#    Let's take [app] / source.exclude_patterns.
+#    Instead of doing:
+#
+#[app]
+#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
+#
+#    This can be translated into:
+#
+#[app:source.exclude_patterns]
+#license
+#data/audio/*.wav
+#data/images/original/*
+#
+
+
+#    -----------------------------------------------------------------------------
+#    Profiles
+#
+#    You can extend section / key with a profile
+#    For example, you want to deploy a demo version of your application without
+#    HD content. You could first change the title to add "(demo)" in the name
+#    and extend the excluded directories to remove the HD content.
+#
+#[app@demo]
+#title = My Application (demo)
+#
+#[app:source.exclude_patterns@demo]
+#images/hd/*
+#
+#    Then, invoke the command line with the "demo" profile:
+#
+#buildozer --profile demo android debug
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/VERSION	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,1 @@
+0.7.0D
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/__init__.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,33 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+
+class Global(object):
+    @property
+    def host(self):
+        return self._host
+G = Global()
+
+
+from core import cagou_main
+
+
+def run():
+    host = G._host = cagou_main.Cagou()
+    host.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/cagou_main.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,666 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 _
+from . import kivy_hack
+kivy_hack.do_hack()
+from constants import Const as C
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core import exceptions
+from sat_frontends.quick_frontend.quick_app import QuickApp
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_chat
+from sat_frontends.quick_frontend import quick_utils
+from sat.tools import config
+from sat.tools.common import dynamic_import
+import kivy
+kivy.require('1.9.1')
+import kivy.support
+main_config = config.parseMainConf()
+bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus')
+# FIXME: event loop is choosen according to bridge_name, a better way should be used
+if 'dbus' in bridge_name:
+    kivy.support.install_gobject_iteration()
+elif bridge_name in ('pb', 'embedded'):
+    kivy.support.install_twisted_reactor()
+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.floatlayout import FloatLayout
+from kivy.uix.screenmanager import ScreenManager, Screen, FallOutTransition, RiseInTransition
+from kivy.uix.dropdown import DropDown
+from cagou_widget import CagouWidget
+from . import widgets_handler
+from .common import IconButton
+from . import menu
+from importlib import import_module
+import os.path
+import glob
+import cagou.plugins
+import cagou.kv
+from kivy import utils as kivy_utils
+import sys
+if kivy_utils.platform == "android":
+    # FIXME: move to separate android module
+    kivy.support.install_android()
+    # sys.platform is "linux" on android by default
+    # so we change it to allow backend to detect android
+    sys.platform = "android"
+    import mmap
+    C.PLUGIN_EXT = 'pyo'
+
+
+class NotifsIcon(IconButton):
+    notifs = properties.ListProperty()
+
+    def on_release(self):
+        callback, args, kwargs = self.notifs.pop(0)
+        callback(*args, **kwargs)
+
+    def addNotif(self, callback, *args, **kwargs):
+        self.notifs.append((callback, args, kwargs))
+
+
+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()
+    notifs_icon = 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)
+
+    def addNotif(self, callback, *args, **kwargs):
+        self.notifs_icon.addNotif(callback, *args, **kwargs)
+
+    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 addNotifUI(self, ui):
+        self.notifs_icon.addNotif(ui.show, force=True)
+
+    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 RootMenus(menu.MenusWidget):
+    pass
+
+
+class RootBody(BoxLayout):
+    pass
+
+
+class CagouRootWidget(FloatLayout):
+    root_menus = properties.ObjectProperty()
+    root_body = properties.ObjectProperty
+
+    def __init__(self, main_widget):
+        super(CagouRootWidget, self).__init__()
+        # header
+        self._head_widget = RootHeadWidget()
+        self.root_body.add_widget(self._head_widget)
+        # body
+        self._manager = ScreenManager()
+        # main widgets
+        main_screen = Screen(name='main')
+        main_screen.add_widget(main_widget)
+        self._manager.add_widget(main_screen)
+        # backend XMLUI (popups, forms, etc)
+        xmlui_screen = Screen(name='xmlui')
+        self._manager.add_widget(xmlui_screen)
+        # extra (file chooser, audio record, etc)
+        extra_screen = Screen(name='extra')
+        self._manager.add_widget(extra_screen)
+        self.root_body.add_widget(self._manager)
+
+    def changeWidget(self, widget, screen_name="main"):
+        """change main widget"""
+        if self._manager.transition.is_active:
+            # FIXME: workaround for what seems a Kivy bug
+            # TODO: report this upstream
+            self._manager.transition.stop()
+        screen = self._manager.get_screen(screen_name)
+        screen.clear_widgets()
+        screen.add_widget(widget)
+
+    def show(self, screen="main"):
+        if self._manager.transition.is_active:
+            # FIXME: workaround for what seems a Kivy bug
+            # TODO: report this upstream
+            self._manager.transition.stop()
+        if self._manager.current == screen:
+            return
+        if screen == "main":
+            self._manager.transition = FallOutTransition()
+        else:
+            self._manager.transition = RiseInTransition()
+        self._manager.current = screen
+
+    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)
+
+    def addNotifUI(self, ui):
+        self._head_widget.addNotifUI(ui)
+
+
+class CagouApp(App):
+    """Kivy App for Cagou"""
+
+    def build(self):
+        return CagouRootWidget(Label(text=u"Loading please wait"))
+
+    def showWidget(self):
+        self._profile_manager = ProfileManager()
+        self.root.changeWidget(self._profile_manager)
+
+    def expand(self, path, *args, **kwargs):
+        """expand path and replace known values
+
+        useful in kv. Values which can be used:
+            - {media}: media dir
+        @param path(unicode): path to expand
+        @param *args: additional arguments used in format
+        @param **kwargs: additional keyword arguments used in format
+        """
+        return os.path.expanduser(path).format(*args, media=self.host.media_dir, **kwargs)
+
+    def on_start(self):
+        if sys.platform == "android":
+            # XXX: we use memory map instead of bridge because if we try to call a bridge method
+            #      in on_pause method, the call data  is not written before the actual pause
+            # we create a memory map on .cagou_status file with a 1 byte status
+            # status is:
+            # R => running
+            # P => paused
+            # S => stopped
+            self._first_pause = True
+            self.cagou_status_fd = open('.cagou_status', 'wb+')
+            self.cagou_status_fd.write('R')
+            self.cagou_status_fd.flush()
+            self.cagou_status = mmap.mmap(self.cagou_status_fd.fileno(), 1, prot=mmap.PROT_WRITE)
+
+    def on_pause(self):
+        self.cagou_status[0] = 'P'
+        return True
+
+    def on_resume(self):
+        self.cagou_status[0] = 'R'
+
+    def on_stop(self):
+        if sys.platform == "android":
+            self.cagou_status[0] = 'S'
+            self.cagou_status.flush()
+            self.cagou_status_fd.close()
+
+
+class Cagou(QuickApp):
+    MB_HANDLE = False
+
+    def __init__(self):
+        if bridge_name == 'embedded':
+            from sat.core import sat_main
+            self.sat = sat_main.SAT()
+        if sys.platform == 'android':
+            from android import AndroidService
+            service = AndroidService(u'Cagou (SàT)'.encode('utf-8'), u'Salut à Toi backend'.encode('utf-8'))
+            service.start(u'service started')
+            self.service = service
+
+        bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
+        if bridge_module is None:
+            log.error(u"Can't import {} bridge".format(bridge_name))
+            sys.exit(3)
+        else:
+            log.debug(u"Loading {} bridge".format(bridge_name))
+        super(Cagou, self).__init__(bridge_factory=bridge_module.Bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False)
+        self._import_kv()
+        self.app = CagouApp()
+        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.app.icon = os.path.join(self.media_dir, "icons/muchoslava/png/cagou_profil_bleu_96.png")
+        self._plg_wids = []  # main widgets plugins
+        self._plg_wids_transfer = []  # transfer widgets plugins
+        self._import_plugins()
+        self._visible_widgets = {}  # visible widgets by classes
+
+    @property
+    def visible_widgets(self):
+        for w_list in self._visible_widgets.itervalues():
+            for w in w_list:
+                yield w
+
+    def onBridgeConnected(self):
+        self.registerSignal("otrState", iface="plugin")
+        self.bridge.getReady(self.onBackendReady)
+
+    def _bridgeEb(self, failure):
+        if bridge_name == "pb" and sys.platform == "android":
+            try:
+                self.retried += 1
+            except AttributeError:
+                self.retried = 1
+            from twisted.internet.error import ConnectionRefusedError
+            if failure.check(ConnectionRefusedError) and self.retried < 100:
+                if self.retried % 20 == 0:
+                    log.debug("backend not ready, retrying ({})".format(self.retried))
+                Clock.schedule_once(lambda dummy: self.connectBridge(), 0.05)
+                return
+        super(Cagou, self)._bridgeEb(failure)
+
+    def run(self):
+        self.connectBridge()
+        self.app.bind(on_stop=self.onStop)
+        self.app.run()
+
+    def onStop(self, obj):
+        try:
+            sat_instance = self.sat
+        except AttributeError:
+            pass
+        else:
+            sat_instance.stopService()
+
+    def onBackendReady(self):
+        self.app.showWidget()
+        self.postInit()
+
+    def postInit(self, dummy=None):
+        # FIXME: resize seem to bug on android, so we use below_target for now
+        self.app.root_window.softinput_mode = "below_target"
+        profile_manager = self.app._profile_manager
+        del self.app._profile_manager
+        super(Cagou, self).postInit(profile_manager)
+
+    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 _defaultFactoryTransfer(self, plugin_info, callback, cancel_cb, profiles):
+        """default factory used to create transfer widgets instances
+
+        @param plugin_info(dict): plugin datas
+        @param callback(callable): method to call with path to file to transfer
+        @param cancel_cb(callable): call when transfer is cancelled
+            transfer 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):
+        """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__)
+        plugin_glob = u"plugin*." + C.PLUGIN_EXT
+        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_main = set()  # used to avoid loading 2 times plugin with same import name
+        imported_names_transfer = 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_TRANSFER:
+                imported_names = imported_names_transfer
+                default_factory = self._defaultFactoryTransfer
+            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 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:
+                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:
+                plugin_info['kv_file'] = u'{}.kv'.format(plug)
+            kv_path = os.path.join(plugins_path, plugin_info['kv_file'])
+            if not os.path.exists(kv_path):
+                log.debug(u"no kv found for {plugin_file}".format(**plugin_info))
+            else:
+                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'] = default_factory
+
+            # 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
+
+            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_transfer.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 _getPluginsSet(self, type_):
+        if type_ == C.PLUG_TYPE_WID:
+            return self._plg_wids
+        elif type_ == C.PLUG_TYPE_TRANSFER:
+            return self._plg_wids_transfer
+        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
+        """
+        plugins_set = self._getPluginsSet(type_)
+        for plugin_data in plugins_set:
+            if plugin_data['main'] == except_cls:
+                continue
+            yield plugin_data
+
+    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
+        """
+        plugins_set = self._getPluginsSet(type_)
+        for plugin_info in plugins_set:
+            for k, w in kwargs.iteritems():
+                try:
+                    if plugin_info[k] != w:
+                        continue
+                except KeyError:
+                    continue
+                return plugin_info
+
+    ## widgets handling
+
+    def newWidget(self, widget):
+        log.debug(u"new widget created: {}".format(widget))
+        if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
+            self.addNote(u"", _(u"room {} has been joined").format(widget.target))
+
+    def getParentHandler(self, widget):
+        """Return handler holding this widget
+
+        @return (WidgetsHandler): handler
+        """
+        w_handler = widget.parent
+        while w_handler and not(isinstance(w_handler, widgets_handler.WidgetsHandler)):
+            w_handler = w_handler.parent
+        return w_handler
+
+    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:
+            raise exceptions.InternalError(u"no CagouWidget found when trying to switch widget")
+        handler = self.getParentHandler(to_change)
+        handler.changeWidget(new)
+
+    def addVisibleWidget(self, widget):
+        """declare a widget visible
+
+        for internal use only!
+        """
+        assert isinstance(widget, quick_widgets.QuickWidget)
+        self._visible_widgets.setdefault(widget.__class__, []).append(widget)
+
+    def removeVisibleWidget(self, widget):
+        """declare a widget not visible anymore
+
+        for internal use only!
+        """
+        self._visible_widgets[widget.__class__].remove(widget)
+        self.widgets.deleteWidget(widget)
+
+    def getVisibleList(self, cls):
+        """get list of visible widgets for a given class
+
+        @param cls(QuickWidget class): type of widgets to get
+        @return (list[QuickWidget class]): visible widgets of this class
+        """
+        try:
+            return self._visible_widgets[cls]
+        except KeyError:
+            return []
+
+    def getOrClone(self, widget):
+        """Get a QuickWidget if it has not parent set else clone it"""
+        if widget.parent is None:
+            return widget
+        targets = list(widget.targets)
+        w = self.widgets.getOrCreateWidget(widget.__class__, targets[0], on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=widget.profiles)
+        for t in targets[1:]:
+            w.addTarget(t)
+        return w
+
+    ## menus ##
+
+    def _menusGetCb(self, backend_menus):
+        main_menu = self.app.root.root_menus
+        self.menus.addMenus(backend_menus)
+        self.menus.addMenu(C.MENU_GLOBAL, (_(u"Help"), _(u"About")), callback=main_menu.onAbout)
+        main_menu.update(C.MENU_GLOBAL)
+
+    ## bridge handlers ##
+
+    def otrStateHandler(self, state, dest_jid, profile):
+        """OTR state has changed for on destinee"""
+        # XXX: this method could be in QuickApp but it's here as
+        #      it's only used by Cagou so far
+        for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)):
+            widget.onOTRState(state, dest_jid, profile)
+
+    ## misc ##
+
+    def plugging_profiles(self):
+        self.app.root.changeWidget(WidgetsHandler())
+        self.bridge.menusGet("", C.NO_SECURITY_LIMIT, callback=self._menusGetCb)
+
+    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=C.XMLUI_DATA_LVL_INFO):
+        """add a note (message which disappear) to root widget's header"""
+        self.app.root.addNote(title, message, level)
+
+    def addNotifUI(self, ui):
+        """add a notification with a XMLUI attached
+
+        @param ui(xmlui.XMLUIPanel): XMLUI instance to show when notification is selected
+        """
+        self.app.root.addNotifUI(ui)
+
+    def showUI(self, ui):
+        """show a XMLUI"""
+        self.app.root.changeWidget(ui, "xmlui")
+        self.app.root.show("xmlui")
+
+    def showExtraUI(self, widget):
+        """show any extra widget"""
+        self.app.root.changeWidget(widget, "extra")
+        self.app.root.show("extra")
+
+    def closeUI(self):
+        self.app.root.show()
+
+    def getDefaultAvatar(self, entity=None):
+        return self.app.default_avatar
+
+    def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None):
+        # TODO
+        log.info(u"FIXME: showDialog not implemented")
+        log.info(u"message: {} -- {}".format(title, message))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/cagou_widget.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,82 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from kivy.uix.image import Image
+from kivy.uix.behaviors import ButtonBehavior
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.dropdown import DropDown
+from kivy import properties
+from cagou import G
+
+
+class HeaderWidgetChoice(ButtonBehavior, BoxLayout):
+    def __init__(self, cagou_widget, plugin_info):
+        self.plugin_info = plugin_info
+        super(HeaderWidgetChoice, self).__init__()
+        self.bind(on_release=lambda btn: cagou_widget.switchWidget(plugin_info))
+
+
+class HeaderWidgetCurrent(ButtonBehavior, Image):
+    pass
+
+
+class HeaderWidgetSelector(DropDown):
+
+    def __init__(self, cagou_widget):
+        super(HeaderWidgetSelector, self).__init__()
+        for plugin_info in G.host.getPluggedWidgets(except_cls=cagou_widget.__class__):
+            choice = HeaderWidgetChoice(cagou_widget, plugin_info)
+            self.add_widget(choice)
+
+
+class CagouWidget(BoxLayout):
+    header_input = properties.ObjectProperty(None)
+    header_box = properties.ObjectProperty(None)
+
+    def __init__(self):
+        for p in G.host.getPluggedWidgets():
+            if p['main'] == self.__class__:
+                self.plugin_info = p
+                break
+        BoxLayout.__init__(self, orientation="vertical")
+        self.selector = HeaderWidgetSelector(self)
+
+    def switchWidget(self, plugin_info):
+        self.selector.dismiss()
+        factory = plugin_info["factory"]
+        new_widget = factory(plugin_info, None, iter(G.host.profiles))
+        G.host.switchWidget(self, new_widget)
+
+    def onHeaderInput(self):
+        log.info(u"header input text entered")
+
+    def onHeaderInputComplete(self, wid, text):
+        return
+
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            G.host.selected_widget = self
+        super(CagouWidget, self).on_touch_down(touch)
+
+    def headerInputAddExtra(self, widget):
+        """add a widget on the right of header input"""
+        self.header_box.add_widget(widget)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/common.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,46 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+"""common widgets, which can be reused everywhere"""
+
+from kivy.uix.image import Image
+from kivy.uix.behaviors import ButtonBehavior
+from kivy.uix.boxlayout import BoxLayout
+from cagou import G
+
+
+class IconButton(ButtonBehavior, Image):
+    pass
+
+
+class JidWidget(ButtonBehavior, BoxLayout):
+
+    def __init__(self, jid, profile, **kwargs):
+        self.jid = jid
+        self.profile = profile
+        self.nick = kwargs.get('nick')
+        super(JidWidget, self).__init__(**kwargs)
+
+    def getImage(self, wid):
+        host = G.host
+        if host.contact_lists[self.profile].isRoom(self.jid.bare):
+            wid.opacity = 0
+            return ""
+        else:
+            return host.getAvatar(self.jid, profile=self.profile) or host.getDefaultAvatar(self.jid)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/config.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,27 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+"""This module keep an open instance of sat configuration"""
+
+from sat.tools import config
+sat_conf = config.parseMainConf()
+
+
+def getConfig(section, name, default):
+    return config.getConfig(sat_conf, section, name, default)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/constants.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Primitivus: a SAT frontend
+# Copyright (C) 2009-2016-2018 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_frontends.quick_frontend import constants
+
+
+class Const(constants.Const):
+    APP_NAME = u"Cagou"
+    LOG_OPT_SECTION = APP_NAME.lower()
+    CONFIG_SECTION = APP_NAME.lower()
+    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_TRANSFER = u'transfer'
+
+    TRANSFER_UPLOAD = u"upload"
+    TRANSFER_SEND = u"send"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/image.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from kivy.uix import image as kivy_img
+from kivy.core.image import Image as CoreImage
+from kivy.resources import resource_find
+import io
+import PIL
+
+
+class Image(kivy_img.Image):
+    """Image widget which accept source without extension"""
+
+    def texture_update(self, *largs):
+        if not self.source:
+            self.texture = None
+        else:
+            filename = resource_find(self.source)
+            self._loops = 0
+            if filename is None:
+                return log.error('Image: Error reading file {filename}'.
+                                    format(filename=self.source))
+            mipmap = self.mipmap
+            if self._coreimage is not None:
+                self._coreimage.unbind(on_texture=self._on_tex_change)
+            try:
+                self._coreimage = ci = CoreImage(filename, mipmap=mipmap,
+                                                 anim_delay=self.anim_delay,
+                                                 keep_data=self.keep_data,
+                                                 nocache=self.nocache)
+            except Exception as e:
+                # loading failed probably because of unmanaged extention,
+                # we try our luck with with PIL
+                try:
+                    im = PIL.Image.open(filename)
+                    ext = im.format.lower()
+                    del im
+                    # we can't use im.tobytes as it would use the
+                    # internal decompressed representation from pillow
+                    # and im.save would need processing to handle format
+                    data = io.BytesIO(open(filename, "rb").read())
+                    cache_filename = u"{}.{}".format(filename,ext) # needed for kivy's Image to use cache
+                    self._coreimage = ci = CoreImage(data, ext=ext,
+                                                     filename=cache_filename, mipmap=mipmap,
+                                                     anim_delay=self.anim_delay,
+                                                     keep_data=self.keep_data,
+                                                     nocache=self.nocache)
+                except Exception as e:
+                    log.warning(u"Can't load image: {}".format(e))
+                    self._coreimage = ci = None
+
+            if ci:
+                ci.bind(on_texture=self._on_tex_change)
+                self.texture = ci.texture
+
+
+class AsyncImage(kivy_img.AsyncImage):
+    """AsyncImage which accept file:// schema"""
+
+    def _load_source(self, *args):
+        if self.source.startswith('file://'):
+            self.source = self.source[7:]
+        else:
+            super(AsyncImage, self)._load_source(*args)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/kivy_hack.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,70 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+CONF_KIVY_LEVEL = 'log_kivy_level'
+
+
+def do_hack():
+    """work around Kivy hijacking of logs and arguments"""
+    # we remove args so kivy doesn't use them
+    # this is need to avoid kivy breaking QuickApp args handling
+    import sys
+    ori_argv = sys.argv[:]
+    sys.argv = sys.argv[:1]
+    from constants import Const as C
+    from sat.core import log_config
+    log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
+
+    import config
+    kivy_level = config.getConfig(C.CONFIG_SECTION, CONF_KIVY_LEVEL, 'follow').upper()
+
+    # kivy handles its own loggers, we don't want that!
+    import logging
+    root_logger = logging.root
+    kivy_logger = logging.getLogger('kivy')
+    ori_addHandler = kivy_logger.addHandler
+    kivy_logger.addHandler = lambda dummy: None
+    ori_setLevel = kivy_logger.setLevel
+    if kivy_level == 'FOLLOW':
+        # level is following SàT level
+        kivy_logger.setLevel = lambda level: None
+    elif kivy_level == 'KIVY':
+        # level will be set by Kivy according to its own conf
+        pass
+    elif kivy_level in C.LOG_LEVELS:
+        kivy_logger.setLevel(kivy_level)
+        kivy_logger.setLevel = lambda level: None
+    else:
+        raise ValueError(u"Unknown value for {name}: {value}".format(name=CONF_KIVY_LEVEL, value=kivy_level))
+
+    # during import kivy set its logging stuff
+    import kivy
+    kivy # to avoid pyflakes warning
+
+    # we want to separate kivy logs from other logs
+    logging.root = root_logger
+    from kivy import logger
+    sys.stderr = logger.previous_stderr
+
+    # we restore original methods
+    kivy_logger.addHandler = ori_addHandler
+    kivy_logger.setLevel = ori_setLevel
+
+    # we restore original arguments
+    sys.argv = ori_argv
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/menu.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,239 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 _
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from cagou.core.constants import Const as C
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.label import Label
+from kivy.uix.popup import Popup
+from kivy import properties
+from kivy.garden import contextmenu
+from sat_frontends.quick_frontend import quick_menus
+from cagou import G
+import webbrowser
+
+ABOUT_TITLE = _(u"About {}".format(C.APP_NAME))
+ABOUT_CONTENT = _(u"""Cagou (Salut à Toi) v{}
+
+Cagou is a libre communication tool based on libre standard XMPP.
+
+Cagou is part of the "Salut à Toi" project
+more informations at [color=5500ff][ref=website]salut-a-toi.org[/ref][/color]
+""").format(C.APP_VERSION)
+
+
+class AboutContent(Label):
+
+    def on_ref_press(self, value):
+        if value == "website":
+            webbrowser.open("https://salut-a-toi.org")
+
+
+class AboutPopup(Popup):
+
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            self.dismiss()
+        return super(AboutPopup, self).on_touch_down(touch)
+
+
+class MainMenu(contextmenu.AppMenu):
+    pass
+
+
+class MenuItem(contextmenu.ContextMenuTextItem):
+    item = properties.ObjectProperty()
+
+    def on_item(self, instance, item):
+        self.text = item.name
+
+    def on_release(self):
+        super(MenuItem, self).on_release()
+        self.parent.hide()
+        selected = G.host.selected_widget
+        profile = None
+        if selected is not None:
+            try:
+                profile = selected.profile
+            except AttributeError:
+                pass
+
+        if profile is None:
+            try:
+                profile = list(selected.profiles)[0]
+            except (AttributeError, IndexError):
+                try:
+                    profile = list(G.host.profiles)[0]
+                except IndexError:
+                    log.warning(u"Can't find profile")
+        self.item.call(selected, profile)
+
+
+class MenuSeparator(contextmenu.ContextMenuDivider):
+    pass
+
+
+class RootMenuContainer(contextmenu.AppMenuTextItem):
+    pass
+
+
+class MenuContainer(contextmenu.ContextMenuTextItem):
+    pass
+
+
+class MenusWidget(BoxLayout):
+
+    def update(self, type_, caller=None):
+        """Method to call when menus have changed
+
+        @param type_(unicode): menu type like in sat.core.sat_main.importMenu
+        @param caller(Widget): instance linked to the menus
+        """
+        self.menus_container = G.host.menus.getMainContainer(type_)
+        self.createMenus(caller)
+
+    def _buildMenus(self, container, caller=None):
+        """Recursively build menus of the container
+
+        @param container(quick_menus.MenuContainer): menu container
+        @param caller(Widget): instance linked to the menus
+        """
+        if caller is None:
+            main_menu = MainMenu()
+            self.add_widget(main_menu)
+            caller = main_menu
+        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
+            # TODO: fix this upstream
+            context_menu._on_visible(False)
+
+            caller = context_menu
+
+        for child in container.getActiveMenus():
+            if isinstance(child, quick_menus.MenuContainer):
+                if isinstance(caller, MainMenu):
+                    menu_container = RootMenuContainer()
+                else:
+                    menu_container = MenuContainer()
+                menu_container.text = child.name
+                caller.add_widget(menu_container)
+                self._buildMenus(child, caller=menu_container)
+            elif isinstance(child, quick_menus.MenuSeparator):
+                wid = MenuSeparator()
+                caller.add_widget(wid)
+            elif isinstance(child, quick_menus.MenuItem):
+                wid = MenuItem(item=child)
+                caller.add_widget(wid)
+            else:
+                log.error(u"Unknown child type: {}".format(child))
+
+    def createMenus(self, caller):
+        self.clear_widgets()
+        self._buildMenus(self.menus_container, caller)
+
+    def onAbout(self):
+        about = AboutPopup()
+        about.title = ABOUT_TITLE
+        about.content = AboutContent(text=ABOUT_CONTENT, markup=True)
+        about.open()
+
+
+class TransferItem(BoxLayout):
+    plug_info = properties.DictProperty()
+
+    def on_touch_up(self, touch):
+        if not self.collide_point(*touch.pos):
+            return super(TransferItem, self).on_touch_up(touch)
+        else:
+            transfer_menu = self.parent
+            while not isinstance(transfer_menu, TransferMenu):
+                transfer_menu = transfer_menu.parent
+            transfer_menu.do_callback(self.plug_info)
+            return True
+
+
+class TransferMenu(BoxLayout):
+    """transfer menu which handle display and callbacks"""
+    # callback will be called with path to file to transfer
+    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 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")
+    items_layout = properties.ObjectProperty()
+
+    def __init__(self, **kwargs):
+        super(TransferMenu, self).__init__(**kwargs)
+        if self.cancel_cb is None:
+            self.cancel_cb = self.onTransferCancelled
+        if self.profiles is None:
+            self.profiles = iter(G.host.profiles)
+        for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_TRANSFER):
+            item = TransferItem(
+                plug_info = plug_info
+                )
+            self.items_layout.add_widget(item)
+
+    def show(self, caller_wid=None):
+        self.visible = True
+        G.host.app.root.add_widget(self)
+
+    def on_touch_down(self, touch):
+        # we remove the menu if we click outside
+        # else we want to handle the event, but not
+        # transmit it to parents
+        if not self.collide_point(*touch.pos):
+            self.parent.remove_widget(self)
+        else:
+            return super(TransferMenu, self).on_touch_down(touch)
+        return True
+
+    def _closeUI(self, wid):
+        G.host.closeUI()
+
+    def onTransferCancelled(self, wid, cleaning_cb=None):
+        self._closeUI(wid)
+        if cleaning_cb is not None:
+            cleaning_cb()
+
+    def do_callback(self, plug_info):
+        self.parent.remove_widget(self)
+        if self.callback is None:
+            log.warning(u"TransferMenu callback is not set")
+        else:
+            wid = None
+            external = plug_info.get('external', False)
+            def onTransferCb(file_path, cleaning_cb=None):
+                if not external:
+                    self._closeUI(wid)
+                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)
+            if not external:
+                G.host.showExtraUI(wid)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/profile_manager.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,179 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from .constants import Const as C
+from sat_frontends.quick_frontend.quick_profile_manager import QuickProfileManager
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix import listview
+from kivy.uix.button import Button
+from kivy.uix.screenmanager import ScreenManager, Screen
+from kivy.adapters import listadapter
+from kivy.metrics import sp
+from kivy import properties
+from cagou import G
+
+
+class ProfileItem(listview.ListItemButton):
+    pass
+
+
+class ProfileListAdapter(listadapter.ListAdapter):
+
+    def __init__(self, pm, *args, **kwargs):
+        super(ProfileListAdapter, self).__init__(*args, **kwargs)
+        self.pm = pm
+
+    def closeUI(self, xmlui):
+        self.pm.screen_manager.transition.direction = 'right'
+        self.pm.screen_manager.current = 'profiles'
+
+    def showUI(self, xmlui):
+        xmlui.setCloseCb(self.closeUI)
+        if xmlui.type == 'popup':
+            xmlui.bind(on_touch_up=lambda obj, value: self.closeUI(xmlui))
+        self.pm.xmlui_screen.clear_widgets()
+        self.pm.xmlui_screen.add_widget(xmlui)
+        self.pm.screen_manager.transition.direction = 'left'
+        self.pm.screen_manager.current = 'xmlui'
+
+    def select_item_view(self, view):
+        def authenticate_cb(data, cb_id, profile):
+            if C.bool(data.pop('validated', C.BOOL_FALSE)):
+                super(ProfileListAdapter, self).select_item_view(view)
+            G.host.actionManager(data, callback=authenticate_cb, ui_show_cb=self.showUI, profile=profile)
+
+        G.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=view.text)
+
+
+class ConnectButton(Button):
+
+    def __init__(self, profile_screen):
+        self.profile_screen = profile_screen
+        self.pm = profile_screen.pm
+        super(ConnectButton, self).__init__()
+
+
+class NewProfileScreen(Screen):
+    profile_name = properties.ObjectProperty(None)
+    jid = properties.ObjectProperty(None)
+    password = properties.ObjectProperty(None)
+    error_msg = properties.StringProperty('')
+
+    def __init__(self, pm):
+        super(NewProfileScreen, self).__init__(name=u'new_profile')
+        self.pm = pm
+
+    def onCreationFailure(self, failure):
+        msg = [l for l in unicode(failure).split('\n') if l][-1]
+        self.error_msg = unicode(msg)
+
+    def onCreationSuccess(self, profile):
+        self.pm.profiles_screen.reload()
+        G.host.bridge.profileStartSession(self.password.text, profile, callback=lambda dummy: self._sessionStarted(profile), errback=self.onCreationFailure)
+
+    def _sessionStarted(self, profile):
+        jid = self.jid.text.strip()
+        G.host.bridge.setParam("JabberID", jid, "Connection", -1, profile)
+        G.host.bridge.setParam("Password", self.password.text, "Connection", -1, profile)
+        self.pm.screen_manager.transition.direction = 'right'
+        self.pm.screen_manager.current = 'profiles'
+
+    def doCreate(self):
+        name = self.profile_name.text.strip()
+        # XXX: we use XMPP password for profile password to simplify
+        #      if user want to change profile password, he can do it in preferences
+        G.host.bridge.asyncCreateProfile(name, self.password.text, callback=lambda: self.onCreationSuccess(name), errback=self.onCreationFailure)
+
+
+class DeleteProfilesScreen(Screen):
+
+    def __init__(self, pm):
+        self.pm = pm
+        super(DeleteProfilesScreen, self).__init__(name=u'delete_profiles')
+
+    def doDelete(self):
+        """This method will delete *ALL* selected profiles"""
+        to_delete = self.pm.getProfiles()
+        deleted = [0]
+
+        def deleteInc():
+            deleted[0] += 1
+            if deleted[0] == len(to_delete):
+                self.pm.profiles_screen.reload()
+                self.pm.screen_manager.transition.direction = 'right'
+                self.pm.screen_manager.current = 'profiles'
+
+        for profile in to_delete:
+            log.info(u"Deleteing profile [{}]".format(profile))
+            G.host.bridge.asyncDeleteProfile(profile, callback=deleteInc, errback=deleteInc)
+
+
+class ProfilesScreen(Screen):
+    layout = properties.ObjectProperty(None)
+
+    def __init__(self, pm):
+        self.pm = pm
+        self.list_adapter = ProfileListAdapter(pm,
+                                               data=[],
+                                               cls=ProfileItem,
+                                               args_converter=self.converter,
+                                               selection_mode='multiple',
+                                               allow_empty_selection=True,
+                                              )
+        super(ProfilesScreen, self).__init__(name=u'profiles')
+        self.layout.add_widget(listview.ListView(adapter=self.list_adapter))
+        connect_btn = ConnectButton(self)
+        self.layout.add_widget(connect_btn)
+        self.reload()
+
+    def _profilesListGetCb(self, profiles):
+        profiles.sort()
+        self.list_adapter.data = profiles
+
+    def converter(self, row_idx, obj):
+        return {'text': obj,
+                'size_hint_y': None,
+                'height': sp(40)}
+
+    def reload(self):
+        """Reload profiles list"""
+        G.host.bridge.profilesListGet(callback=self._profilesListGetCb)
+
+
+class ProfileManager(QuickProfileManager, BoxLayout):
+
+    def __init__(self, autoconnect=None):
+        QuickProfileManager.__init__(self, G.host, autoconnect)
+        BoxLayout.__init__(self, orientation="vertical")
+        self.screen_manager = ScreenManager()
+        self.profiles_screen = ProfilesScreen(self)
+        self.new_profile_screen = NewProfileScreen(self)
+        self.delete_profiles_screen = DeleteProfilesScreen(self)
+        self.xmlui_screen = Screen(name=u'xmlui')
+        self.screen_manager.add_widget(self.profiles_screen)
+        self.screen_manager.add_widget(self.xmlui_screen)
+        self.screen_manager.add_widget(self.new_profile_screen)
+        self.screen_manager.add_widget(self.delete_profiles_screen)
+        self.add_widget(self.screen_manager)
+
+    def getProfiles(self):
+        return [pi.text for pi in self.profiles_screen.list_adapter.selection]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/simple_xhtml.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,498 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from kivy.uix.stacklayout import StackLayout
+from kivy.uix.label import Label
+from kivy.utils import escape_markup
+from kivy import properties
+from xml.etree import ElementTree as ET
+from sat_frontends.tools import css_color, strings as sat_strings
+from cagou.core.image import AsyncImage
+import webbrowser
+
+
+class Escape(unicode):
+    """Class used to mark that a message need to be escaped"""
+
+    def __init__(self, text):
+        super(Escape, self).__init__(text)
+
+
+class SimpleXHTMLWidgetEscapedText(Label):
+
+    def _addUrlMarkup(self, text):
+        text_elts = []
+        idx = 0
+        links = 0
+        while True:
+            m = sat_strings.RE_URL.search(text[idx:])
+            if m is not None:
+                text_elts.append(escape_markup(m.string[0:m.start()]))
+                link_key = u'link_' + unicode(links)
+                url = m.group()
+                text_elts.append(u'[color=5500ff][ref={link}]{url}[/ref][/color]'.format(
+                    link = link_key,
+                    url = url
+                    ))
+                if not links:
+                    self.ref_urls = {link_key: url}
+                else:
+                    self.ref_urls[link_key] = url
+                links += 1
+                idx += m.end()
+            else:
+                if links:
+                    text_elts.append(escape_markup(text[idx:]))
+                    self.markup = True
+                    self.text = u''.join(text_elts)
+                break
+
+    def on_text(self, instance, text):
+        # do NOT call the method if self.markup is set
+        # this would result in infinite loop (because self.text
+        # is changed if an URL is found, and in this case markup too)
+        if text and not self.markup:
+            self._addUrlMarkup(text)
+
+    def on_ref_press(self, ref):
+        url = self.ref_urls[ref]
+        webbrowser.open(url)
+
+
+class SimpleXHTMLWidgetText(Label):
+    pass
+
+
+class SimpleXHTMLWidgetImage(AsyncImage):
+    # following properties are desired height/width
+    # i.e. the ones specified in height/width attributes of <img>
+    # (or wanted for whatever reason)
+    # set to 0 to ignore them
+    target_height = properties.NumericProperty()
+    target_width = properties.NumericProperty()
+
+    def _get_parent_container(self):
+        """get parent SimpleXHTMLWidget instance
+
+        @param warning(bool): if True display a log.error if nothing found
+        @return (SimpleXHTMLWidget, None): found SimpleXHTMLWidget instance
+        """
+        parent = self.parent
+        while parent and not isinstance(parent, SimpleXHTMLWidget):
+            parent = parent.parent
+        if parent is None:
+            log.error(u"no SimpleXHTMLWidget parent found")
+        return parent
+
+    def _on_source_load(self, value):
+        # this method is called when image is loaded
+        super(SimpleXHTMLWidgetImage, self)._on_source_load(value)
+        if self.parent is not None:
+            container = self._get_parent_container()
+            # image is loaded, we need to recalculate size
+            self.on_container_width(container, container.width)
+
+    def on_container_width(self, container, container_width):
+        """adapt size according to container width
+
+        called when parent container (SimpleXHTMLWidget) width change
+        """
+        target_size = (self.target_width or self.texture.width, self.target_height or self.texture.height)
+        padding = container.padding
+        padding_h = (padding[0] + padding[2]) if len(padding) == 4 else padding[0]
+        width = container_width - padding_h
+        if target_size[0] < width:
+            self.size = target_size
+        else:
+            height = width / self.image_ratio
+            self.size = (width, height)
+
+    def on_parent(self, instance, parent):
+        if parent is not None:
+            container = self._get_parent_container()
+            container.bind(width=self.on_container_width)
+
+
+class SimpleXHTMLWidget(StackLayout):
+    """widget handling simple XHTML parsing"""
+    xhtml = properties.StringProperty()
+    color = properties.ListProperty([1, 1, 1, 1])
+    # XXX: bold is only used for escaped text
+    bold = properties.BooleanProperty(False)
+    content_width = properties.NumericProperty(0)
+
+    # text/XHTML input
+
+    def on_xhtml(self, instance, xhtml):
+        """parse xhtml and set content accordingly
+
+        if xhtml is an instance of Escape, a Label with not markup
+        will be used
+        """
+        self.clear_widgets()
+        if isinstance(xhtml, Escape):
+            label = SimpleXHTMLWidgetEscapedText(text=xhtml, color=self.color)
+            self.bind(color=label.setter('color'))
+            self.bind(bold=label.setter('bold'))
+            self.add_widget(label)
+        else:
+            xhtml = ET.fromstring(xhtml.encode('utf-8'))
+            self.current_wid = None
+            self.styles = []
+            self._callParseMethod(xhtml)
+
+    def escape(self, text):
+        """mark that a text need to be escaped (i.e. no markup)"""
+        return Escape(text)
+
+    # sizing
+
+    def on_width(self, instance, width):
+        if len(self.children) == 1:
+            wid = self.children[0]
+            if isinstance(wid, Label):
+                # we have simple text
+                try:
+                    full_width = wid._full_width
+                except AttributeError:
+                    # on first time, we need the required size
+                    # for the full text, without width limit
+                    wid.size_hint = (None, None)
+                    wid.texture_update()
+                    full_width = wid._full_width = wid.texture_size[0]
+
+                if full_width > width:
+                    wid.text_size = width, None
+                    wid.width = width
+                else:
+                    wid.text_size = None, None
+                    wid.texture_update()
+                    wid.width = wid.texture_size[0]
+                self.content_width = wid.width + self.padding[0] + self.padding[2]
+            else:
+                wid.size_hint = (1, None)
+                wid.height = 100
+                self.content_width = self.width
+        else:
+            self._do_complexe_sizing(width)
+
+    def _do_complexe_sizing(self, width):
+        try:
+            self.splitted
+        except AttributeError:
+            # XXX: to make things easier, we split labels in words
+            log.debug(u"split start")
+            children = self.children[::-1]
+            self.clear_widgets()
+            for child in children:
+                if isinstance(child, Label):
+                    log.debug(u"label before split: {}".format(child.text))
+                    styles = []
+                    tag = False
+                    new_text = []
+                    current_tag = []
+                    current_value = []
+                    current_wid = self._createText()
+                    value = False
+                    close = False
+                    # we will parse the text and create a new widget
+                    # on each new word (actually each space)
+                    # FIXME: handle '\n' and other white chars
+                    for c in child.text:
+                        if tag:
+                            # we are parsing a markup tag
+                            if c == u']':
+                                current_tag_s = u''.join(current_tag)
+                                current_style = (current_tag_s, u''.join(current_value))
+                                if close:
+                                    for idx, s in enumerate(reversed(styles)):
+                                        if s[0] == current_tag_s:
+                                            del styles[len(styles) - idx - 1]
+                                            break
+                                else:
+                                    styles.append(current_style)
+                                current_tag = []
+                                current_value = []
+                                tag = False
+                                value = False
+                                close = False
+                            elif c == u'/':
+                                close = True
+                            elif c == u'=':
+                                value = True
+                            elif value:
+                                current_value.append(c)
+                            else:
+                                current_tag.append(c)
+                            new_text.append(c)
+                        else:
+                            # we are parsing regular text
+                            if c == u'[':
+                                new_text.append(c)
+                                tag = True
+                            elif c == u' ':
+                                # new word, we do a new widget
+                                new_text.append(u' ')
+                                for t, v in reversed(styles):
+                                    new_text.append(u'[/{}]'.format(t))
+                                current_wid.text = u''.join(new_text)
+                                new_text = []
+                                self.add_widget(current_wid)
+                                log.debug(u"new widget: {}".format(current_wid.text))
+                                current_wid = self._createText()
+                                for t, v in styles:
+                                    new_text.append(u'[{tag}{value}]'.format(
+                                        tag = t,
+                                        value = u'={}'.format(v) if v else u''))
+                            else:
+                                new_text.append(c)
+                    if current_wid.text:
+                        # we may have a remaining widget after the parsing
+                        close_styles = []
+                        for t, v in reversed(styles):
+                            close_styles.append(u'[/{}]'.format(t))
+                        current_wid.text = u''.join(close_styles)
+                        self.add_widget(current_wid)
+                        log.debug(u"new widget: {}".format(current_wid.text))
+                else:
+                    # non Label widgets, we just add them
+                    self.add_widget(child)
+            self.splitted = True
+            log.debug(u"split OK")
+
+        # we now set the content width
+        # FIXME: for now we just use the full width
+        self.content_width = width
+
+    # XHTML parsing methods
+
+    def _callParseMethod(self, e):
+        """call the suitable method to parse the element
+
+        self.xhtml_[tag] will be called if it exists, else
+        self.xhtml_generic will be used
+        @param e(ET.Element): element to parse
+        """
+        try:
+            method = getattr(self, "xhtml_{}".format(e.tag))
+        except AttributeError:
+            log.warning(u"Unhandled XHTML tag: {}".format(e.tag))
+            method = self.xhtml_generic
+        method(e)
+
+    def _addStyle(self, tag, value=None, append_to_list=True):
+        """add a markup style to label
+
+        @param tag(unicode): markup tag
+        @param value(unicode): markup value if suitable
+        @param append_to_list(bool): if True style we be added to self.styles
+            self.styles is needed to keep track of styles to remove
+            should most probably be set to True
+        """
+        label = self._getLabel()
+        label.text += u'[{tag}{value}]'.format(
+            tag = tag,
+            value = u'={}'.format(value) if value else ''
+            )
+        if append_to_list:
+            self.styles.append((tag, value))
+
+    def _removeStyle(self, tag, remove_from_list=True):
+        """remove a markup style from the label
+
+        @param tag(unicode): markup tag to remove
+        @param remove_from_list(bool): if True, remove from self.styles too
+            should most probably be set to True
+        """
+        label = self._getLabel()
+        label.text += u'[/{tag}]'.format(
+            tag = tag
+            )
+        if remove_from_list:
+            for rev_idx, style in enumerate(reversed(self.styles)):
+                if style[0] == tag:
+                    tag_idx = len(self.styles) - 1 - rev_idx
+                    del self.styles[tag_idx]
+                    break
+
+    def _getLabel(self):
+        """get current Label if it exists, or create a new one"""
+        if not isinstance(self.current_wid, Label):
+            self._addLabel()
+        return self.current_wid
+
+    def _addLabel(self):
+        """add a new Label
+
+        current styles will be closed and reopened if needed
+        """
+        self._closeLabel()
+        self.current_wid = self._createText()
+        for tag, value in self.styles:
+            self._addStyle(tag, value, append_to_list=False)
+        self.add_widget(self.current_wid)
+
+    def _createText(self):
+        label = SimpleXHTMLWidgetText(color=self.color, markup=True)
+        self.bind(color=label.setter('color'))
+        label.bind(texture_size=label.setter('size'))
+        return label
+
+    def _closeLabel(self):
+        """close current style tags in current label
+
+        needed when you change label to keep style between
+        different widgets
+        """
+        if isinstance(self.current_wid, Label):
+            for tag, value in reversed(self.styles):
+                self._removeStyle(tag, remove_from_list=False)
+
+    def _parseCSS(self, e):
+        """parse CSS found in "style" attribute of element
+
+        self._css_styles will be created and contained markup styles added by this method
+        @param e(ET.Element): element which may have a "style" attribute
+        """
+        styles_limit = len(self.styles)
+        styles = e.attrib['style'].split(u';')
+        for style in styles:
+            try:
+                prop, value = style.split(u':')
+            except ValueError:
+                log.warning(u"can't parse style: {}".format(style))
+                continue
+            prop = prop.strip().replace(u'-', '_')
+            value = value.strip()
+            try:
+                method = getattr(self, "css_{}".format(prop))
+            except AttributeError:
+                log.warning(u"Unhandled CSS: {}".format(prop))
+            else:
+                method(e, value)
+        self._css_styles = self.styles[styles_limit:]
+
+    def _closeCSS(self):
+        """removed CSS styles
+
+        styles in self._css_styles will be removed
+        and the attribute will be deleted
+        """
+        for tag, dummy in reversed(self._css_styles):
+            self._removeStyle(tag)
+        del self._css_styles
+
+    def xhtml_generic(self, elem, style=True, markup=None):
+        """generic method for adding HTML elements
+
+        this method handle content, style and children parsing
+        @param elem(ET.Element): element to add
+        @param style(bool): if True handle style attribute (CSS)
+        @param markup(tuple[unicode, (unicode, None)], None): kivy markup to use
+        """
+        # we first add markup and CSS style
+        if markup is not None:
+            if isinstance(markup, basestring):
+                tag, value = markup, None
+            else:
+                tag, value = markup
+            self._addStyle(tag, value)
+        style_ = 'style' in elem.attrib and style
+        if style_:
+            self._parseCSS(elem)
+
+        # then content
+        if elem.text:
+            self._getLabel().text += escape_markup(elem.text)
+
+        # we parse the children
+        for child in elem:
+            self._callParseMethod(child)
+
+        # closing CSS style and markup
+        if style_:
+            self._closeCSS()
+        if markup is not None:
+            self._removeStyle(tag)
+
+        # and the tail, which is regular text
+        if elem.tail:
+            self._getLabel().text += escape_markup(elem.tail)
+
+    # method handling XHTML elements
+
+    def xhtml_br(self, elem):
+        label = self._getLabel()
+        label.text+='\n'
+        self.xhtml_generic(style=False)
+
+    def xhtml_em(self, elem):
+        self.xhtml_generic(elem, markup='i')
+
+    def xhtml_img(self, elem):
+        try:
+            src = elem.attrib['src']
+        except KeyError:
+            log.warning(u"<img> element without src: {}".format(ET.tostring(elem)))
+            return
+        try:
+            target_height = int(elem.get(u'height', 0))
+        except ValueError:
+            log.warning(u"Can't parse image height: {}".format(elem.get(u'height')))
+            target_height = 0
+        try:
+            target_width = int(elem.get(u'width', 0))
+        except ValueError:
+            log.warning(u"Can't parse image width: {}".format(elem.get(u'width')))
+            target_width = 0
+
+        img = SimpleXHTMLWidgetImage(source=src, target_height=target_height, target_width=target_width)
+        self.current_wid = img
+        self.add_widget(img)
+
+    def xhtml_p(self, elem):
+        if isinstance(self.current_wid, Label):
+            self.current_wid.text+="\n\n"
+        self.xhtml_generic(elem)
+
+    def xhtml_span(self, elem):
+        self.xhtml_generic(elem)
+
+    def xhtml_strong(self, elem):
+        self.xhtml_generic(elem, markup='b')
+
+    # methods handling CSS properties
+
+    def css_color(self, elem, value):
+        self._addStyle(u"color", css_color.parse(value))
+
+    def css_text_decoration(self, elem, value):
+        if value == u'underline':
+            log.warning(u"{} not handled yet, it needs Kivy 1.9.2 to be released".format(value))
+            # FIXME: activate when 1.9.2 is out
+            # self._addStyle('u')
+        elif value == u'line-through':
+            log.warning(u"{} not handled yet, it needs Kivy 1.9.2 to be released".format(value))
+            # FIXME: activate when 1.9.2 is out
+            # self._addStyle('s')
+        else:
+            log.warning(u"unhandled text decoration: {}".format(value))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/widgets_handler.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,230 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat_frontends.quick_frontend import quick_widgets
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.button import Button
+from kivy.uix.carousel import Carousel
+from kivy.metrics import dp
+from kivy import properties
+from cagou import G
+
+
+CAROUSEL_SCROLL_DISTANCE = dp(50)
+CAROUSEL_SCROLL_TIMEOUT = 80
+NEW_WIDGET_DIST = 10
+REMOVE_WIDGET_DIST = NEW_WIDGET_DIST
+
+
+class WHSplitter(Button):
+    horizontal=properties.BooleanProperty(True)
+    thickness=properties.NumericProperty(dp(20))
+    split_move = None # we handle one split at a time, so we use a class attribute
+
+    def __init__(self, handler, **kwargs):
+        super(WHSplitter, self).__init__(**kwargs)
+        self.handler = handler
+
+    def getPos(self, touch):
+        if self.horizontal:
+            relative_y = self.handler.to_local(*touch.pos, relative=True)[1]
+            return self.handler.height - relative_y
+        else:
+            return touch.x
+
+    def on_touch_move(self, touch):
+        if self.split_move is None and self.collide_point(*touch.opos):
+            WHSplitter.split_move = self
+
+        if self.split_move is self:
+            pos = self.getPos(touch)
+            if pos > NEW_WIDGET_DIST:
+                # we are above minimal distance, we resize the widget
+                self.handler.setWidgetSize(self.horizontal, pos)
+
+    def on_touch_up(self, touch):
+        if self.split_move is self:
+            pos = self.getPos(touch)
+            if pos <= REMOVE_WIDGET_DIST:
+                # if we go under minimal distance, the widget is not wanted anymore
+                self.handler.removeWidget(self.horizontal)
+            WHSplitter.split_move=None
+        return super(WHSplitter, self).on_touch_up(touch)
+
+
+class HandlerCarousel(Carousel):
+
+    def __init__(self, *args, **kwargs):
+        super(HandlerCarousel, self).__init__(
+            *args,
+            direction='right',
+            loop=True,
+            **kwargs)
+        self._former_slide = None
+        self.bind(current_slide=self.onSlideChange)
+        self._slides_update_lock = False
+
+    def changeWidget(self, new_widget):
+        """Change currently displayed widget
+
+        slides widgets will be updated
+        """
+        # slides update need to be blocked to avoid the update in onSlideChange
+        # which would mess the removal of current widgets
+        self._slides_update_lock = True
+        current = self.current_slide
+        for w in self.slides:
+            if w == current or w == new_widget:
+                continue
+            if isinstance(w, quick_widgets.QuickWidget):
+                G.host.widgets.deleteWidget(w)
+        self.clear_widgets()
+        self.add_widget(new_widget)
+        self._slides_update_lock = False
+        self.updateHiddenSlides()
+
+    def onSlideChange(self, handler, new_slide):
+        if isinstance(self._former_slide, quick_widgets.QuickWidget):
+            G.host.removeVisibleWidget(self._former_slide)
+        self._former_slide = new_slide
+        if isinstance(new_slide, quick_widgets.QuickWidget):
+            G.host.addVisibleWidget(new_slide)
+            self.updateHiddenSlides()
+
+    def hiddenList(self, visible_list):
+        """return widgets of same class as holded one which are hidden
+
+        @param visible_list(list[QuickWidget]): widgets visible
+        @return (iter[QuickWidget]): widgets hidden
+        """
+        added = [(w.targets, w.profiles) for w in visible_list]  # we want to avoid recreated widgets
+        for w in G.host.widgets.getWidgets(self.current_slide.__class__, profiles=self.current_slide.profiles):
+            if w in visible_list or (w.targets, w.profiles) in added:
+                continue
+            yield w
+
+    def widgets_sort(self, widget):
+        """method used as key to sort the widgets
+
+        order of the widgets when changing slide is affected
+        @param widget(QuickWidget): widget to sort
+        @return: a value which will be used for sorting
+        """
+        try:
+            return unicode(widget.target).lower()
+        except AttributeError:
+            return unicode(list(widget.targets)[0]).lower()
+
+    def updateHiddenSlides(self):
+        """adjust carousel slides according to visible widgets"""
+        if self._slides_update_lock:
+            return
+        if not isinstance(self.current_slide, quick_widgets.QuickWidget):
+            return
+        # lock must be used here to avoid recursions
+        self._slides_update_lock = True
+        visible_list = G.host.getVisibleList(self.current_slide.__class__)
+        hidden = list(self.hiddenList(visible_list))
+        slides_sorted =  sorted(hidden + [self.current_slide], key=self.widgets_sort)
+        to_remove = set(self.slides).difference({self.current_slide})
+        for w in to_remove:
+            self.remove_widget(w)
+        if hidden:
+            # no need to add more than two widgets (next and previous),
+            # as the list will be updated on each new visible widget
+            current_idx = slides_sorted.index(self.current_slide)
+            try:
+                next_slide = slides_sorted[current_idx+1]
+            except IndexError:
+                next_slide = slides_sorted[0]
+            self.add_widget(G.host.getOrClone(next_slide))
+            if len(hidden)>1:
+                previous_slide = slides_sorted[current_idx-1]
+                self.add_widget(G.host.getOrClone(previous_slide))
+
+        if len(self.slides) == 1:
+            # we block carousel with high scroll_distance to avoid swiping
+            # when the is not other instance of the widget
+            self.scroll_distance=2**32
+            self.scroll_timeout=0
+        else:
+            self.scroll_distance = CAROUSEL_SCROLL_DISTANCE
+            self.scroll_timeout=CAROUSEL_SCROLL_TIMEOUT
+        self._slides_update_lock = False
+
+
+class WidgetsHandler(BoxLayout):
+
+    def __init__(self, wid=None, **kw):
+        if wid is None:
+            wid=self.default_widget
+        self.vert_wid = self.hor_wid = None
+        BoxLayout.__init__(self, orientation="vertical", **kw)
+        self.blh = BoxLayout(orientation="horizontal")
+        self.blv = BoxLayout(orientation="vertical")
+        self.blv.add_widget(WHSplitter(self))
+        self.carousel = HandlerCarousel()
+        self.blv.add_widget(self.carousel)
+        self.blh.add_widget(WHSplitter(self, horizontal=False))
+        self.blh.add_widget(self.blv)
+        self.add_widget(self.blh)
+        self.changeWidget(wid)
+
+    @property
+    def default_widget(self):
+        return G.host.default_wid['factory'](G.host.default_wid, None, None)
+
+    @property
+    def cagou_widget(self):
+        """get holded CagouWidget"""
+        return self.carousel.current_slide
+
+    def changeWidget(self, new_widget):
+        self.carousel.changeWidget(new_widget)
+
+    def removeWidget(self, vertical):
+        if vertical and self.vert_wid is not None:
+            self.remove_widget(self.vert_wid)
+            self.vert_wid.onDelete()
+            self.vert_wid = None
+        elif self.hor_wid is not None:
+            self.blh.remove_widget(self.hor_wid)
+            self.hor_wid.onDelete()
+            self.hor_wid = None
+
+    def setWidgetSize(self, vertical, size):
+        if vertical:
+            if self.vert_wid is None:
+                self.vert_wid = WidgetsHandler(self.default_widget, size_hint=(1, None))
+                self.add_widget(self.vert_wid, len(self.children))
+            self.vert_wid.height=size
+        else:
+            if self.hor_wid is None:
+                self.hor_wid = WidgetsHandler(self.default_widget, size_hint=(None, 1))
+                self.blh.add_widget(self.hor_wid, len(self.blh.children))
+            self.hor_wid.width=size
+
+    def onDelete(self):
+        # when this handler is deleted, we need to delete the holded CagouWidget
+        cagou_widget = self.cagou_widget
+        if isinstance(cagou_widget, quick_widgets.QuickWidget):
+            G.host.removeVisibleWidget(cagou_widget)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/xmlui.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,561 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: a SàT frontend
+# Copyright (C) 2016-2018 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 _
+from .constants import Const as C
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import xmlui
+from kivy.uix.scrollview import ScrollView
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.gridlayout import GridLayout
+from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
+from kivy.uix.textinput import TextInput
+from kivy.uix.label import Label
+from kivy.uix.button import Button
+from kivy.uix.togglebutton import ToggleButton
+from kivy.uix.widget import Widget
+from kivy.uix.dropdown import DropDown
+from kivy.uix.switch import Switch
+from kivy import properties
+from cagou import G
+
+
+## Widgets ##
+
+
+class TextInputOnChange(object):
+
+    def __init__(self):
+        self._xmlui_onchange_cb = None
+        self._got_focus = False
+
+    def _xmluiOnChange(self, callback):
+        self._xmlui_onchange_cb = callback
+
+    def on_focus(self, instance, focus):
+        # we need to wait for first focus, else initial value
+        # will trigger a on_text
+        if not self._got_focus and focus:
+            self._got_focus = True
+
+    def on_text(self, instance, new_text):
+        log.debug("on_text: %s" % new_text)
+        if self._xmlui_onchange_cb is not None and self._got_focus:
+            self._xmlui_onchange_cb(self)
+
+
+class EmptyWidget(xmlui.EmptyWidget, Widget):
+
+    def __init__(self, _xmlui_parent):
+        Widget.__init__(self)
+
+
+class TextWidget(xmlui.TextWidget, Label):
+
+    def __init__(self, xmlui_parent, value):
+        Label.__init__(self, text=value)
+
+
+class LabelWidget(xmlui.LabelWidget, TextWidget):
+    pass
+
+
+class JidWidget(xmlui.JidWidget, TextWidget):
+    pass
+
+
+class StringWidget(xmlui.StringWidget, TextInput, TextInputOnChange):
+
+    def __init__(self, xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, text=value, multiline=False)
+        TextInputOnChange.__init__(self)
+        self.readonly = read_only
+
+    def _xmluiSetValue(self, value):
+        self.text = value
+
+    def _xmluiGetValue(self):
+        return self.text
+
+
+class JidInputWidget(xmlui.JidInputWidget, StringWidget):
+    pass
+
+
+class ButtonWidget(xmlui.ButtonWidget, Button):
+
+    def __init__(self, _xmlui_parent, value, click_callback):
+        Button.__init__(self)
+        self.text = value
+        self.callback = click_callback
+
+    def _xmluiOnClick(self, callback):
+        self.callback = callback
+
+    def on_release(self):
+        self.callback(self)
+
+
+class DividerWidget(xmlui.DividerWidget, Widget):
+    # FIXME: not working properly + only 'line' is handled
+    style = properties.OptionProperty('line',
+        options=['line', 'dot', 'dash', 'plain', 'blank'])
+
+    def __init__(self, _xmlui_parent, style="line"):
+        Widget.__init__(self, style=style)
+
+
+class ListWidgetItem(ToggleButton):
+    value = properties.StringProperty()
+
+    def on_release(self):
+        super(ListWidgetItem, self).on_release()
+        parent = self.parent
+        while parent is not None and not isinstance(parent, DropDown):
+            parent = parent.parent
+
+        if parent is not None and parent.attach_to is not None:
+            parent.select(self)
+
+    @property
+    def selected(self):
+        return self.state == 'down'
+
+    @selected.setter
+    def selected(self, value):
+        self.state = 'down' if value else 'normal'
+
+
+class ListWidget(xmlui.ListWidget, Button):
+
+    def __init__(self, _xmlui_parent, options, selected, flags):
+        Button.__init__(self)
+        self.text = _(u"open list")
+        self._dropdown = DropDown()
+        self._dropdown.auto_dismiss = False
+        self._dropdown.bind(on_select = self.on_select)
+        self.multi = 'single' not in flags
+        self._dropdown.dismiss_on_select = not self.multi
+        self._values = []
+        for option in options:
+            self.addValue(option)
+        self._xmluiSelectValues(selected)
+        self._on_change = None
+
+    @property
+    def items(self):
+        return self._dropdown.children[0].children
+
+    def on_touch_down(self, touch):
+        # we simulate auto-dismiss ourself because dropdown
+        # will dismiss even if attached button is touched
+        # resulting in a dismiss just before a toggle in on_release
+        # so the dropbox would always be opened, we don't want that!
+        if super(ListWidget, self).on_touch_down(touch):
+            return True
+        if self._dropdown.parent:
+            self._dropdown.dismiss()
+
+    def on_release(self):
+        if self._dropdown.parent is not None:
+            # we want to close a list already opened
+            self._dropdown.dismiss()
+        else:
+            self._dropdown.open(self)
+
+    def on_select(self, drop_down, item):
+        if not self.multi:
+            self._xmluiSelectValues([item.value])
+        if self._on_change is not None:
+            self._on_change(self)
+
+    def addValue(self, option, selected=False):
+        """add a value in the list
+
+        @param option(tuple): value, label in a tuple
+        """
+        self._values.append(option)
+        item = ListWidgetItem()
+        item.value, item.text = option
+        item.selected = selected
+        self._dropdown.add_widget(item)
+
+    def _xmluiSelectValue(self, value):
+        self._xmluiSelectValues([value])
+
+    def _xmluiSelectValues(self, values):
+        for item in self.items:
+            item.selected = item.value in values
+            if item.selected and not self.multi:
+                self.text = item.text
+
+    def _xmluiGetSelectedValues(self):
+        return [item.value for item in self.items if item.selected]
+
+    def _xmluiAddValues(self, values, select=True):
+        values = set(values).difference([c.value for c in self.items])
+        for v in values:
+            self.addValue(v, select)
+
+    def _xmluiOnChange(self, callback):
+        self._on_change = callback
+
+
+class JidsListWidget(ListWidget):
+    # TODO: real list dedicated to jids
+
+    def __init__(self, _xmlui_parent, jids, flags):
+        ListWidget.__init__(self, _xmlui_parent, [(j,j) for j in jids], [], flags)
+
+
+class PasswordWidget(xmlui.PasswordWidget, TextInput, TextInputOnChange):
+
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, password=True, multiline=False,
+            text=value, readonly=read_only, size=(100,25), size_hint=(1,None))
+        TextInputOnChange.__init__(self)
+
+    def _xmluiSetValue(self, value):
+        self.text = value
+
+    def _xmluiGetValue(self):
+        return self.text
+
+
+class BoolWidget(xmlui.BoolWidget, Switch):
+
+    def __init__(self, _xmlui_parent, state, read_only=False):
+        Switch.__init__(self, active=state)
+        if read_only:
+            self.disabled = True
+
+    def _xmluiSetValue(self, value):
+        self.active = value
+
+    def _xmluiGetValue(self):
+        return C.BOOL_TRUE if self.active else C.BOOL_FALSE
+
+    def _xmluiOnChange(self, callback):
+        self.bind(active=lambda instance, value: callback(instance))
+
+
+class IntWidget(xmlui.IntWidget, TextInput, TextInputOnChange):
+
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, text=value, input_filter='int', multiline=False)
+        TextInputOnChange.__init__(self)
+        if read_only:
+            self.disabled = True
+
+    def _xmluiSetValue(self, value):
+        self.text = value
+
+    def _xmluiGetValue(self):
+        return self.text
+
+
+## Containers ##
+
+
+class VerticalContainer(xmlui.VerticalContainer, GridLayout):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        GridLayout.__init__(self)
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class PairsContainer(xmlui.PairsContainer, GridLayout):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        GridLayout.__init__(self)
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class LabelContainer(PairsContainer, xmlui.LabelContainer):
+    pass
+
+
+class TabsPanelContainer(TabbedPanelItem):
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class TabsContainer(xmlui.TabsContainer, TabbedPanel):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        xmlui_panel = xmlui_parent
+        while not isinstance(xmlui_panel, XMLUIPanel):
+            xmlui_panel = xmlui_panel.xmlui_parent
+        xmlui_panel.addPostTreat(self._postTreat)
+        TabbedPanel.__init__(self, do_default_tab=False)
+
+    def _xmluiAddTab(self, label, selected):
+        tab = TabsPanelContainer(text=label)
+        self.add_widget(tab)
+        return tab
+
+    def _postTreat(self):
+        """bind minimum height of tabs' content so self.height is adapted"""
+        # we need to do this in postTreat because contents exists after UI construction
+        for t in self.tab_list:
+            t.content.bind(minimum_height=self._updateHeight)
+
+    def _updateHeight(self, instance, height):
+        """Called after UI is constructed (so height can be calculated)"""
+        # needed because TabbedPanel doesn't have a minimum_height property
+        self.height = max([t.content.minimum_height for t in self.tab_list]) + self.tab_height + 5
+
+
+class AdvancedListRow(GridLayout):
+    global_index = 0
+    index = properties.ObjectProperty()
+    selected = properties.BooleanProperty(False)
+
+    def __init__(self, **kwargs):
+        self.global_index = AdvancedListRow.global_index
+        AdvancedListRow.global_index += 1
+        super(AdvancedListRow, self).__init__(**kwargs)
+
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            parent = self.parent
+            while parent is not None and not isinstance(parent, AdvancedListContainer):
+                parent = parent.parent
+            if parent is None:
+                log.error(u"Can't find parent AdvancedListContainer")
+            else:
+                if parent.selectable:
+                    self.selected = parent._xmluiToggleSelected(self)
+
+        return super(AdvancedListRow, self).on_touch_down(touch)
+
+
+class AdvancedListContainer(xmlui.AdvancedListContainer, GridLayout):
+
+    def __init__(self, xmlui_parent, columns, selectable='no'):
+        self.xmlui_parent = xmlui_parent
+        GridLayout.__init__(self)
+        self._columns = columns
+        self.selectable = selectable != 'no'
+        self._current_row = None
+        self._selected = []
+        self._xmlui_select_cb = None
+
+    def _xmluiToggleSelected(self, row):
+        """inverse selection status of an AdvancedListRow
+
+        @param row(AdvancedListRow): row to (un)select
+        @return (bool): True if row is selected
+        """
+        try:
+            self._selected.remove(row)
+        except ValueError:
+            self._selected.append(row)
+            if self._xmlui_select_cb is not None:
+                self._xmlui_select_cb(self)
+            return True
+        else:
+            return False
+
+    def _xmluiAppend(self, widget):
+        if self._current_row is None:
+            log.error(u"No row set, ignoring append")
+            return
+        self._current_row.add_widget(widget)
+
+    def _xmluiAddRow(self, idx):
+        self._current_row = AdvancedListRow()
+        self._current_row.cols = self._columns
+        self._current_row.index = idx
+        self.add_widget(self._current_row)
+
+    def _xmluiGetSelectedWidgets(self):
+        return self._selected
+
+    def _xmluiGetSelectedIndex(self):
+        if not self._selected:
+            return None
+        return self._selected[0].index
+
+    def _xmluiOnSelect(self, callback):
+        """ Call callback with widget as only argument """
+        self._xmlui_select_cb = callback
+
+## Dialogs ##
+
+
+class NoteDialog(xmlui.NoteDialog):
+
+    def __init__(self, _xmlui_parent, title, message, level):
+        xmlui.NoteDialog.__init__(self, _xmlui_parent)
+        self.title, self.message, self.level = title, message, level
+
+    def _xmluiShow(self):
+        G.host.addNote(self.title, self.message, self.level)
+
+
+class FileDialog(xmlui.FileDialog, BoxLayout):
+    message = properties.ObjectProperty()
+
+    def __init__(self, _xmlui_parent, title, message, level, filetype):
+        xmlui.FileDialog.__init__(self, _xmlui_parent)
+        BoxLayout.__init__(self)
+        self.message.text = message
+        if filetype == C.XMLUI_DATA_FILETYPE_DIR:
+            self.file_chooser.dirselect = True
+
+    def _xmluiShow(self):
+        G.host.addNotifUI(self)
+
+    def _xmluiClose(self):
+        # FIXME: notif UI is not removed if dialog is not shown yet
+        G.host.closeUI()
+
+    def onSelect(self, path):
+        try:
+            path = path[0]
+        except IndexError:
+            path = None
+        if not path:
+            self._xmluiCancelled()
+        else:
+            self._xmluiValidated({'path': path})
+
+    def show(self, *args, **kwargs):
+        assert kwargs["force"]
+        G.host.showUI(self)
+
+
+## Factory ##
+
+
+class WidgetFactory(object):
+
+    def __getattr__(self, attr):
+        if attr.startswith("create"):
+            cls = globals()[attr[6:]]
+            return cls
+
+
+## Core ##
+
+
+class Title(Label):
+
+    def __init__(self, *args, **kwargs):
+        kwargs['size'] = (100, 25)
+        kwargs['size_hint'] = (1,None)
+        super(Title, self).__init__(*args, **kwargs)
+
+
+class FormButton(Button):
+    pass
+
+
+class XMLUIPanelGrid(GridLayout):
+    pass
+
+class XMLUIPanel(xmlui.XMLUIPanel, ScrollView):
+    widget_factory = WidgetFactory()
+
+    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, ignore=None, profile=C.PROF_KEY_NONE):
+        ScrollView.__init__(self)
+        self.close_cb = None
+        self._grid = XMLUIPanelGrid()
+        self._post_treats = []  # list of callback to call after UI is constructed
+        ScrollView.add_widget(self, self._grid)
+        xmlui.XMLUIPanel.__init__(self,
+                                  host,
+                                  parsed_xml,
+                                  title=title,
+                                  flags=flags,
+                                  callback=callback,
+                                  ignore=ignore,
+                                  profile=profile)
+
+    def add_widget(self, wid):
+        self._grid.add_widget(wid)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def _xmluiClose(self):
+        if self.close_cb is not None:
+            self.close_cb(self)
+        else:
+            G.host.closeUI()
+
+    def onParamChange(self, ctrl):
+        super(XMLUIPanel, self).onParamChange(ctrl)
+        self.save_btn.disabled = False
+
+    def addPostTreat(self, callback):
+        self._post_treats.append(callback)
+
+    def _postTreatCb(self):
+        for cb in self._post_treats:
+            cb()
+        del self._post_treats
+
+    def _saveButtonCb(self, button):
+        button.disabled = True
+        self.onSaveParams(button)
+
+    def constructUI(self, parsed_dom):
+        xmlui.XMLUIPanel.constructUI(self, parsed_dom, self._postTreatCb)
+        if self.xmlui_title:
+            self.add_widget(Title(text=self.xmlui_title))
+        self.add_widget(self.main_cont)
+        if self.type == 'form':
+            submit_btn = FormButton(text=_(u"Submit"))
+            submit_btn.bind(on_press=self.onFormSubmitted)
+            self.add_widget(submit_btn)
+            if not 'NO_CANCEL' in self.flags:
+                cancel_btn = FormButton(text=_(u"Cancel"))
+                cancel_btn.bind(on_press=self.onFormCancelled)
+                self.add_widget(cancel_btn)
+        elif self.type == 'param':
+            self.save_btn = FormButton(text=_(u"Save"), disabled=True)
+            self.save_btn.bind(on_press=self._saveButtonCb)
+            self.add_widget(self.save_btn)
+        self.add_widget(Widget())  # to have elements on the top
+
+    def show(self, *args, **kwargs):
+        if not self.user_action and not kwargs.get("force", False):
+            G.host.addNotifUI(self)
+        else:
+            G.host.showUI(self)
+
+
+class XMLUIDialog(xmlui.XMLUIDialog):
+    dialog_factory = WidgetFactory()
+
+
+xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel)
+xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog)
+create = xmlui.create
Binary file cagou/images/button.png has changed
Binary file cagou/images/button_selected.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/cagou_widget.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,68 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+
+<HeaderWidgetChoice>:
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 1
+        BorderImage:
+            pos: self.pos
+            size: self.size
+            source: 'atlas://data/images/defaulttheme/button'
+    size_hint_y: None
+    height: dp(44)
+    Image:
+        size_hint: None, 1
+        source: root.plugin_info['icon_medium']
+        allow_stretch: True
+        width: self.texture_size[0]*self.height/(self.texture_size[1] or 1)
+    Label:
+        size_hint: 1, 1
+        text: root.plugin_info['name']
+        bold: True
+        text_size: self.size
+        halign: "center"
+        valign: "middle"
+
+<HeaderWidgetSelector>:
+    size_hint: 0.3, None
+    auto_width: False
+    canvas.before:
+        Color:
+            rgba: 0, 0, 0, 1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+
+<CagouWidget>:
+    header_input: header_input
+    header_box: header_box
+    BoxLayout:
+        id: header_box
+        size_hint: 1, None
+        height: dp(32)
+        HeaderWidgetCurrent:
+            on_release: root.selector.open(self)
+            source: root.plugin_info['icon_small']
+            size_hint: None, 1
+            allow_stretch: True
+            width: self.texture_size[0]*self.height/(self.texture_size[1] or 1)
+        TextInput:
+            id: header_input
+            multiline: False
+            on_text_validate: root.onHeaderInput()
+            on_text: root.onHeaderInputComplete(*args)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/common.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,37 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+
+<JidWidget>:
+    size_hint: None, None
+    height: dp(70)
+    canvas.before:
+        Color:
+            rgba: 0.2, 0.2, 0.2, 1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    Image:
+        source: root.getImage(self)
+        size_hint: None, None
+        size: dp(64), dp(64)
+    Label:
+        bold: True
+        text: root.jid
+        text_size: self.size
+        halign: 'left'
+        valign: 'middle'
+        padding_x: dp(20)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/menu.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,88 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+#:import _ sat.core.i18n._
+
+<AboutContent>:
+    text_size: self.size
+    halign: "center"
+    valign: "middle"
+
+<AboutPopup>:
+    title_align: "center"
+    size_hint: 0.8, 0.8
+
+<MenuItem>:
+    # following is need to fix a bug in contextmenu
+    size_hint: 1, None
+
+<MenusWidget>:
+    height: self.children[0].height if self.children else 30
+
+<MainMenu>:
+    cancel_handler_widget: self.parent
+
+<TransferMenu>:
+    items_layout: items_layout
+    orientation: "vertical"
+    pos_hint: {"top": 0.5}
+    size_hint: 1, 0.5
+    canvas.before:
+        Color:
+            rgba: 0, 0, 0, 1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    BoxLayout:
+        size_hint: 1, None
+        height: dp(50)
+        ToggleButton:
+            id: upload_btn
+            text: _(u"upload")
+            group: "transfer"
+            state: "down"
+        ToggleButton:
+            id: send_btn
+            text: _(u"send")
+            group: "transfer"
+    Label:
+        size_hint: 1, 0.3
+        text: root.transfer_txt if upload_btn.state == 'down' else root.send_txt
+        text_size: self.size
+        halign: 'center'
+        valign: 'top'
+    ScrollView:
+        do_scroll_x: False
+        StackLayout:
+            size_hint: 1, None
+            padding: 20, 0
+            spacing: 15, 5
+            id: items_layout
+
+<TransferItem>:
+    orientation: "vertical"
+    size_hint: None, None
+    size: dp(50), dp(90)
+    IconButton:
+        source: root.plug_info['icon_medium']
+        allow_stretch: True
+        size_hint: 1, None
+        height: dp(50)
+    Label:
+        text: root.plug_info['name']
+        text_size: self.size
+        halign: "center"
+        valign: "top"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/profile_manager.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,168 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+
+<ProfileManager>:
+    Label:
+        text: "Profile Manager"
+        size_hint: 1,0.05
+
+<PMLabel@Label>:
+    size_hint: 1, None
+    height: sp(30)
+
+<PMInput@TextInput>:
+    multiline: False
+    size_hint: 1, None
+    height: sp(30)
+    write_tab: False
+
+<PMButton@Button>:
+    size_hint: 1, 0.2
+
+
+<NewProfileScreen>:
+    profile_name: profile_name
+    jid: jid
+    password: password
+
+    BoxLayout:
+        orientation: "vertical"
+
+        Label:
+            text: "Creation of a new profile"
+            bold: True
+            size_hint: 1, 0.1
+        Label:
+            text: root.error_msg
+            bold: True
+            size_hint: 1, 0.1
+            color: 1,0,0,1
+        GridLayout:
+            cols: 2
+
+            PMLabel:
+                text: "Profile name"
+            PMInput:
+                id: profile_name
+
+            PMLabel:
+                text: "JID"
+            PMInput:
+                id: jid
+
+            PMLabel:
+                text: "Password"
+            PMInput:
+                id: password
+                password: True
+
+            Widget:
+                size_hint: 1, 0.2
+
+            Widget:
+                size_hint: 1, 0.2
+
+            PMButton:
+                text: "OK"
+                on_press: root.doCreate()
+
+            PMButton:
+                text: "Cancel"
+                on_press:
+                    root.pm.screen_manager.transition.direction = 'right'
+                    root.pm.screen_manager.current = 'profiles'
+
+            Widget:
+
+
+<DeleteProfilesScreen>:
+    BoxLayout:
+        orientation: "vertical"
+
+        Label:
+            text: "Are you sure you want to delete the following profiles?"
+            size_hint: 1, 0.1
+
+        Label:
+            text: u'\n'.join([i.text for i in root.pm.profiles_screen.list_adapter.selection])
+            bold: True
+
+        Label:
+            text: u'/!\\ WARNING: this operation is irreversible'
+            color: 1,0,0,1
+            bold: True
+            size_hint: 1, 0.2
+
+        GridLayout:
+            cols: 2
+
+            Button:
+                text: "Delete"
+                size_hint: 1, 0.2
+                on_press: root.doDelete()
+
+            Button:
+                text: "Cancel"
+                size_hint: 1, 0.2
+                on_press:
+                    root.pm.screen_manager.transition.direction = 'right'
+                    root.pm.screen_manager.current = 'profiles'
+
+            Widget:
+
+
+<ProfilesScreen>:
+    layout: layout
+    BoxLayout:
+        id: layout
+        orientation: 'vertical'
+
+        Label:
+            text: "Select a profile or create a new one"
+            size_hint: 1,0.05
+
+        GridLayout:
+            cols: 2
+            size_hint: 1, 0.1
+            Button:
+                size_hint: 1, 0.1
+                text: "New"
+                on_press:
+                    root.pm.screen_manager.transition.direction = 'left'
+                    root.pm.screen_manager.current = 'new_profile'
+            Button:
+                disabled: not root.list_adapter.selection
+                text: "Delete"
+                size_hint: 1, 0.1
+                on_press:
+                    root.pm.screen_manager.transition.direction = 'left'
+                    root.pm.screen_manager.current = 'delete_profiles'
+
+
+<ConnectButton>:
+    text: "Connect"
+    size_hint: 1, 0.1
+    disabled: not self.profile_screen.list_adapter.selection
+    on_press: self.pm._onConnectProfiles()
+
+
+<ProfileItem>:
+    # FIXME: using cagou/images path for now, will use atlas later
+    background_normal: "cagou/images/button_selected.png" if self.is_selected else "cagou/images/button.png"
+    deselected_color: 1,1,1,1
+    selected_color: 1,1,1,1
+    color: 0,0,0,1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/root_widget.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,95 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+#:import IconButton cagou.core.common.IconButton
+
+# <NotifIcon>:
+#     source: app.expand("{media}/icons/muchoslava/png/cagou_profil_bleu_32.png")
+#     size_hint: None, None
+#     size: self.texture_size
+
+<Note>:
+    text: self.message
+
+<NoteDrop>:
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 1
+        BorderImage:
+            pos: self.pos
+            size: self.size
+            source: 'atlas://data/images/defaulttheme/button'
+    size_hint: 1, None
+    text_size: self.width, None
+    halign: 'center'
+    height: self.texture_size[1]
+    padding: dp(2), dp(10)
+
+<NotesDrop>:
+    clear_btn: clear_btn.__self__
+    auto_width: False
+    size_hint: 0.8, None
+    canvas.before:
+        Color:
+            rgba: 0.8, 0.8, 0.8, 1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    Button:
+        id: clear_btn
+        text: "clear"
+        bold: True
+        size_hint: 1, None
+        height: dp(50)
+        on_release: del root.notes[:]; root.dismiss()
+
+<RootHeadWidget>:
+    manager: manager
+    notifs_icon: notifs_icon
+    size_hint: 1, None
+    height: dp(35)
+    IconButton:
+        source: app.expand("{media}/icons/muchoslava/png/cagou_profil_bleu_32.png") if root.notes else app.expand("{media}/misc/black.png")
+        allow_stretch: True
+        size_hint: None, 1
+        width: self.norm_image_size[0]
+        on_release: root.notes_drop.open(self) if root.notes else None
+    ScreenManager:
+        id: manager
+    NotifsIcon:
+        id: notifs_icon
+        allow_stretch: True
+        source: app.expand("{media}/icons/muchoslava/png/cagou_profil_bleu_32.png") if self.notifs else app.expand("{media}/misc/black.png")
+        size_hint: None, 1
+        width: self.norm_image_size[0]
+
+<RootMenus>:
+    size_hint: 1, None
+    pos_hint: {'top': 1}
+
+<CagouRootWidget>:
+    root_body: root_body
+    root_menus: root_menus
+    # main body
+    RootBody:
+        id: root_body
+        orientation: "vertical"
+        size_hint: 1, None
+        height: root.height - root_menus.height
+    # general menus
+    # need to be added at the end so it's drawed above other widgets
+    RootMenus:
+        id: root_menus
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/simple_xhtml.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,27 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+
+<SimpleXHTMLWidgetEscapedText>:
+    size_hint: None, None
+    size: self.texture_size
+
+<SimpleXHTMLWidgetText>:
+    size_hint: None, None
+    size: self.texture_size
+
+<SimpleXHTMLWidgetImage>:
+    size_hint: None, None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/widgets_handler.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,28 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+<WHSplitter>:
+    border: (3, 3, 3, 3)
+    horizontal_suff: '_h' if self.horizontal else ''
+    background_normal: 'atlas://data/images/defaulttheme/splitter{}{}'.format('_disabled' if self.disabled else '', self.horizontal_suff)
+    background_down: 'atlas://data/images/defaulttheme/splitter_down{}{}'.format('_disabled' if self.disabled else '', self.horizontal_suff)
+    size_hint: (1, None) if self.horizontal else (None, 1)
+    size: (100, self.thickness) if self.horizontal else (self.thickness, 100)
+    Image:
+        pos: root.pos
+        size: root.size
+        allow_stretch: True
+        source: 'atlas://data/images/defaulttheme/splitter_grip' + root.horizontal_suff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/kv/xmlui.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,128 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+#:set common_height 30
+#:set button_height 50
+
+<EmptyWidget,TextWidget,LabelWidget,JidWidget,StringWidget,PasswordWidget,JidInputWidget>:
+    size_hint: 1, None
+    height: dp(common_height)
+
+
+<ButtonWidget>:
+    size_hint: 1, None
+    height: dp(button_height)
+
+
+<BoolWidget>:
+    size_hint: 1, 1
+
+
+<DividerWidget>:
+    size_hint: 1, None
+    height: dp(20)
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 0.8
+        Line
+            points: 0, dp(10), self.width, dp(10)
+            width: dp(3)
+
+
+<ListWidgetItem>:
+    size_hint_y: None
+    height: dp(button_height)
+
+
+<ListWidget>:
+    size_hint: 1, None
+    height: dp(button_height)
+
+
+<AdvancedListRow>:
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 0.2 if self.global_index%2 else 0.1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    size_hint: 1, None
+    height: self.minimum_height
+    rows: 1
+    canvas.after:
+        Color:
+            rgba: 0, 0, 1, 0.5 if self.selected else 0
+        Rectangle:
+            pos: self.pos
+            size: self.size
+
+
+<AdvancedListContainer>:
+    cols: 1
+    size_hint: 1, None
+    height: self.minimum_height
+
+
+<VerticalContainer>:
+    cols: 1
+    size_hint: 1, None
+    height: self.minimum_height
+
+
+<PairsContainer>:
+    cols: 2
+    size_hint: 1, None
+    height: self.minimum_height
+
+
+<TabsContainer>:
+    size_hint: 1, None
+    height: 100
+
+
+<FormButton>:
+    size_hint: 1, None
+    height: dp(button_height)
+
+
+<FileDialog>:
+    orientation: "vertical"
+    message: message
+    file_chooser: file_chooser
+    Label:
+        id: message
+        size_hint: 1, None
+        text_size: root.width, None
+        size: self.texture_size
+    FileChooserListView:
+        id: file_chooser
+    Button:
+        size_hint: 1, None
+        height: dp(50)
+        text: "choose"
+        on_release: root.onSelect(file_chooser.selection)
+    Button:
+        size_hint: 1, None
+        height: dp(50)
+        text: "cancel"
+        on_release: root.onCancel()
+
+
+
+<XMLUIPanelGrid>:
+    cols: 1
+    size_hint: 1, None
+    height: self.minimum_height
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_android_gallery.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+import sys
+import tempfile
+import os
+import os.path
+if sys.platform=="android":
+    from jnius import autoclass
+    from android import activity, mActivity
+
+    Intent = autoclass('android.content.Intent')
+    OpenableColumns = autoclass('android.provider.OpenableColumns')
+    PHOTO_GALLERY = 1
+    RESULT_OK = -1
+
+
+
+PLUGIN_INFO = {
+    "name": _(u"gallery"),
+    "main": "AndroidGallery",
+    "platforms": ('android',),
+    "external": True,
+    "description": _(u"upload a photo from photo gallery"),
+    "icon_medium": u"{media}/icons/muchoslava/png/gallery_50.png",
+}
+
+
+class AndroidGallery(object):
+
+    def __init__(self, callback, cancel_cb):
+        self.callback = callback
+        self.cancel_cb = cancel_cb
+        activity.bind(on_activity_result=self.on_activity_result)
+        intent = Intent()
+        intent.setType('image/*')
+        intent.setAction(Intent.ACTION_GET_CONTENT)
+        mActivity.startActivityForResult(intent, PHOTO_GALLERY);
+
+    def on_activity_result(self, requestCode, resultCode, data):
+        # TODO: move file dump to a thread or use async callbacks during file writting
+        if requestCode == PHOTO_GALLERY and resultCode == RESULT_OK:
+            if data is None:
+                log.warning(u"No data found in activity result")
+                self.cancel_cb(self, None)
+                return
+            uri = data.getData()
+
+            # we get filename in the way explained at https://developer.android.com/training/secure-file-sharing/retrieve-info.html
+            cursor = mActivity.getContentResolver().query(uri, None, None, None, None )
+            name_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+            cursor.moveToFirst()
+            filename = cursor.getString(name_idx)
+
+            # we save data in a temporary file that we send to callback
+            # the file will be removed once upload is done (or if an error happens)
+            input_stream = mActivity.getContentResolver().openInputStream(uri)
+            tmp_dir = tempfile.mkdtemp()
+            tmp_file = os.path.join(tmp_dir, filename)
+            def cleaning():
+                os.unlink(tmp_file)
+                os.rmdir(tmp_dir)
+                log.debug(u'temporary file cleaned')
+            buff = bytearray(4096)
+            with open(tmp_file, 'wb') as f:
+                while True:
+                    ret = input_stream.read(buff, 0, 4096)
+                    if ret != -1:
+                        f.write(buff)
+                    else:
+                        break
+            input_stream.close()
+            self.callback(tmp_file, cleaning)
+        else:
+            self.cancel_cb(self, None)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_android_photo.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+import sys
+import os
+import os.path
+import time
+if sys.platform == "android":
+    from plyer import camera
+    from jnius import autoclass
+    Environment = autoclass('android.os.Environment')
+else:
+    import tempfile
+
+
+PLUGIN_INFO = {
+    "name": _(u"take photo"),
+    "main": "AndroidPhoto",
+    "platforms": ('android',),
+    "external": True,
+    "description": _(u"upload a photo from photo application"),
+    "icon_medium": u"{media}/icons/muchoslava/png/camera_off_50.png",
+}
+
+
+class AndroidPhoto(object):
+
+    def __init__(self, callback, cancel_cb):
+        self.callback = callback
+        self.cancel_cb = cancel_cb
+        filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg", time.gmtime())
+        tmp_dir = self.getTmpDir()
+        tmp_file = os.path.join(tmp_dir, filename)
+        log.debug(u"Picture will be saved to {}".format(tmp_file))
+        camera.take_picture(tmp_file, self.callback)
+        # we don't delete the file, as it is nice to keep it locally
+
+    def getTmpDir(self):
+        if sys.platform == "android":
+            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
+            return dcim_path
+        else:
+            return tempfile.mkdtemp()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_android_video.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+import sys
+import os
+import os.path
+import time
+if sys.platform == "android":
+    from plyer import camera
+    from jnius import autoclass
+    Environment = autoclass('android.os.Environment')
+else:
+    import tempfile
+
+
+PLUGIN_INFO = {
+    "name": _(u"take video"),
+    "main": "AndroidVideo",
+    "platforms": ('android',),
+    "external": True,
+    "description": _(u"upload a video from video application"),
+    "icon_medium": u"{media}/icons/muchoslava/png/film_camera_off_50.png",
+}
+
+
+class AndroidVideo(object):
+
+    def __init__(self, callback, cancel_cb):
+        self.callback = callback
+        self.cancel_cb = cancel_cb
+        filename = time.strftime("%Y-%m-%d_%H:%M:%S.mpg", time.gmtime())
+        tmp_dir = self.getTmpDir()
+        tmp_file = os.path.join(tmp_dir, filename)
+        log.debug(u"Video will be saved to {}".format(tmp_file))
+        camera.take_video(tmp_file, self.callback)
+        # we don't delete the file, as it is nice to keep it locally
+
+    def getTmpDir(self):
+        if sys.platform == "android":
+            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
+            return dcim_path
+        else:
+            return tempfile.mkdtemp()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_file.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,35 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+#:import expanduser os.path.expanduser
+#:import platform kivy.utils.platform
+
+
+<FileTransmitter>:
+    orientation: "vertical"
+    FileChooserListView:
+        id: filechooser
+        rootpath: "/" if platform == 'android' else expanduser('~')
+    Button:
+        text: "choose"
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.onTransmitOK(filechooser)
+    Button:
+        text: "cancel"
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.cancel_cb(root)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_file.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 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": "FileTransmitter",
+    "description": _(u"transmit a local file"),
+    "icon_medium": u"{media}/icons/muchoslava/png/fichier_50.png",
+}
+
+
+class FileTransmitter(BoxLayout):
+    callback = properties.ObjectProperty()
+    cancel_cb = properties.ObjectProperty()
+
+    def onTransmitOK(self, filechooser):
+        if filechooser.selection:
+            file_path = filechooser.selection[0]
+            self.callback(file_path)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_voice.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,71 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+#:import _ sat.core.i18n._
+#:import IconButton cagou.core.common.IconButton
+
+
+<VoiceRecorder>:
+    orientation: "vertical"
+    counter: counter
+    Label:
+        size_hint: 1, 0.4
+        text_size: self.size
+        halign: 'center'
+        valign: 'top'
+        text: _(u"Push the microphone button to start the record, then push it again to stop it.\nWhen you are satisfied, click on the transmit button")
+    Label:
+        id: counter
+        size_hint: 1, None
+        height: dp(60)
+        bold: True
+        font_size: sp(40)
+        text_size: self.size
+        text: u"{}:{:02}".format(root.time/60, root.time%60)
+        halign: 'center'
+        valign: 'middle'
+    BoxLayout:
+        size_hint: 1, None
+        height: dp(60)
+        Widget
+        IconButton:
+            source: app.expand("{media}/icons/muchoslava/png/") + ("micro_on_50.png" if root.recording else "micro_off_50.png")
+            allow_stretch: True
+            size_hint: None, None
+            size: dp(60), dp(60)
+            on_release: root.switchRecording()
+        IconButton:
+            opacity: 0 if root.recording or not root.time and not root.playing else 1
+            source: app.expand("{media}/icons/muchoslava/png/") + ("stop_50.png" if root.playing else "play_50.png")
+            allow_stretch: True
+            size_hint: None, None
+            size: dp(60), dp(60)
+            on_release: root.playRecord()
+        Widget
+    Widget:
+        size_hint: 1, None
+        height: dp(50)
+    Button:
+        text: _("transmit")
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.callback(root.audio.file_path)
+    Button:
+        text: _("cancel")
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.cancel_cb(root)
+    Widget
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_transfer_voice.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from kivy.uix.boxlayout import BoxLayout
+import sys
+import time
+from kivy.clock import Clock
+from kivy import properties
+if sys.platform == "android":
+    from plyer import audio
+
+
+PLUGIN_INFO = {
+    "name": _(u"voice"),
+    "main": "VoiceRecorder",
+    "platforms": ["android"],
+    "description": _(u"transmit a voice record"),
+    "icon_medium": u"{media}/icons/muchoslava/png/micro_off_50.png",
+}
+
+
+class VoiceRecorder(BoxLayout):
+    callback = properties.ObjectProperty()
+    cancel_cb = properties.ObjectProperty()
+    recording = properties.BooleanProperty(False)
+    playing = properties.BooleanProperty(False)
+    time = properties.NumericProperty(0)
+
+    def __init__(self, **kwargs):
+        super(VoiceRecorder, self).__init__(**kwargs)
+        self._started_at = None
+        self._counter_timer = None
+        self._play_timer = None
+        self.record_time = None
+        self.audio = audio
+        self.audio.file_path = "/sdcard/cagou_record.3gp"
+
+    def _updateTimer(self, dt):
+        self.time = int(time.time() - self._started_at)
+
+    def switchRecording(self):
+        if self.playing:
+            self._stopPlaying()
+        if self.recording:
+            try:
+                audio.stop()
+            except Exception as e:
+                # an exception can happen if record is pressed
+                # repeatedly in a short time (not a normal use)
+                log.warning(u"Exception on stop: {}".format(e))
+            self._counter_timer.cancel()
+            self.time = self.time + 1
+        else:
+            audio.start()
+            self._started_at = time.time()
+            self.time = 0
+            self._counter_timer = Clock.schedule_interval(self._updateTimer, 1)
+
+        self.recording = not self.recording
+
+    def _stopPlaying(self, dummy=None):
+        if self.record_time is None:
+            log.error("_stopPlaying should no be called when record_time is None")
+            return
+        audio.stop()
+        self.playing = False
+        self.time = self.record_time
+        if self._counter_timer is not None:
+            self._counter_timer.cancel()
+
+    def playRecord(self):
+        if self.recording:
+            return
+        if self.playing:
+            self._stopPlaying()
+        else:
+            try:
+                audio.play()
+            except Exception as e:
+                # an exception can happen in the same situation
+                # as for audio.stop() above (i.e. bad record)
+                log.warning(u"Exception on play: {}".format(e))
+                self.time = 0
+                return
+
+            self.playing = True
+            self.record_time = self.time
+            Clock.schedule_once(self._stopPlaying, self.time + 1)
+            self._started_at = time.time()
+            self.time = 0
+            self._counter_timer =  Clock.schedule_interval(self._updateTimer, 0.5)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_chat.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,146 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+#:import TransferMenu cagou.core.menu.TransferMenu
+#:import SimpleXHTMLWidget cagou.core.simple_xhtml.SimpleXHTMLWidget
+#:import _ sat.core.i18n._
+#:import C cagou.core.constants.Const
+
+
+<MessAvatar>:
+    size_hint: None, None
+    size: dp(30), dp(30)
+
+<MessagesWidget>:
+    cols: 1
+    padding: dp(10)
+    spacing: dp(5)
+    size_hint: 1, None
+    height: self.minimum_height
+
+<MessageWidget>:
+    cols: 1
+    mess_xhtml: mess_xhtml
+    padding: dp(10)
+    spacing: dp(5)
+    size_hint: 1, None
+    height: self.minimum_height
+    on_width: self.widthAdjust()
+    avatar: avatar
+    delivery: delivery
+    BoxLayout:
+        id: header_box
+        size_hint: 1, None
+        height: avatar.height if root.mess_data.type != C.MESS_TYPE_INFO else 0
+        opacity: 1 if root.mess_data.type != C.MESS_TYPE_INFO else 0
+        MessAvatar:
+            id: avatar
+        Label:
+            id: time_label
+            text_size: None, None
+            size_hint: None, None
+            size: self.texture_size
+            padding: dp(5), 0
+            text: u"{}, {}".format(root.mess_data.nick, root.mess_data.time_text)
+        Label:
+            id: delivery
+            text_size: None, None
+            size_hint: None, None
+            size: self.texture_size
+            padding: dp(5), 0
+            # XXX: DejaVuSans font is needed as check mark is not in Roboto
+            # this can be removed when Kivy will be able to handle fallback mechanism
+            # which will allow us to use fonts with more unicode characters
+            font_name: "DejaVuSans"
+            text: u''
+            color: 0, 1, 0, 1
+    BoxLayout:
+        # BoxLayout is needed here, else GridLayout won't let the Label choose its width
+        size_hint: 1, None
+        height: mess_xhtml.height
+        on_size: root.widthAdjust()
+        SimpleXHTMLWidget:
+            canvas.before:
+                Color:
+                    rgba: 1, 1, 1, 1
+                BorderImage:
+                    source: app.expand("{media}/misc/black.png") if root.mess_data.type == "info" else app.expand("{media}/misc/borders/{}.jpg", "blue" if root.mess_data.own_mess else "gray")
+                    pos: self.pos
+                    size: self.content_width, self.height
+            id: mess_xhtml
+            size_hint: 0.8, None
+            height: self.minimum_height
+            xhtml: root.message_xhtml or self.escape(root.message or u' ')
+            color: (0.74,0.74,0.24,1) if root.mess_data.type == "info" else (0, 0, 0, 1)
+            padding: root.mess_padding
+            bold: True if root.mess_data.type == "info" else False
+
+<Chat>:
+    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(40)
+        message_input: message_input
+        MessageInputWidget:
+            id: message_input
+            size_hint: 1, 1
+            hint_text: _(u"Enter your message here")
+            on_text_validate: root.onSend(args[0])
+        IconButton
+            # transfer 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: TransferMenu(callback=root.onTransferOK).show(self)
+
+<EncryptionButton>:
+    size_hint: None, 1
+    width: dp(30)
+    allow_stretch: True
+    source: self.getIconSource()
+
+<OtrButton@Button>:
+    size_hint: None, None
+    size: self.texture_size
+    padding: dp(5), dp(10)
+
+<OtrMenu>:
+    size_hint_x: None
+    width: start_btn.width
+    auto_width: False
+    canvas.before:
+        Color:
+            rgba: 0, 0, 0, 1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    OtrButton:
+        id: start_btn
+        text: _(u"Start/Refresh encrypted session")
+        on_release: root.otr_start()
+    OtrButton:
+        text: _(u"Finish encrypted session")
+        on_release: root.otr_end()
+    OtrButton:
+        text: _(u"Authenticate destinee")
+        on_release: root.otr_authenticate()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_chat.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,460 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from cagou.core.constants import Const as C
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.gridlayout import GridLayout
+from kivy.uix.textinput import TextInput
+from kivy.metrics import dp
+from kivy import properties
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_chat
+from sat_frontends.tools import jid
+from cagou.core import cagou_widget
+from cagou.core.image import Image
+from cagou.core.common import IconButton, JidWidget
+from kivy.uix.dropdown import DropDown
+from cagou import G
+import mimetypes
+
+
+PLUGIN_INFO = {
+    "name": _(u"chat"),
+    "main": "Chat",
+    "description": _(u"instant messaging with one person or a group"),
+    "icon_small": u"{media}/icons/muchoslava/png/chat_new_32.png",
+    "icon_medium": u"{media}/icons/muchoslava/png/chat_new_44.png"
+}
+
+# following const are here temporary, they should move to quick frontend
+OTR_STATE_UNTRUSTED = 'untrusted'
+OTR_STATE_TRUSTED = 'trusted'
+OTR_STATE_TRUST = (OTR_STATE_UNTRUSTED, OTR_STATE_TRUSTED)
+OTR_STATE_UNENCRYPTED = 'unencrypted'
+OTR_STATE_ENCRYPTED = 'encrypted'
+OTR_STATE_ENCRYPTION = (OTR_STATE_UNENCRYPTED, OTR_STATE_ENCRYPTED)
+
+
+class MessAvatar(Image):
+    pass
+
+
+class MessageWidget(GridLayout):
+    mess_data = properties.ObjectProperty()
+    mess_xhtml = properties.ObjectProperty()
+    mess_padding = (dp(5), dp(5))
+    avatar = properties.ObjectProperty()
+    delivery = properties.ObjectProperty()
+
+    def __init__(self, **kwargs):
+        # self must be registered in widgets before kv is parsed
+        kwargs['mess_data'].widgets.add(self)
+        super(MessageWidget, self).__init__(**kwargs)
+        avatar_path = self.mess_data.avatar
+        if avatar_path is not None:
+            self.avatar.source = avatar_path
+
+    @property
+    def chat(self):
+        """return parent Chat instance"""
+        return self.mess_data.parent
+
+    @property
+    def message(self):
+        """Return currently displayed message"""
+        return self.mess_data.main_message
+
+    @property
+    def message_xhtml(self):
+        """Return currently displayed message"""
+        return self.mess_data.main_message_xhtml
+
+    def widthAdjust(self):
+        """this widget grows up with its children"""
+        pass
+        # parent = self.mess_xhtml.parent
+        # padding_x = self.mess_padding[0]
+        # text_width, text_height = self.mess_xhtml.texture_size
+        # if text_width > parent.width:
+        #     self.mess_xhtml.text_size = (parent.width - padding_x, None)
+        #     self.text_max = text_width
+        # elif self.mess_xhtml.text_size[0] is not None and text_width  < parent.width - padding_x:
+        #     if text_width < self.text_max:
+        #         self.mess_xhtml.text_size = (None, None)
+        #     else:
+        #         self.mess_xhtml.text_size = (parent.width  - padding_x, None)
+
+    def update(self, update_dict):
+        if 'avatar' in update_dict:
+            self.avatar.source = update_dict['avatar']
+        if 'status' in update_dict:
+            status = update_dict['status']
+            self.delivery.text =  u'\u2714' if status == 'delivered' else u''
+
+
+class MessageInputBox(BoxLayout):
+    pass
+
+
+class MessageInputWidget(TextInput):
+
+    def _key_down(self, key, repeat=False):
+        displayed_str, internal_str, internal_action, scale = key
+        if internal_action == 'enter':
+            self.dispatch('on_text_validate')
+        else:
+            super(MessageInputWidget, self)._key_down(key, repeat)
+
+
+class MessagesWidget(GridLayout):
+    pass
+
+
+class EncryptionButton(IconButton):
+
+    def __init__(self, chat, **kwargs):
+        """
+        @param chat(Chat): Chat instance
+        """
+        self.chat = chat
+        # for now we do a simple ContextMenu as we have only OTR
+        self.otr_menu = OtrMenu(chat)
+        super(EncryptionButton, self).__init__(**kwargs)
+        self.bind(on_release=self.otr_menu.open)
+
+    def getIconSource(self):
+        """get path of icon"""
+        # TODO: use a more generic method to get icon name
+        if self.chat.otr_state_encryption == OTR_STATE_UNENCRYPTED:
+            icon_name = 'cadenas_ouvert'
+        else:
+            if self.chat.otr_state_trust == OTR_STATE_TRUSTED:
+                icon_name = 'cadenas_ferme'
+            else:
+                icon_name = 'cadenas_ferme_pas_authenthifie'
+
+        return G.host.app.expand("{media}/icons/muchoslava/png/" + icon_name + "_30.png")
+
+
+class OtrMenu(DropDown):
+
+    def __init__(self, chat, **kwargs):
+        """
+        @param chat(Chat): Chat instance
+        """
+        self.chat = chat
+        super(OtrMenu, self).__init__(**kwargs)
+
+    def otr_start(self):
+        self.dismiss()
+        G.host.launchMenu(
+            C.MENU_SINGLE,
+            (u"otr", u"start/refresh"),
+            {u'jid': unicode(self.chat.target)},
+            None,
+            C.NO_SECURITY_LIMIT,
+            self.chat.profile
+            )
+
+    def otr_end(self):
+        self.dismiss()
+        G.host.launchMenu(
+            C.MENU_SINGLE,
+            (u"otr", u"end session"),
+            {u'jid': unicode(self.chat.target)},
+            None,
+            C.NO_SECURITY_LIMIT,
+            self.chat.profile
+            )
+
+    def otr_authenticate(self):
+        self.dismiss()
+        G.host.launchMenu(
+            C.MENU_SINGLE,
+            (u"otr", u"authenticate"),
+            {u'jid': unicode(self.chat.target)},
+            None,
+            C.NO_SECURITY_LIMIT,
+            self.chat.profile
+            )
+
+
+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)
+        self.otr_state_encryption = OTR_STATE_UNENCRYPTED
+        self.otr_state_trust = OTR_STATE_UNTRUSTED
+        cagou_widget.CagouWidget.__init__(self)
+        if type_ == C.CHAT_ONE2ONE:
+            self.encryption_btn = EncryptionButton(self)
+            self.headerInputAddExtra(self.encryption_btn)
+        self.header_input.hint_text = u"{}".format(target)
+        self.host.addListener('progressError', self.onProgressError, profiles)
+        self.host.addListener('progressFinished', self.onProgressFinished, profiles)
+        self._waiting_pids = {}  # waiting progress ids
+        self.postInit()
+        # completion attribtues
+        self._hi_comp_data = None
+        self._hi_comp_last = None
+        self._hi_comp_dropdown = DropDown()
+        self._hi_comp_allowed = True
+
+    @classmethod
+    def factory(cls, plugin_info, target, profiles):
+        profiles = list(profiles)
+        if len(profiles) > 1:
+            raise NotImplementedError(u"Multi-profiles is not available yet for chat")
+        if target is None:
+            target = G.host.profiles[profiles[0]].whoami
+        return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=profiles)
+
+    ## header ##
+
+    def changeWidget(self, jid_):
+        """change current widget for a new one with given jid
+
+        @param jid_(jid.JID): jid of the widget to create
+        """
+        plugin_info = G.host.getPluginInfo(main=Chat)
+        factory = plugin_info['factory']
+        G.host.switchWidget(self, factory(plugin_info, jid_, profiles=[self.profile]))
+        self.header_input.text = ''
+
+    def onHeaderInput(self):
+        text = self.header_input.text.strip()
+        try:
+            if text.count(u'@') != 1 or text.count(u' '):
+                raise ValueError
+            jid_ = jid.JID(text)
+        except ValueError:
+            log.info(u"entered text is not a jid")
+            return
+
+        def discoCb(disco):
+            # TODO: check if plugin XEP-0045 is activated
+            if "conference" in [i[0] for i in disco[1]]:
+                G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, callback=self._mucJoinCb, errback=self._mucJoinEb)
+            else:
+                self.changeWidget(jid_)
+
+        def discoEb(failure):
+            log.warning(u"Disco failure, ignore this text: {}".format(failure))
+
+        G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, errback=discoEb)
+
+    def onHeaderInputCompleted(self, input_wid, completed_text):
+        self._hi_comp_allowed = False
+        input_wid.text = completed_text
+        self._hi_comp_allowed = True
+        self._hi_comp_dropdown.dismiss()
+        self.onHeaderInput()
+
+    def onHeaderInputComplete(self, wid, text):
+        if not self._hi_comp_allowed:
+            return
+        text = text.lstrip()
+        if not text:
+            self._hi_comp_data = None
+            self._hi_comp_last = None
+            return
+
+        profile = list(self.profiles)[0]
+
+        if self._hi_comp_data is None:
+            # first completion, we build the initial list
+            comp_data = self._hi_comp_data = []
+            self._hi_comp_last = ''
+            for jid_, jid_data in G.host.contact_lists[profile].all_iter:
+                comp_data.append((jid_, jid_data))
+            comp_data.sort(key=lambda datum: datum[0])
+        else:
+            comp_data = self._hi_comp_data
+
+        # XXX: dropdown is rebuilt each time backspace is pressed or if the text is changed,
+        #      it works OK, but some optimisation may be done here
+        dropdown = self._hi_comp_dropdown
+
+        if not text.startswith(self._hi_comp_last) or not self._hi_comp_last:
+            # text has changed or backspace has been pressed, we restart
+            dropdown.clear_widgets()
+
+            for jid_, jid_data in comp_data:
+                nick = jid_data.get(u'nick', u'')
+                if text in jid_.bare or text in nick.lower():
+                    btn = JidWidget(
+                        jid = jid_.bare,
+                        profile = profile,
+                        size_hint = (0.5, None),
+                        nick = nick,
+                        on_release=lambda dummy, txt=jid_.bare: self.onHeaderInputCompleted(wid, txt)
+                        )
+                    dropdown.add_widget(btn)
+        else:
+            # more chars, we continue completion by removing unwanted widgets
+            to_remove = []
+            for c in dropdown.children[0].children:
+                if text not in c.jid and text not in (c.nick or ''):
+                    to_remove.append(c)
+            for c in to_remove:
+                dropdown.remove_widget(c)
+
+        dropdown.open(wid)
+        self._hi_comp_last = text
+
+    def messageDataConverter(self, idx, mess_id):
+        return {"mess_data": self.messages[mess_id]}
+
+    def _onHistoryPrinted(self):
+        """Refresh or scroll down the focus after the history is printed"""
+        # self.adapter.data = self.messages
+        for mess_data in self.messages.itervalues():
+            self.appendMessage(mess_data)
+        super(Chat, self)._onHistoryPrinted()
+
+    def createMessage(self, message):
+        self.appendMessage(message)
+
+    def appendMessage(self, mess_data):
+        self.messages_widget.add_widget(MessageWidget(mess_data=mess_data))
+
+    def onSend(self, input_widget):
+        G.host.messageSend(
+            self.target,
+            {'': input_widget.text}, # TODO: handle language
+            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
+            profile_key=self.profile
+            )
+        input_widget.text = ''
+
+    def onProgressFinished(self, progress_id, metadata, profile):
+        try:
+            callback, cleaning_cb = self._waiting_pids.pop(progress_id)
+        except KeyError:
+            return
+        if cleaning_cb is not None:
+            cleaning_cb()
+        callback(metadata, profile)
+
+    def onProgressError(self, progress_id, err_msg, profile):
+        try:
+            dummy, cleaning_cb = self._waiting_pids[progress_id]
+        except KeyError:
+            return
+        else:
+            del self._waiting_pids[progress_id]
+            if cleaning_cb is not None:
+                cleaning_cb()
+        # TODO: display message to user
+        log.warning(u"Can't transfer file: {}".format(err_msg))
+
+    def fileTransferDone(self, metadata, profile):
+        log.debug("file transfered: {}".format(metadata))
+        extra = {}
+
+        # FIXME: Q&D way of getting file type, upload plugins shouls give it
+        mime_type = mimetypes.guess_type(metadata['url'])[0]
+        if mime_type is not None:
+            if mime_type.split(u'/')[0] == 'image':
+                # we generate url ourselves, so this formatting is safe
+                extra['xhtml'] = u"<img src='{url}' />".format(**metadata)
+
+        G.host.messageSend(
+            self.target,
+            {'': metadata['url']},
+            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+            extra = extra,
+            profile_key=profile
+            )
+
+    def fileTransferCb(self, progress_data, cleaning_cb):
+        try:
+            progress_id = progress_data['progress']
+        except KeyError:
+            xmlui = progress_data['xmlui']
+            G.host.showUI(xmlui)
+        else:
+            self._waiting_pids[progress_id] = (self.fileTransferDone, cleaning_cb)
+
+    def onTransferOK(self, file_path, cleaning_cb, transfer_type):
+        if transfer_type == C.TRANSFER_UPLOAD:
+            G.host.bridge.fileUpload(
+                file_path,
+                "",
+                "",
+                {"ignore_tls_errors": C.BOOL_TRUE},  # FIXME: should not be the default
+                self.profile,
+                callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb)
+                )
+        elif transfer_type == C.TRANSFER_SEND:
+            if self.type == C.CHAT_GROUP:
+                log.warning(u"P2P transfer is not possible for group chat")
+                # TODO: show an error dialog to user, or better hide the send button for MUC
+            else:
+                jid_ = self.target
+                if not jid_.resource:
+                    jid_ = G.host.contact_lists[self.profile].getFullJid(jid_)
+                G.host.bridge.fileSend(unicode(jid_), file_path, "", "", profile=self.profile)
+                # TODO: notification of sending/failing
+        else:
+            raise log.error(u"transfer of type {} are not handled".format(transfer_type))
+
+
+    def _mucJoinCb(self, joined_data):
+        joined, room_jid_s, occupants, user_nick, subject, profile = joined_data
+        self.host.mucRoomJoinedHandler(*joined_data[1:])
+        jid_ = jid.JID(room_jid_s)
+        self.changeWidget(jid_)
+
+    def _mucJoinEb(self, failure):
+        log.warning(u"Can't join room: {}".format(failure))
+
+    def _onDelete(self):
+        self.host.removeListener('progressFinished', self.onProgressFinished)
+        self.host.removeListener('progressError', self.onProgressError)
+        return super(Chat, self).onDelete()
+
+    def onOTRState(self, state, dest_jid, profile):
+        assert profile in self.profiles
+        if state in OTR_STATE_ENCRYPTION:
+            self.otr_state_encryption = state
+        elif state in OTR_STATE_TRUST:
+            self.otr_state_trust = state
+        else:
+            log.error(_(u"Unknown OTR state received: {}".format(state)))
+            return
+        self.encryption_btn.source = self.encryption_btn.getIconSource()
+
+    def onDelete(self, force=False):
+        if force==True:
+            return self._onDelete()
+        if len(list(G.host.widgets.getWidgets(self.__class__, self.target, profiles=self.profiles))) > 1:
+            # we don't keep duplicate widgets
+            return self._onDelete()
+        return False
+
+
+PLUGIN_INFO["factory"] = Chat.factory
+quick_widgets.register(quick_chat.QuickChat, Chat)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_contact_list.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,34 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+<ContactListView>:
+    row_height: dp(50)
+
+<ContactItem>:
+    padding: dp(10), dp(3)
+    size_hint: 1, None
+    height: dp(50)
+    Avatar:
+        source: root.data.get('avatar', app.default_avatar)
+        size_hint: None, 1
+        width: dp(60)
+    Label:
+        id: jid_label
+        padding: dp(5), 0
+        text: root.jid
+        text_size: self.size
+        bold: True
+        valign: "middle"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_contact_list.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
+from sat_frontends.tools import jid
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.listview import ListView
+from kivy.adapters.listadapter import ListAdapter
+from kivy.metrics import dp
+from kivy import properties
+from cagou.core import cagou_widget
+from cagou.core import image
+from cagou import G
+
+
+PLUGIN_INFO = {
+    "name": _(u"contacts"),
+    "main": "ContactList",
+    "description": _(u"list of contacts"),
+    "icon_small": u"{media}/icons/muchoslava/png/contact_list_new_32.png",
+    "icon_medium": u"{media}/icons/muchoslava/png/contact_list_new_44.png"
+}
+
+
+class Avatar(image.Image):
+    pass
+
+
+class ContactItem(BoxLayout):
+    data = properties.DictProperty()
+    jid = properties.StringProperty('')
+
+    def __init__(self, **kwargs):
+        super(ContactItem, self).__init__(**kwargs)
+
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            # XXX: for now clicking on an item launch the corresponding Chat widget
+            #      behaviour should change in the future
+            try:
+                # FIXME: Q&D way to get chat plugin, should be replaced by a clean method
+                #        in host
+                plg_infos = [p for p in G.host.getPluggedWidgets() if 'chat' in p['import_name']][0]
+            except IndexError:
+                log.warning(u"No plugin widget found to display chat")
+            else:
+                factory = plg_infos['factory']
+                G.host.switchWidget(self, factory(plg_infos, jid.JID(self.jid), profiles=iter(G.host.profiles)))
+
+
+class ContactListView(ListView):
+    pass
+
+
+class ContactList(QuickContactList, cagou_widget.CagouWidget):
+
+    def __init__(self, host, target, profiles):
+        QuickContactList.__init__(self, G.host, profiles)
+        cagou_widget.CagouWidget.__init__(self)
+        self.adapter = ListAdapter(data={},
+                                   cls=ContactItem,
+                                   args_converter=self.contactDataConverter,
+                                   selection_mode='multiple',
+                                   allow_empty_selection=True,
+                                  )
+        self.add_widget(ContactListView(adapter=self.adapter))
+        self.postInit()
+        self.update()
+
+    def onHeaderInputComplete(self, wid, text):
+        # FIXME: this is implementation dependent, need to be done properly
+        items = self.children[0].children[0].children[0].children
+
+        for item in items:
+            if text not in item.ids.jid_label.text:
+                item.height = 0
+                item.opacity = 0
+            else:
+                item.height = dp(50)
+                item.opacity = 1
+
+    def contactDataConverter(self, idx, bare_jid):
+        return {"jid": bare_jid, "data": self._items_cache[bare_jid]}
+
+    def update(self, entities=None, type_=None, profile=None):
+        log.debug("update: %s %s %s" % (entities, type_, profile))
+        # FIXME: for now we update on each event
+        # if entities is None and type_ is None:
+        self._items_cache = self.items_sorted
+        self.adapter.data = self.items_sorted.keys()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_settings.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,15 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_settings.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from sat.core.constants import Const as C
+from sat_frontends.quick_frontend import quick_widgets
+from kivy.uix.label import Label
+from kivy.uix.widget import Widget
+from cagou.core import cagou_widget
+from cagou import G
+
+
+PLUGIN_INFO = {
+    "name": _(u"settings"),
+    "main": "Settings",
+    "description": _(u"Cagou/SàT settings"),
+    "icon_small": u"{media}/icons/muchoslava/png/settings_32.png",
+    "icon_medium": u"{media}/icons/muchoslava/png/settings_44.png"
+}
+
+
+class Settings(quick_widgets.QuickWidget, cagou_widget.CagouWidget):
+
+    def __init__(self, host, target, profiles):
+        quick_widgets.QuickWidget.__init__(self, G.host, target, profiles)
+        cagou_widget.CagouWidget.__init__(self)
+        # the Widget() avoid CagouWidget header to be down at the beginning
+        # then up when the UI is loaded
+        self.loading_widget = Widget()
+        self.add_widget(self.loading_widget)
+        G.host.bridge.getParamsUI(-1, C.APP_NAME, self.profile, callback=self.getParamsUICb, errback=self.getParamsUIEb)
+
+    def changeWidget(self, widget):
+        self.clear_widgets([self.loading_widget])
+        del self.loading_widget
+        self.add_widget(widget)
+
+    def getParamsUICb(self, xmlui):
+        G.host.actionManager({"xmlui": xmlui}, ui_show_cb=self.changeWidget, profile=self.profile)
+
+    def getParamsUIEb(self, failure):
+        self.changeWidget(Label(
+            text=_(u"Can't load parameters!"),
+            bold=True,
+            color=(1,0,0,1)))
+        G.host.showDialog(u"Can't load params UI", failure, "error")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_widget_selector.kv	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,30 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+<WidgetSelItem>:
+    size_hint: (1, None)
+    height: dp(40)
+    Widget:
+    Image:
+        source: root.plugin_info["icon_medium"]
+        allow_stretch: True
+        keep_ratio: True
+        width: self.texture_size[0]
+    Label:
+        text: root.plugin_info["name"]
+        bold: True
+        font_size: sp(20)
+    Widget:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_widget_selector.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,80 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from cagou.core.constants import Const as C
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.listview import ListView
+from kivy.adapters.listadapter import ListAdapter
+from kivy import properties
+from kivy.uix.behaviors import ButtonBehavior
+from cagou.core import cagou_widget
+from cagou import G
+
+
+PLUGIN_INFO = {
+    "name": _(u"widget selector"),
+    "import_name": C.WID_SELECTOR,
+    "main": "WidgetSelector",
+    "description": _(u"show available widgets and allow to select one"),
+    "icon_small": u"{media}/icons/muchoslava/png/selector_new_32.png",
+    "icon_medium": u"{media}/icons/muchoslava/png/selector_new_44.png"
+}
+
+
+class WidgetSelItem(ButtonBehavior, BoxLayout):
+    plugin_info = properties.DictProperty()
+
+    def __init__(self, **kwargs):
+        super(WidgetSelItem, self).__init__(**kwargs)
+
+    def select(self, *args):
+        log.debug(u"widget selection: {}".format(self.plugin_info["name"]))
+        factory = self.plugin_info["factory"]
+        G.host.switchWidget(self, factory(self.plugin_info, None, profiles=iter(G.host.profiles)))
+
+    def deselect(self, *args):
+        pass
+
+
+class WidgetSelector(cagou_widget.CagouWidget):
+
+    def __init__(self):
+        super(WidgetSelector, self).__init__()
+        self.adapter = ListAdapter(
+            data=G.host.getPluggedWidgets(except_cls=self.__class__),
+            cls=WidgetSelItem,
+            args_converter=self.dataConverter,
+            selection_mode='single',
+            allow_empty_selection=True,
+           )
+        self.add_widget(ListView(adapter=self.adapter))
+
+    @classmethod
+    def factory(cls, plugin_info, target, profiles):
+        return cls()
+
+    def dataConverter(self, idx, plugin_info):
+        return {"plugin_info": plugin_info}
+
+
+PLUGIN_INFO["factory"] = WidgetSelector.factory
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/main.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,41 @@
+#!/usr//bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+import sys
+import os
+# we want the service to access the modules from parent dir (sat, etc.)
+os.chdir('..')
+sys.path.insert(0, '')
+from sat.core.constants import Const as C
+from sat.core import log_config
+# SàT log conf must be done before calling Kivy
+log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
+# if this module is called, we should be on android,
+# but just in case...
+from kivy import utils as kivy_utils
+if kivy_utils.platform == "android":
+    # sys.platform is "linux" on android by default
+    # so we change it to allow backend to detect android
+    sys.platform = "android"
+    C.PLUGIN_EXT = "pyo"
+from sat.core import sat_main
+from twisted.internet import reactor
+
+sat = sat_main.SAT()
+reactor.run()
--- a/src/buildozer.spec	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,236 +0,0 @@
-[app]
-
-# (str) Title of your application
-title = Cagou
-
-# (str) Package name
-package.name = cagou
-
-# (str) Package domain (needed for android/ios packaging)
-package.domain = org.goffi.cagou
-
-# (str) Source code where the main.py live
-source.dir = .
-
-# (list) Source files to include (let empty to include all the files)
-source.include_exts = py,png,jpg,kv,atlas,conf
-# FIXME: check if we can do sat.conf only, without every .conf
-
-# (list) List of inclusions using pattern matching
-#source.include_patterns = assets/*,images/*.png
-source.include_patterns = media
-
-# (list) Source files to exclude (let empty to not exclude anything)
-#source.exclude_exts = spec
-
-# (list) List of directory to exclude (let empty to not exclude anything)
-#source.exclude_dirs = tests, bin
-
-# (list) List of exclusions using pattern matching
-#source.exclude_patterns = license,images/*/*.jpg
-
-# (str) Application versioning (method 1)
-version = 0.1
-
-# (str) Application versioning (method 2)
-# version.regex = __version__ = ['"](.*)['"]
-# version.filename = %(source.dir)s/main.py
-
-# (list) Application requirements
-# comma seperated e.g. requirements = sqlite3,kivy
-requirements = kivy, sqlite3, twisted, wokkel, pil, lxml, pyxdg, markdown, html2text, python-dateutil, pycrypto, pyopenssl, plyer, potr
-
-# (str) Custom source folders for requirements
-# Sets custom source for any requirements with recipes
-# requirements.source.kivy = ../../kivy
-
-# (list) Garden requirements
-#garden_requirements =
-
-# (str) Presplash of the application
-presplash.filename = %(source.dir)s/media/icons/muchoslava/png/cagou_profil_bleu_512.png
-
-# (str) Icon of the application
-icon.filename = %(source.dir)s/media/icons/muchoslava/png/cagou_profil_bleu_96.png
-
-# (str) Supported orientation (one of landscape, portrait or all)
-orientation = portrait
-
-# (list) List of service to declare
-#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
-
-#
-# OSX Specific
-#
-
-#
-# author = © Copyright Info
-
-#
-# Android specific
-#
-
-# (bool) Indicate if the application should be fullscreen or not
-fullscreen = 1
-
-# (list) Permissions
-android.permissions = INTERNET, ACCESS_NETWORK_STATE, VIBRATE, RECORD_AUDIO
-
-# (int) Android API to use
-#android.api = 19
-
-# (int) Minimum API required
-#android.minapi = 9
-
-# (int) Android SDK version to use
-#android.sdk = 20
-
-# (str) Android NDK version to use
-#android.ndk = 9c
-
-# (bool) Use --private data storage (True) or --dir public storage (False)
-#android.private_storage = True
-
-# (str) Android NDK directory (if empty, it will be automatically downloaded.)
-#android.ndk_path =
-
-# (str) Android SDK directory (if empty, it will be automatically downloaded.)
-#android.sdk_path =
-
-# (str) ANT directory (if empty, it will be automatically downloaded.)
-#android.ant_path =
-
-# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
-# we use our own p4a and mount in root dir on docker image
-android.p4a_dir = /python-for-android-old
-
-# (str) The directory in which python-for-android should look for your own build recipes (if any)
-#p4a.local_recipes =
-
-# (list) python-for-android whitelist
-#android.p4a_whitelist =
-
-# (bool) If True, then skip trying to update the Android sdk
-# This can be useful to avoid excess Internet downloads or save time
-# when an update is due and you just want to test/build your package
-# android.skip_update = False
-
-# (str) Bootstrap to use for android builds (android_new only)
-# android.bootstrap = sdl2
-
-# (str) Android entry point, default is ok for Kivy-based app
-#android.entrypoint = org.renpy.android.PythonActivity
-
-# (list) List of Java .jar files to add to the libs so that pyjnius can access
-# their classes. Don't add jars that you do not need, since extra jars can slow
-# down the build process. Allows wildcards matching, for example:
-# OUYA-ODK/libs/*.jar
-#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
-
-# (list) List of Java files to add to the android project (can be java or a
-# directory containing the files)
-#android.add_src =
-
-# (str) python-for-android branch to use, if not master, useful to try
-# not yet merged features.
-#android.branch = master
-
-# (str) OUYA Console category. Should be one of GAME or APP
-# If you leave this blank, OUYA support will not be enabled
-#android.ouya.category = GAME
-
-# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
-#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
-
-# (str) XML file to include as an intent filters in <activity> tag
-#android.manifest.intent_filters =
-
-# (list) Android additionnal libraries to copy into libs/armeabi
-#android.add_libs_armeabi = libs/android/*.so
-#android.add_libs_armeabi_v7a = libs/android-v7/*.so
-#android.add_libs_x86 = libs/android-x86/*.so
-#android.add_libs_mips = libs/android-mips/*.so
-
-# (bool) Indicate whether the screen should stay on
-# Don't forget to add the WAKE_LOCK permission if you set this to True
-#android.wakelock = False
-
-# (list) Android application meta-data to set (key=value format)
-#android.meta_data =
-
-# (list) Android library project to add (will be added in the
-# project.properties automatically.)
-#android.library_references =
-
-# (str) Android logcat filters to use
-#android.logcat_filters = *:S python:D
-
-# (bool) Copy library instead of making a libpymodules.so
-#android.copy_libs = 1
-
-#
-# iOS specific
-#
-
-# (str) Path to a custom kivy-ios folder
-#ios.kivy_ios_dir = ../kivy-ios
-
-# (str) Name of the certificate to use for signing the debug version
-# Get a list of available identities: buildozer ios list_identities
-#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
-
-# (str) Name of the certificate to use for signing the release version
-#ios.codesign.release = %(ios.codesign.debug)s
-
-
-[buildozer]
-
-# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
-log_level = 1
-
-# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
-warn_on_root = 1
-
-# (str) Path to build artifact storage, absolute or relative to spec file
-# build_dir = ./.buildozer
-
-# (str) Path to build output (i.e. .apk, .ipa) storage
-# bin_dir = ./bin
-
-#    -----------------------------------------------------------------------------
-#    List as sections
-#
-#    You can define all the "list" as [section:key].
-#    Each line will be considered as a option to the list.
-#    Let's take [app] / source.exclude_patterns.
-#    Instead of doing:
-#
-#[app]
-#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
-#
-#    This can be translated into:
-#
-#[app:source.exclude_patterns]
-#license
-#data/audio/*.wav
-#data/images/original/*
-#
-
-
-#    -----------------------------------------------------------------------------
-#    Profiles
-#
-#    You can extend section / key with a profile
-#    For example, you want to deploy a demo version of your application without
-#    HD content. You could first change the title to add "(demo)" in the name
-#    and extend the excluded directories to remove the HD content.
-#
-#[app@demo]
-#title = My Application (demo)
-#
-#[app:source.exclude_patterns@demo]
-#images/hd/*
-#
-#    Then, invoke the command line with the "demo" profile:
-#
-#buildozer --profile demo android debug
--- a/src/cagou/__init__.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-#!/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/>.
-
-
-class Global(object):
-    @property
-    def host(self):
-        return self._host
-G = Global()
-
-
-from core import cagou_main
-
-
-def run():
-    host = G._host = cagou_main.Cagou()
-    host.run()
--- a/src/cagou/core/cagou_main.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,666 +0,0 @@
-#!/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 _
-from . import kivy_hack
-kivy_hack.do_hack()
-from constants import Const as C
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core import exceptions
-from sat_frontends.quick_frontend.quick_app import QuickApp
-from sat_frontends.quick_frontend import quick_widgets
-from sat_frontends.quick_frontend import quick_chat
-from sat_frontends.quick_frontend import quick_utils
-from sat.tools import config
-from sat.tools.common import dynamic_import
-import kivy
-kivy.require('1.9.1')
-import kivy.support
-main_config = config.parseMainConf()
-bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus')
-# FIXME: event loop is choosen according to bridge_name, a better way should be used
-if 'dbus' in bridge_name:
-    kivy.support.install_gobject_iteration()
-elif bridge_name in ('pb', 'embedded'):
-    kivy.support.install_twisted_reactor()
-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.floatlayout import FloatLayout
-from kivy.uix.screenmanager import ScreenManager, Screen, FallOutTransition, RiseInTransition
-from kivy.uix.dropdown import DropDown
-from cagou_widget import CagouWidget
-from . import widgets_handler
-from .common import IconButton
-from . import menu
-from importlib import import_module
-import os.path
-import glob
-import cagou.plugins
-import cagou.kv
-from kivy import utils as kivy_utils
-import sys
-if kivy_utils.platform == "android":
-    # FIXME: move to separate android module
-    kivy.support.install_android()
-    # sys.platform is "linux" on android by default
-    # so we change it to allow backend to detect android
-    sys.platform = "android"
-    import mmap
-    C.PLUGIN_EXT = 'pyo'
-
-
-class NotifsIcon(IconButton):
-    notifs = properties.ListProperty()
-
-    def on_release(self):
-        callback, args, kwargs = self.notifs.pop(0)
-        callback(*args, **kwargs)
-
-    def addNotif(self, callback, *args, **kwargs):
-        self.notifs.append((callback, args, kwargs))
-
-
-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()
-    notifs_icon = 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)
-
-    def addNotif(self, callback, *args, **kwargs):
-        self.notifs_icon.addNotif(callback, *args, **kwargs)
-
-    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 addNotifUI(self, ui):
-        self.notifs_icon.addNotif(ui.show, force=True)
-
-    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 RootMenus(menu.MenusWidget):
-    pass
-
-
-class RootBody(BoxLayout):
-    pass
-
-
-class CagouRootWidget(FloatLayout):
-    root_menus = properties.ObjectProperty()
-    root_body = properties.ObjectProperty
-
-    def __init__(self, main_widget):
-        super(CagouRootWidget, self).__init__()
-        # header
-        self._head_widget = RootHeadWidget()
-        self.root_body.add_widget(self._head_widget)
-        # body
-        self._manager = ScreenManager()
-        # main widgets
-        main_screen = Screen(name='main')
-        main_screen.add_widget(main_widget)
-        self._manager.add_widget(main_screen)
-        # backend XMLUI (popups, forms, etc)
-        xmlui_screen = Screen(name='xmlui')
-        self._manager.add_widget(xmlui_screen)
-        # extra (file chooser, audio record, etc)
-        extra_screen = Screen(name='extra')
-        self._manager.add_widget(extra_screen)
-        self.root_body.add_widget(self._manager)
-
-    def changeWidget(self, widget, screen_name="main"):
-        """change main widget"""
-        if self._manager.transition.is_active:
-            # FIXME: workaround for what seems a Kivy bug
-            # TODO: report this upstream
-            self._manager.transition.stop()
-        screen = self._manager.get_screen(screen_name)
-        screen.clear_widgets()
-        screen.add_widget(widget)
-
-    def show(self, screen="main"):
-        if self._manager.transition.is_active:
-            # FIXME: workaround for what seems a Kivy bug
-            # TODO: report this upstream
-            self._manager.transition.stop()
-        if self._manager.current == screen:
-            return
-        if screen == "main":
-            self._manager.transition = FallOutTransition()
-        else:
-            self._manager.transition = RiseInTransition()
-        self._manager.current = screen
-
-    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)
-
-    def addNotifUI(self, ui):
-        self._head_widget.addNotifUI(ui)
-
-
-class CagouApp(App):
-    """Kivy App for Cagou"""
-
-    def build(self):
-        return CagouRootWidget(Label(text=u"Loading please wait"))
-
-    def showWidget(self):
-        self._profile_manager = ProfileManager()
-        self.root.changeWidget(self._profile_manager)
-
-    def expand(self, path, *args, **kwargs):
-        """expand path and replace known values
-
-        useful in kv. Values which can be used:
-            - {media}: media dir
-        @param path(unicode): path to expand
-        @param *args: additional arguments used in format
-        @param **kwargs: additional keyword arguments used in format
-        """
-        return os.path.expanduser(path).format(*args, media=self.host.media_dir, **kwargs)
-
-    def on_start(self):
-        if sys.platform == "android":
-            # XXX: we use memory map instead of bridge because if we try to call a bridge method
-            #      in on_pause method, the call data  is not written before the actual pause
-            # we create a memory map on .cagou_status file with a 1 byte status
-            # status is:
-            # R => running
-            # P => paused
-            # S => stopped
-            self._first_pause = True
-            self.cagou_status_fd = open('.cagou_status', 'wb+')
-            self.cagou_status_fd.write('R')
-            self.cagou_status_fd.flush()
-            self.cagou_status = mmap.mmap(self.cagou_status_fd.fileno(), 1, prot=mmap.PROT_WRITE)
-
-    def on_pause(self):
-        self.cagou_status[0] = 'P'
-        return True
-
-    def on_resume(self):
-        self.cagou_status[0] = 'R'
-
-    def on_stop(self):
-        if sys.platform == "android":
-            self.cagou_status[0] = 'S'
-            self.cagou_status.flush()
-            self.cagou_status_fd.close()
-
-
-class Cagou(QuickApp):
-    MB_HANDLE = False
-
-    def __init__(self):
-        if bridge_name == 'embedded':
-            from sat.core import sat_main
-            self.sat = sat_main.SAT()
-        if sys.platform == 'android':
-            from android import AndroidService
-            service = AndroidService(u'Cagou (SàT)'.encode('utf-8'), u'Salut à Toi backend'.encode('utf-8'))
-            service.start(u'service started')
-            self.service = service
-
-        bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
-        if bridge_module is None:
-            log.error(u"Can't import {} bridge".format(bridge_name))
-            sys.exit(3)
-        else:
-            log.debug(u"Loading {} bridge".format(bridge_name))
-        super(Cagou, self).__init__(bridge_factory=bridge_module.Bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False)
-        self._import_kv()
-        self.app = CagouApp()
-        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.app.icon = os.path.join(self.media_dir, "icons/muchoslava/png/cagou_profil_bleu_96.png")
-        self._plg_wids = []  # main widgets plugins
-        self._plg_wids_transfer = []  # transfer widgets plugins
-        self._import_plugins()
-        self._visible_widgets = {}  # visible widgets by classes
-
-    @property
-    def visible_widgets(self):
-        for w_list in self._visible_widgets.itervalues():
-            for w in w_list:
-                yield w
-
-    def onBridgeConnected(self):
-        self.registerSignal("otrState", iface="plugin")
-        self.bridge.getReady(self.onBackendReady)
-
-    def _bridgeEb(self, failure):
-        if bridge_name == "pb" and sys.platform == "android":
-            try:
-                self.retried += 1
-            except AttributeError:
-                self.retried = 1
-            from twisted.internet.error import ConnectionRefusedError
-            if failure.check(ConnectionRefusedError) and self.retried < 100:
-                if self.retried % 20 == 0:
-                    log.debug("backend not ready, retrying ({})".format(self.retried))
-                Clock.schedule_once(lambda dummy: self.connectBridge(), 0.05)
-                return
-        super(Cagou, self)._bridgeEb(failure)
-
-    def run(self):
-        self.connectBridge()
-        self.app.bind(on_stop=self.onStop)
-        self.app.run()
-
-    def onStop(self, obj):
-        try:
-            sat_instance = self.sat
-        except AttributeError:
-            pass
-        else:
-            sat_instance.stopService()
-
-    def onBackendReady(self):
-        self.app.showWidget()
-        self.postInit()
-
-    def postInit(self, dummy=None):
-        # FIXME: resize seem to bug on android, so we use below_target for now
-        self.app.root_window.softinput_mode = "below_target"
-        profile_manager = self.app._profile_manager
-        del self.app._profile_manager
-        super(Cagou, self).postInit(profile_manager)
-
-    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 _defaultFactoryTransfer(self, plugin_info, callback, cancel_cb, profiles):
-        """default factory used to create transfer widgets instances
-
-        @param plugin_info(dict): plugin datas
-        @param callback(callable): method to call with path to file to transfer
-        @param cancel_cb(callable): call when transfer is cancelled
-            transfer 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):
-        """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__)
-        plugin_glob = u"plugin*." + C.PLUGIN_EXT
-        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_main = set()  # used to avoid loading 2 times plugin with same import name
-        imported_names_transfer = 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_TRANSFER:
-                imported_names = imported_names_transfer
-                default_factory = self._defaultFactoryTransfer
-            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 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:
-                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:
-                plugin_info['kv_file'] = u'{}.kv'.format(plug)
-            kv_path = os.path.join(plugins_path, plugin_info['kv_file'])
-            if not os.path.exists(kv_path):
-                log.debug(u"no kv found for {plugin_file}".format(**plugin_info))
-            else:
-                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'] = default_factory
-
-            # 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
-
-            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_transfer.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 _getPluginsSet(self, type_):
-        if type_ == C.PLUG_TYPE_WID:
-            return self._plg_wids
-        elif type_ == C.PLUG_TYPE_TRANSFER:
-            return self._plg_wids_transfer
-        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
-        """
-        plugins_set = self._getPluginsSet(type_)
-        for plugin_data in plugins_set:
-            if plugin_data['main'] == except_cls:
-                continue
-            yield plugin_data
-
-    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
-        """
-        plugins_set = self._getPluginsSet(type_)
-        for plugin_info in plugins_set:
-            for k, w in kwargs.iteritems():
-                try:
-                    if plugin_info[k] != w:
-                        continue
-                except KeyError:
-                    continue
-                return plugin_info
-
-    ## widgets handling
-
-    def newWidget(self, widget):
-        log.debug(u"new widget created: {}".format(widget))
-        if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
-            self.addNote(u"", _(u"room {} has been joined").format(widget.target))
-
-    def getParentHandler(self, widget):
-        """Return handler holding this widget
-
-        @return (WidgetsHandler): handler
-        """
-        w_handler = widget.parent
-        while w_handler and not(isinstance(w_handler, widgets_handler.WidgetsHandler)):
-            w_handler = w_handler.parent
-        return w_handler
-
-    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:
-            raise exceptions.InternalError(u"no CagouWidget found when trying to switch widget")
-        handler = self.getParentHandler(to_change)
-        handler.changeWidget(new)
-
-    def addVisibleWidget(self, widget):
-        """declare a widget visible
-
-        for internal use only!
-        """
-        assert isinstance(widget, quick_widgets.QuickWidget)
-        self._visible_widgets.setdefault(widget.__class__, []).append(widget)
-
-    def removeVisibleWidget(self, widget):
-        """declare a widget not visible anymore
-
-        for internal use only!
-        """
-        self._visible_widgets[widget.__class__].remove(widget)
-        self.widgets.deleteWidget(widget)
-
-    def getVisibleList(self, cls):
-        """get list of visible widgets for a given class
-
-        @param cls(QuickWidget class): type of widgets to get
-        @return (list[QuickWidget class]): visible widgets of this class
-        """
-        try:
-            return self._visible_widgets[cls]
-        except KeyError:
-            return []
-
-    def getOrClone(self, widget):
-        """Get a QuickWidget if it has not parent set else clone it"""
-        if widget.parent is None:
-            return widget
-        targets = list(widget.targets)
-        w = self.widgets.getOrCreateWidget(widget.__class__, targets[0], on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=widget.profiles)
-        for t in targets[1:]:
-            w.addTarget(t)
-        return w
-
-    ## menus ##
-
-    def _menusGetCb(self, backend_menus):
-        main_menu = self.app.root.root_menus
-        self.menus.addMenus(backend_menus)
-        self.menus.addMenu(C.MENU_GLOBAL, (_(u"Help"), _(u"About")), callback=main_menu.onAbout)
-        main_menu.update(C.MENU_GLOBAL)
-
-    ## bridge handlers ##
-
-    def otrStateHandler(self, state, dest_jid, profile):
-        """OTR state has changed for on destinee"""
-        # XXX: this method could be in QuickApp but it's here as
-        #      it's only used by Cagou so far
-        for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)):
-            widget.onOTRState(state, dest_jid, profile)
-
-    ## misc ##
-
-    def plugging_profiles(self):
-        self.app.root.changeWidget(WidgetsHandler())
-        self.bridge.menusGet("", C.NO_SECURITY_LIMIT, callback=self._menusGetCb)
-
-    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=C.XMLUI_DATA_LVL_INFO):
-        """add a note (message which disappear) to root widget's header"""
-        self.app.root.addNote(title, message, level)
-
-    def addNotifUI(self, ui):
-        """add a notification with a XMLUI attached
-
-        @param ui(xmlui.XMLUIPanel): XMLUI instance to show when notification is selected
-        """
-        self.app.root.addNotifUI(ui)
-
-    def showUI(self, ui):
-        """show a XMLUI"""
-        self.app.root.changeWidget(ui, "xmlui")
-        self.app.root.show("xmlui")
-
-    def showExtraUI(self, widget):
-        """show any extra widget"""
-        self.app.root.changeWidget(widget, "extra")
-        self.app.root.show("extra")
-
-    def closeUI(self):
-        self.app.root.show()
-
-    def getDefaultAvatar(self, entity=None):
-        return self.app.default_avatar
-
-    def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None):
-        # TODO
-        log.info(u"FIXME: showDialog not implemented")
-        log.info(u"message: {} -- {}".format(title, message))
--- a/src/cagou/core/cagou_widget.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from kivy.uix.image import Image
-from kivy.uix.behaviors import ButtonBehavior
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.dropdown import DropDown
-from kivy import properties
-from cagou import G
-
-
-class HeaderWidgetChoice(ButtonBehavior, BoxLayout):
-    def __init__(self, cagou_widget, plugin_info):
-        self.plugin_info = plugin_info
-        super(HeaderWidgetChoice, self).__init__()
-        self.bind(on_release=lambda btn: cagou_widget.switchWidget(plugin_info))
-
-
-class HeaderWidgetCurrent(ButtonBehavior, Image):
-    pass
-
-
-class HeaderWidgetSelector(DropDown):
-
-    def __init__(self, cagou_widget):
-        super(HeaderWidgetSelector, self).__init__()
-        for plugin_info in G.host.getPluggedWidgets(except_cls=cagou_widget.__class__):
-            choice = HeaderWidgetChoice(cagou_widget, plugin_info)
-            self.add_widget(choice)
-
-
-class CagouWidget(BoxLayout):
-    header_input = properties.ObjectProperty(None)
-    header_box = properties.ObjectProperty(None)
-
-    def __init__(self):
-        for p in G.host.getPluggedWidgets():
-            if p['main'] == self.__class__:
-                self.plugin_info = p
-                break
-        BoxLayout.__init__(self, orientation="vertical")
-        self.selector = HeaderWidgetSelector(self)
-
-    def switchWidget(self, plugin_info):
-        self.selector.dismiss()
-        factory = plugin_info["factory"]
-        new_widget = factory(plugin_info, None, iter(G.host.profiles))
-        G.host.switchWidget(self, new_widget)
-
-    def onHeaderInput(self):
-        log.info(u"header input text entered")
-
-    def onHeaderInputComplete(self, wid, text):
-        return
-
-    def on_touch_down(self, touch):
-        if self.collide_point(*touch.pos):
-            G.host.selected_widget = self
-        super(CagouWidget, self).on_touch_down(touch)
-
-    def headerInputAddExtra(self, widget):
-        """add a widget on the right of header input"""
-        self.header_box.add_widget(widget)
--- a/src/cagou/core/common.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-#!/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/>.
-
-"""common widgets, which can be reused everywhere"""
-
-from kivy.uix.image import Image
-from kivy.uix.behaviors import ButtonBehavior
-from kivy.uix.boxlayout import BoxLayout
-from cagou import G
-
-
-class IconButton(ButtonBehavior, Image):
-    pass
-
-
-class JidWidget(ButtonBehavior, BoxLayout):
-
-    def __init__(self, jid, profile, **kwargs):
-        self.jid = jid
-        self.profile = profile
-        self.nick = kwargs.get('nick')
-        super(JidWidget, self).__init__(**kwargs)
-
-    def getImage(self, wid):
-        host = G.host
-        if host.contact_lists[self.profile].isRoom(self.jid.bare):
-            wid.opacity = 0
-            return ""
-        else:
-            return host.getAvatar(self.jid, profile=self.profile) or host.getDefaultAvatar(self.jid)
--- a/src/cagou/core/config.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-#!/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/>.
-
-"""This module keep an open instance of sat configuration"""
-
-from sat.tools import config
-sat_conf = config.parseMainConf()
-
-
-def getConfig(section, name, default):
-    return config.getConfig(sat_conf, section, name, default)
--- a/src/cagou/core/constants.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-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_frontends.quick_frontend import constants
-
-
-class Const(constants.Const):
-    APP_NAME = u"Cagou"
-    LOG_OPT_SECTION = APP_NAME.lower()
-    CONFIG_SECTION = APP_NAME.lower()
-    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_TRANSFER = u'transfer'
-
-    TRANSFER_UPLOAD = u"upload"
-    TRANSFER_SEND = u"send"
--- a/src/cagou/core/image.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from kivy.uix import image as kivy_img
-from kivy.core.image import Image as CoreImage
-from kivy.resources import resource_find
-import io
-import PIL
-
-
-class Image(kivy_img.Image):
-    """Image widget which accept source without extension"""
-
-    def texture_update(self, *largs):
-        if not self.source:
-            self.texture = None
-        else:
-            filename = resource_find(self.source)
-            self._loops = 0
-            if filename is None:
-                return log.error('Image: Error reading file {filename}'.
-                                    format(filename=self.source))
-            mipmap = self.mipmap
-            if self._coreimage is not None:
-                self._coreimage.unbind(on_texture=self._on_tex_change)
-            try:
-                self._coreimage = ci = CoreImage(filename, mipmap=mipmap,
-                                                 anim_delay=self.anim_delay,
-                                                 keep_data=self.keep_data,
-                                                 nocache=self.nocache)
-            except Exception as e:
-                # loading failed probably because of unmanaged extention,
-                # we try our luck with with PIL
-                try:
-                    im = PIL.Image.open(filename)
-                    ext = im.format.lower()
-                    del im
-                    # we can't use im.tobytes as it would use the
-                    # internal decompressed representation from pillow
-                    # and im.save would need processing to handle format
-                    data = io.BytesIO(open(filename, "rb").read())
-                    cache_filename = u"{}.{}".format(filename,ext) # needed for kivy's Image to use cache
-                    self._coreimage = ci = CoreImage(data, ext=ext,
-                                                     filename=cache_filename, mipmap=mipmap,
-                                                     anim_delay=self.anim_delay,
-                                                     keep_data=self.keep_data,
-                                                     nocache=self.nocache)
-                except Exception as e:
-                    log.warning(u"Can't load image: {}".format(e))
-                    self._coreimage = ci = None
-
-            if ci:
-                ci.bind(on_texture=self._on_tex_change)
-                self.texture = ci.texture
-
-
-class AsyncImage(kivy_img.AsyncImage):
-    """AsyncImage which accept file:// schema"""
-
-    def _load_source(self, *args):
-        if self.source.startswith('file://'):
-            self.source = self.source[7:]
-        else:
-            super(AsyncImage, self)._load_source(*args)
-
--- a/src/cagou/core/kivy_hack.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-#!/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/>.
-
-CONF_KIVY_LEVEL = 'log_kivy_level'
-
-
-def do_hack():
-    """work around Kivy hijacking of logs and arguments"""
-    # we remove args so kivy doesn't use them
-    # this is need to avoid kivy breaking QuickApp args handling
-    import sys
-    ori_argv = sys.argv[:]
-    sys.argv = sys.argv[:1]
-    from constants import Const as C
-    from sat.core import log_config
-    log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
-
-    import config
-    kivy_level = config.getConfig(C.CONFIG_SECTION, CONF_KIVY_LEVEL, 'follow').upper()
-
-    # kivy handles its own loggers, we don't want that!
-    import logging
-    root_logger = logging.root
-    kivy_logger = logging.getLogger('kivy')
-    ori_addHandler = kivy_logger.addHandler
-    kivy_logger.addHandler = lambda dummy: None
-    ori_setLevel = kivy_logger.setLevel
-    if kivy_level == 'FOLLOW':
-        # level is following SàT level
-        kivy_logger.setLevel = lambda level: None
-    elif kivy_level == 'KIVY':
-        # level will be set by Kivy according to its own conf
-        pass
-    elif kivy_level in C.LOG_LEVELS:
-        kivy_logger.setLevel(kivy_level)
-        kivy_logger.setLevel = lambda level: None
-    else:
-        raise ValueError(u"Unknown value for {name}: {value}".format(name=CONF_KIVY_LEVEL, value=kivy_level))
-
-    # during import kivy set its logging stuff
-    import kivy
-    kivy # to avoid pyflakes warning
-
-    # we want to separate kivy logs from other logs
-    logging.root = root_logger
-    from kivy import logger
-    sys.stderr = logger.previous_stderr
-
-    # we restore original methods
-    kivy_logger.addHandler = ori_addHandler
-    kivy_logger.setLevel = ori_setLevel
-
-    # we restore original arguments
-    sys.argv = ori_argv
--- a/src/cagou/core/menu.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,239 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core.i18n import _
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from cagou.core.constants import Const as C
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.label import Label
-from kivy.uix.popup import Popup
-from kivy import properties
-from kivy.garden import contextmenu
-from sat_frontends.quick_frontend import quick_menus
-from cagou import G
-import webbrowser
-
-ABOUT_TITLE = _(u"About {}".format(C.APP_NAME))
-ABOUT_CONTENT = _(u"""Cagou (Salut à Toi) v{}
-
-Cagou is a libre communication tool based on libre standard XMPP.
-
-Cagou is part of the "Salut à Toi" project
-more informations at [color=5500ff][ref=website]salut-a-toi.org[/ref][/color]
-""").format(C.APP_VERSION)
-
-
-class AboutContent(Label):
-
-    def on_ref_press(self, value):
-        if value == "website":
-            webbrowser.open("https://salut-a-toi.org")
-
-
-class AboutPopup(Popup):
-
-    def on_touch_down(self, touch):
-        if self.collide_point(*touch.pos):
-            self.dismiss()
-        return super(AboutPopup, self).on_touch_down(touch)
-
-
-class MainMenu(contextmenu.AppMenu):
-    pass
-
-
-class MenuItem(contextmenu.ContextMenuTextItem):
-    item = properties.ObjectProperty()
-
-    def on_item(self, instance, item):
-        self.text = item.name
-
-    def on_release(self):
-        super(MenuItem, self).on_release()
-        self.parent.hide()
-        selected = G.host.selected_widget
-        profile = None
-        if selected is not None:
-            try:
-                profile = selected.profile
-            except AttributeError:
-                pass
-
-        if profile is None:
-            try:
-                profile = list(selected.profiles)[0]
-            except (AttributeError, IndexError):
-                try:
-                    profile = list(G.host.profiles)[0]
-                except IndexError:
-                    log.warning(u"Can't find profile")
-        self.item.call(selected, profile)
-
-
-class MenuSeparator(contextmenu.ContextMenuDivider):
-    pass
-
-
-class RootMenuContainer(contextmenu.AppMenuTextItem):
-    pass
-
-
-class MenuContainer(contextmenu.ContextMenuTextItem):
-    pass
-
-
-class MenusWidget(BoxLayout):
-
-    def update(self, type_, caller=None):
-        """Method to call when menus have changed
-
-        @param type_(unicode): menu type like in sat.core.sat_main.importMenu
-        @param caller(Widget): instance linked to the menus
-        """
-        self.menus_container = G.host.menus.getMainContainer(type_)
-        self.createMenus(caller)
-
-    def _buildMenus(self, container, caller=None):
-        """Recursively build menus of the container
-
-        @param container(quick_menus.MenuContainer): menu container
-        @param caller(Widget): instance linked to the menus
-        """
-        if caller is None:
-            main_menu = MainMenu()
-            self.add_widget(main_menu)
-            caller = main_menu
-        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
-            # TODO: fix this upstream
-            context_menu._on_visible(False)
-
-            caller = context_menu
-
-        for child in container.getActiveMenus():
-            if isinstance(child, quick_menus.MenuContainer):
-                if isinstance(caller, MainMenu):
-                    menu_container = RootMenuContainer()
-                else:
-                    menu_container = MenuContainer()
-                menu_container.text = child.name
-                caller.add_widget(menu_container)
-                self._buildMenus(child, caller=menu_container)
-            elif isinstance(child, quick_menus.MenuSeparator):
-                wid = MenuSeparator()
-                caller.add_widget(wid)
-            elif isinstance(child, quick_menus.MenuItem):
-                wid = MenuItem(item=child)
-                caller.add_widget(wid)
-            else:
-                log.error(u"Unknown child type: {}".format(child))
-
-    def createMenus(self, caller):
-        self.clear_widgets()
-        self._buildMenus(self.menus_container, caller)
-
-    def onAbout(self):
-        about = AboutPopup()
-        about.title = ABOUT_TITLE
-        about.content = AboutContent(text=ABOUT_CONTENT, markup=True)
-        about.open()
-
-
-class TransferItem(BoxLayout):
-    plug_info = properties.DictProperty()
-
-    def on_touch_up(self, touch):
-        if not self.collide_point(*touch.pos):
-            return super(TransferItem, self).on_touch_up(touch)
-        else:
-            transfer_menu = self.parent
-            while not isinstance(transfer_menu, TransferMenu):
-                transfer_menu = transfer_menu.parent
-            transfer_menu.do_callback(self.plug_info)
-            return True
-
-
-class TransferMenu(BoxLayout):
-    """transfer menu which handle display and callbacks"""
-    # callback will be called with path to file to transfer
-    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 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")
-    items_layout = properties.ObjectProperty()
-
-    def __init__(self, **kwargs):
-        super(TransferMenu, self).__init__(**kwargs)
-        if self.cancel_cb is None:
-            self.cancel_cb = self.onTransferCancelled
-        if self.profiles is None:
-            self.profiles = iter(G.host.profiles)
-        for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_TRANSFER):
-            item = TransferItem(
-                plug_info = plug_info
-                )
-            self.items_layout.add_widget(item)
-
-    def show(self, caller_wid=None):
-        self.visible = True
-        G.host.app.root.add_widget(self)
-
-    def on_touch_down(self, touch):
-        # we remove the menu if we click outside
-        # else we want to handle the event, but not
-        # transmit it to parents
-        if not self.collide_point(*touch.pos):
-            self.parent.remove_widget(self)
-        else:
-            return super(TransferMenu, self).on_touch_down(touch)
-        return True
-
-    def _closeUI(self, wid):
-        G.host.closeUI()
-
-    def onTransferCancelled(self, wid, cleaning_cb=None):
-        self._closeUI(wid)
-        if cleaning_cb is not None:
-            cleaning_cb()
-
-    def do_callback(self, plug_info):
-        self.parent.remove_widget(self)
-        if self.callback is None:
-            log.warning(u"TransferMenu callback is not set")
-        else:
-            wid = None
-            external = plug_info.get('external', False)
-            def onTransferCb(file_path, cleaning_cb=None):
-                if not external:
-                    self._closeUI(wid)
-                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)
-            if not external:
-                G.host.showExtraUI(wid)
--- a/src/cagou/core/profile_manager.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from .constants import Const as C
-from sat_frontends.quick_frontend.quick_profile_manager import QuickProfileManager
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix import listview
-from kivy.uix.button import Button
-from kivy.uix.screenmanager import ScreenManager, Screen
-from kivy.adapters import listadapter
-from kivy.metrics import sp
-from kivy import properties
-from cagou import G
-
-
-class ProfileItem(listview.ListItemButton):
-    pass
-
-
-class ProfileListAdapter(listadapter.ListAdapter):
-
-    def __init__(self, pm, *args, **kwargs):
-        super(ProfileListAdapter, self).__init__(*args, **kwargs)
-        self.pm = pm
-
-    def closeUI(self, xmlui):
-        self.pm.screen_manager.transition.direction = 'right'
-        self.pm.screen_manager.current = 'profiles'
-
-    def showUI(self, xmlui):
-        xmlui.setCloseCb(self.closeUI)
-        if xmlui.type == 'popup':
-            xmlui.bind(on_touch_up=lambda obj, value: self.closeUI(xmlui))
-        self.pm.xmlui_screen.clear_widgets()
-        self.pm.xmlui_screen.add_widget(xmlui)
-        self.pm.screen_manager.transition.direction = 'left'
-        self.pm.screen_manager.current = 'xmlui'
-
-    def select_item_view(self, view):
-        def authenticate_cb(data, cb_id, profile):
-            if C.bool(data.pop('validated', C.BOOL_FALSE)):
-                super(ProfileListAdapter, self).select_item_view(view)
-            G.host.actionManager(data, callback=authenticate_cb, ui_show_cb=self.showUI, profile=profile)
-
-        G.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=view.text)
-
-
-class ConnectButton(Button):
-
-    def __init__(self, profile_screen):
-        self.profile_screen = profile_screen
-        self.pm = profile_screen.pm
-        super(ConnectButton, self).__init__()
-
-
-class NewProfileScreen(Screen):
-    profile_name = properties.ObjectProperty(None)
-    jid = properties.ObjectProperty(None)
-    password = properties.ObjectProperty(None)
-    error_msg = properties.StringProperty('')
-
-    def __init__(self, pm):
-        super(NewProfileScreen, self).__init__(name=u'new_profile')
-        self.pm = pm
-
-    def onCreationFailure(self, failure):
-        msg = [l for l in unicode(failure).split('\n') if l][-1]
-        self.error_msg = unicode(msg)
-
-    def onCreationSuccess(self, profile):
-        self.pm.profiles_screen.reload()
-        G.host.bridge.profileStartSession(self.password.text, profile, callback=lambda dummy: self._sessionStarted(profile), errback=self.onCreationFailure)
-
-    def _sessionStarted(self, profile):
-        jid = self.jid.text.strip()
-        G.host.bridge.setParam("JabberID", jid, "Connection", -1, profile)
-        G.host.bridge.setParam("Password", self.password.text, "Connection", -1, profile)
-        self.pm.screen_manager.transition.direction = 'right'
-        self.pm.screen_manager.current = 'profiles'
-
-    def doCreate(self):
-        name = self.profile_name.text.strip()
-        # XXX: we use XMPP password for profile password to simplify
-        #      if user want to change profile password, he can do it in preferences
-        G.host.bridge.asyncCreateProfile(name, self.password.text, callback=lambda: self.onCreationSuccess(name), errback=self.onCreationFailure)
-
-
-class DeleteProfilesScreen(Screen):
-
-    def __init__(self, pm):
-        self.pm = pm
-        super(DeleteProfilesScreen, self).__init__(name=u'delete_profiles')
-
-    def doDelete(self):
-        """This method will delete *ALL* selected profiles"""
-        to_delete = self.pm.getProfiles()
-        deleted = [0]
-
-        def deleteInc():
-            deleted[0] += 1
-            if deleted[0] == len(to_delete):
-                self.pm.profiles_screen.reload()
-                self.pm.screen_manager.transition.direction = 'right'
-                self.pm.screen_manager.current = 'profiles'
-
-        for profile in to_delete:
-            log.info(u"Deleteing profile [{}]".format(profile))
-            G.host.bridge.asyncDeleteProfile(profile, callback=deleteInc, errback=deleteInc)
-
-
-class ProfilesScreen(Screen):
-    layout = properties.ObjectProperty(None)
-
-    def __init__(self, pm):
-        self.pm = pm
-        self.list_adapter = ProfileListAdapter(pm,
-                                               data=[],
-                                               cls=ProfileItem,
-                                               args_converter=self.converter,
-                                               selection_mode='multiple',
-                                               allow_empty_selection=True,
-                                              )
-        super(ProfilesScreen, self).__init__(name=u'profiles')
-        self.layout.add_widget(listview.ListView(adapter=self.list_adapter))
-        connect_btn = ConnectButton(self)
-        self.layout.add_widget(connect_btn)
-        self.reload()
-
-    def _profilesListGetCb(self, profiles):
-        profiles.sort()
-        self.list_adapter.data = profiles
-
-    def converter(self, row_idx, obj):
-        return {'text': obj,
-                'size_hint_y': None,
-                'height': sp(40)}
-
-    def reload(self):
-        """Reload profiles list"""
-        G.host.bridge.profilesListGet(callback=self._profilesListGetCb)
-
-
-class ProfileManager(QuickProfileManager, BoxLayout):
-
-    def __init__(self, autoconnect=None):
-        QuickProfileManager.__init__(self, G.host, autoconnect)
-        BoxLayout.__init__(self, orientation="vertical")
-        self.screen_manager = ScreenManager()
-        self.profiles_screen = ProfilesScreen(self)
-        self.new_profile_screen = NewProfileScreen(self)
-        self.delete_profiles_screen = DeleteProfilesScreen(self)
-        self.xmlui_screen = Screen(name=u'xmlui')
-        self.screen_manager.add_widget(self.profiles_screen)
-        self.screen_manager.add_widget(self.xmlui_screen)
-        self.screen_manager.add_widget(self.new_profile_screen)
-        self.screen_manager.add_widget(self.delete_profiles_screen)
-        self.add_widget(self.screen_manager)
-
-    def getProfiles(self):
-        return [pi.text for pi in self.profiles_screen.list_adapter.selection]
--- a/src/cagou/core/simple_xhtml.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,498 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from kivy.uix.stacklayout import StackLayout
-from kivy.uix.label import Label
-from kivy.utils import escape_markup
-from kivy import properties
-from xml.etree import ElementTree as ET
-from sat_frontends.tools import css_color, strings as sat_strings
-from cagou.core.image import AsyncImage
-import webbrowser
-
-
-class Escape(unicode):
-    """Class used to mark that a message need to be escaped"""
-
-    def __init__(self, text):
-        super(Escape, self).__init__(text)
-
-
-class SimpleXHTMLWidgetEscapedText(Label):
-
-    def _addUrlMarkup(self, text):
-        text_elts = []
-        idx = 0
-        links = 0
-        while True:
-            m = sat_strings.RE_URL.search(text[idx:])
-            if m is not None:
-                text_elts.append(escape_markup(m.string[0:m.start()]))
-                link_key = u'link_' + unicode(links)
-                url = m.group()
-                text_elts.append(u'[color=5500ff][ref={link}]{url}[/ref][/color]'.format(
-                    link = link_key,
-                    url = url
-                    ))
-                if not links:
-                    self.ref_urls = {link_key: url}
-                else:
-                    self.ref_urls[link_key] = url
-                links += 1
-                idx += m.end()
-            else:
-                if links:
-                    text_elts.append(escape_markup(text[idx:]))
-                    self.markup = True
-                    self.text = u''.join(text_elts)
-                break
-
-    def on_text(self, instance, text):
-        # do NOT call the method if self.markup is set
-        # this would result in infinite loop (because self.text
-        # is changed if an URL is found, and in this case markup too)
-        if text and not self.markup:
-            self._addUrlMarkup(text)
-
-    def on_ref_press(self, ref):
-        url = self.ref_urls[ref]
-        webbrowser.open(url)
-
-
-class SimpleXHTMLWidgetText(Label):
-    pass
-
-
-class SimpleXHTMLWidgetImage(AsyncImage):
-    # following properties are desired height/width
-    # i.e. the ones specified in height/width attributes of <img>
-    # (or wanted for whatever reason)
-    # set to 0 to ignore them
-    target_height = properties.NumericProperty()
-    target_width = properties.NumericProperty()
-
-    def _get_parent_container(self):
-        """get parent SimpleXHTMLWidget instance
-
-        @param warning(bool): if True display a log.error if nothing found
-        @return (SimpleXHTMLWidget, None): found SimpleXHTMLWidget instance
-        """
-        parent = self.parent
-        while parent and not isinstance(parent, SimpleXHTMLWidget):
-            parent = parent.parent
-        if parent is None:
-            log.error(u"no SimpleXHTMLWidget parent found")
-        return parent
-
-    def _on_source_load(self, value):
-        # this method is called when image is loaded
-        super(SimpleXHTMLWidgetImage, self)._on_source_load(value)
-        if self.parent is not None:
-            container = self._get_parent_container()
-            # image is loaded, we need to recalculate size
-            self.on_container_width(container, container.width)
-
-    def on_container_width(self, container, container_width):
-        """adapt size according to container width
-
-        called when parent container (SimpleXHTMLWidget) width change
-        """
-        target_size = (self.target_width or self.texture.width, self.target_height or self.texture.height)
-        padding = container.padding
-        padding_h = (padding[0] + padding[2]) if len(padding) == 4 else padding[0]
-        width = container_width - padding_h
-        if target_size[0] < width:
-            self.size = target_size
-        else:
-            height = width / self.image_ratio
-            self.size = (width, height)
-
-    def on_parent(self, instance, parent):
-        if parent is not None:
-            container = self._get_parent_container()
-            container.bind(width=self.on_container_width)
-
-
-class SimpleXHTMLWidget(StackLayout):
-    """widget handling simple XHTML parsing"""
-    xhtml = properties.StringProperty()
-    color = properties.ListProperty([1, 1, 1, 1])
-    # XXX: bold is only used for escaped text
-    bold = properties.BooleanProperty(False)
-    content_width = properties.NumericProperty(0)
-
-    # text/XHTML input
-
-    def on_xhtml(self, instance, xhtml):
-        """parse xhtml and set content accordingly
-
-        if xhtml is an instance of Escape, a Label with not markup
-        will be used
-        """
-        self.clear_widgets()
-        if isinstance(xhtml, Escape):
-            label = SimpleXHTMLWidgetEscapedText(text=xhtml, color=self.color)
-            self.bind(color=label.setter('color'))
-            self.bind(bold=label.setter('bold'))
-            self.add_widget(label)
-        else:
-            xhtml = ET.fromstring(xhtml.encode('utf-8'))
-            self.current_wid = None
-            self.styles = []
-            self._callParseMethod(xhtml)
-
-    def escape(self, text):
-        """mark that a text need to be escaped (i.e. no markup)"""
-        return Escape(text)
-
-    # sizing
-
-    def on_width(self, instance, width):
-        if len(self.children) == 1:
-            wid = self.children[0]
-            if isinstance(wid, Label):
-                # we have simple text
-                try:
-                    full_width = wid._full_width
-                except AttributeError:
-                    # on first time, we need the required size
-                    # for the full text, without width limit
-                    wid.size_hint = (None, None)
-                    wid.texture_update()
-                    full_width = wid._full_width = wid.texture_size[0]
-
-                if full_width > width:
-                    wid.text_size = width, None
-                    wid.width = width
-                else:
-                    wid.text_size = None, None
-                    wid.texture_update()
-                    wid.width = wid.texture_size[0]
-                self.content_width = wid.width + self.padding[0] + self.padding[2]
-            else:
-                wid.size_hint = (1, None)
-                wid.height = 100
-                self.content_width = self.width
-        else:
-            self._do_complexe_sizing(width)
-
-    def _do_complexe_sizing(self, width):
-        try:
-            self.splitted
-        except AttributeError:
-            # XXX: to make things easier, we split labels in words
-            log.debug(u"split start")
-            children = self.children[::-1]
-            self.clear_widgets()
-            for child in children:
-                if isinstance(child, Label):
-                    log.debug(u"label before split: {}".format(child.text))
-                    styles = []
-                    tag = False
-                    new_text = []
-                    current_tag = []
-                    current_value = []
-                    current_wid = self._createText()
-                    value = False
-                    close = False
-                    # we will parse the text and create a new widget
-                    # on each new word (actually each space)
-                    # FIXME: handle '\n' and other white chars
-                    for c in child.text:
-                        if tag:
-                            # we are parsing a markup tag
-                            if c == u']':
-                                current_tag_s = u''.join(current_tag)
-                                current_style = (current_tag_s, u''.join(current_value))
-                                if close:
-                                    for idx, s in enumerate(reversed(styles)):
-                                        if s[0] == current_tag_s:
-                                            del styles[len(styles) - idx - 1]
-                                            break
-                                else:
-                                    styles.append(current_style)
-                                current_tag = []
-                                current_value = []
-                                tag = False
-                                value = False
-                                close = False
-                            elif c == u'/':
-                                close = True
-                            elif c == u'=':
-                                value = True
-                            elif value:
-                                current_value.append(c)
-                            else:
-                                current_tag.append(c)
-                            new_text.append(c)
-                        else:
-                            # we are parsing regular text
-                            if c == u'[':
-                                new_text.append(c)
-                                tag = True
-                            elif c == u' ':
-                                # new word, we do a new widget
-                                new_text.append(u' ')
-                                for t, v in reversed(styles):
-                                    new_text.append(u'[/{}]'.format(t))
-                                current_wid.text = u''.join(new_text)
-                                new_text = []
-                                self.add_widget(current_wid)
-                                log.debug(u"new widget: {}".format(current_wid.text))
-                                current_wid = self._createText()
-                                for t, v in styles:
-                                    new_text.append(u'[{tag}{value}]'.format(
-                                        tag = t,
-                                        value = u'={}'.format(v) if v else u''))
-                            else:
-                                new_text.append(c)
-                    if current_wid.text:
-                        # we may have a remaining widget after the parsing
-                        close_styles = []
-                        for t, v in reversed(styles):
-                            close_styles.append(u'[/{}]'.format(t))
-                        current_wid.text = u''.join(close_styles)
-                        self.add_widget(current_wid)
-                        log.debug(u"new widget: {}".format(current_wid.text))
-                else:
-                    # non Label widgets, we just add them
-                    self.add_widget(child)
-            self.splitted = True
-            log.debug(u"split OK")
-
-        # we now set the content width
-        # FIXME: for now we just use the full width
-        self.content_width = width
-
-    # XHTML parsing methods
-
-    def _callParseMethod(self, e):
-        """call the suitable method to parse the element
-
-        self.xhtml_[tag] will be called if it exists, else
-        self.xhtml_generic will be used
-        @param e(ET.Element): element to parse
-        """
-        try:
-            method = getattr(self, "xhtml_{}".format(e.tag))
-        except AttributeError:
-            log.warning(u"Unhandled XHTML tag: {}".format(e.tag))
-            method = self.xhtml_generic
-        method(e)
-
-    def _addStyle(self, tag, value=None, append_to_list=True):
-        """add a markup style to label
-
-        @param tag(unicode): markup tag
-        @param value(unicode): markup value if suitable
-        @param append_to_list(bool): if True style we be added to self.styles
-            self.styles is needed to keep track of styles to remove
-            should most probably be set to True
-        """
-        label = self._getLabel()
-        label.text += u'[{tag}{value}]'.format(
-            tag = tag,
-            value = u'={}'.format(value) if value else ''
-            )
-        if append_to_list:
-            self.styles.append((tag, value))
-
-    def _removeStyle(self, tag, remove_from_list=True):
-        """remove a markup style from the label
-
-        @param tag(unicode): markup tag to remove
-        @param remove_from_list(bool): if True, remove from self.styles too
-            should most probably be set to True
-        """
-        label = self._getLabel()
-        label.text += u'[/{tag}]'.format(
-            tag = tag
-            )
-        if remove_from_list:
-            for rev_idx, style in enumerate(reversed(self.styles)):
-                if style[0] == tag:
-                    tag_idx = len(self.styles) - 1 - rev_idx
-                    del self.styles[tag_idx]
-                    break
-
-    def _getLabel(self):
-        """get current Label if it exists, or create a new one"""
-        if not isinstance(self.current_wid, Label):
-            self._addLabel()
-        return self.current_wid
-
-    def _addLabel(self):
-        """add a new Label
-
-        current styles will be closed and reopened if needed
-        """
-        self._closeLabel()
-        self.current_wid = self._createText()
-        for tag, value in self.styles:
-            self._addStyle(tag, value, append_to_list=False)
-        self.add_widget(self.current_wid)
-
-    def _createText(self):
-        label = SimpleXHTMLWidgetText(color=self.color, markup=True)
-        self.bind(color=label.setter('color'))
-        label.bind(texture_size=label.setter('size'))
-        return label
-
-    def _closeLabel(self):
-        """close current style tags in current label
-
-        needed when you change label to keep style between
-        different widgets
-        """
-        if isinstance(self.current_wid, Label):
-            for tag, value in reversed(self.styles):
-                self._removeStyle(tag, remove_from_list=False)
-
-    def _parseCSS(self, e):
-        """parse CSS found in "style" attribute of element
-
-        self._css_styles will be created and contained markup styles added by this method
-        @param e(ET.Element): element which may have a "style" attribute
-        """
-        styles_limit = len(self.styles)
-        styles = e.attrib['style'].split(u';')
-        for style in styles:
-            try:
-                prop, value = style.split(u':')
-            except ValueError:
-                log.warning(u"can't parse style: {}".format(style))
-                continue
-            prop = prop.strip().replace(u'-', '_')
-            value = value.strip()
-            try:
-                method = getattr(self, "css_{}".format(prop))
-            except AttributeError:
-                log.warning(u"Unhandled CSS: {}".format(prop))
-            else:
-                method(e, value)
-        self._css_styles = self.styles[styles_limit:]
-
-    def _closeCSS(self):
-        """removed CSS styles
-
-        styles in self._css_styles will be removed
-        and the attribute will be deleted
-        """
-        for tag, dummy in reversed(self._css_styles):
-            self._removeStyle(tag)
-        del self._css_styles
-
-    def xhtml_generic(self, elem, style=True, markup=None):
-        """generic method for adding HTML elements
-
-        this method handle content, style and children parsing
-        @param elem(ET.Element): element to add
-        @param style(bool): if True handle style attribute (CSS)
-        @param markup(tuple[unicode, (unicode, None)], None): kivy markup to use
-        """
-        # we first add markup and CSS style
-        if markup is not None:
-            if isinstance(markup, basestring):
-                tag, value = markup, None
-            else:
-                tag, value = markup
-            self._addStyle(tag, value)
-        style_ = 'style' in elem.attrib and style
-        if style_:
-            self._parseCSS(elem)
-
-        # then content
-        if elem.text:
-            self._getLabel().text += escape_markup(elem.text)
-
-        # we parse the children
-        for child in elem:
-            self._callParseMethod(child)
-
-        # closing CSS style and markup
-        if style_:
-            self._closeCSS()
-        if markup is not None:
-            self._removeStyle(tag)
-
-        # and the tail, which is regular text
-        if elem.tail:
-            self._getLabel().text += escape_markup(elem.tail)
-
-    # method handling XHTML elements
-
-    def xhtml_br(self, elem):
-        label = self._getLabel()
-        label.text+='\n'
-        self.xhtml_generic(style=False)
-
-    def xhtml_em(self, elem):
-        self.xhtml_generic(elem, markup='i')
-
-    def xhtml_img(self, elem):
-        try:
-            src = elem.attrib['src']
-        except KeyError:
-            log.warning(u"<img> element without src: {}".format(ET.tostring(elem)))
-            return
-        try:
-            target_height = int(elem.get(u'height', 0))
-        except ValueError:
-            log.warning(u"Can't parse image height: {}".format(elem.get(u'height')))
-            target_height = 0
-        try:
-            target_width = int(elem.get(u'width', 0))
-        except ValueError:
-            log.warning(u"Can't parse image width: {}".format(elem.get(u'width')))
-            target_width = 0
-
-        img = SimpleXHTMLWidgetImage(source=src, target_height=target_height, target_width=target_width)
-        self.current_wid = img
-        self.add_widget(img)
-
-    def xhtml_p(self, elem):
-        if isinstance(self.current_wid, Label):
-            self.current_wid.text+="\n\n"
-        self.xhtml_generic(elem)
-
-    def xhtml_span(self, elem):
-        self.xhtml_generic(elem)
-
-    def xhtml_strong(self, elem):
-        self.xhtml_generic(elem, markup='b')
-
-    # methods handling CSS properties
-
-    def css_color(self, elem, value):
-        self._addStyle(u"color", css_color.parse(value))
-
-    def css_text_decoration(self, elem, value):
-        if value == u'underline':
-            log.warning(u"{} not handled yet, it needs Kivy 1.9.2 to be released".format(value))
-            # FIXME: activate when 1.9.2 is out
-            # self._addStyle('u')
-        elif value == u'line-through':
-            log.warning(u"{} not handled yet, it needs Kivy 1.9.2 to be released".format(value))
-            # FIXME: activate when 1.9.2 is out
-            # self._addStyle('s')
-        else:
-            log.warning(u"unhandled text decoration: {}".format(value))
--- a/src/cagou/core/widgets_handler.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,230 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat_frontends.quick_frontend import quick_widgets
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.button import Button
-from kivy.uix.carousel import Carousel
-from kivy.metrics import dp
-from kivy import properties
-from cagou import G
-
-
-CAROUSEL_SCROLL_DISTANCE = dp(50)
-CAROUSEL_SCROLL_TIMEOUT = 80
-NEW_WIDGET_DIST = 10
-REMOVE_WIDGET_DIST = NEW_WIDGET_DIST
-
-
-class WHSplitter(Button):
-    horizontal=properties.BooleanProperty(True)
-    thickness=properties.NumericProperty(dp(20))
-    split_move = None # we handle one split at a time, so we use a class attribute
-
-    def __init__(self, handler, **kwargs):
-        super(WHSplitter, self).__init__(**kwargs)
-        self.handler = handler
-
-    def getPos(self, touch):
-        if self.horizontal:
-            relative_y = self.handler.to_local(*touch.pos, relative=True)[1]
-            return self.handler.height - relative_y
-        else:
-            return touch.x
-
-    def on_touch_move(self, touch):
-        if self.split_move is None and self.collide_point(*touch.opos):
-            WHSplitter.split_move = self
-
-        if self.split_move is self:
-            pos = self.getPos(touch)
-            if pos > NEW_WIDGET_DIST:
-                # we are above minimal distance, we resize the widget
-                self.handler.setWidgetSize(self.horizontal, pos)
-
-    def on_touch_up(self, touch):
-        if self.split_move is self:
-            pos = self.getPos(touch)
-            if pos <= REMOVE_WIDGET_DIST:
-                # if we go under minimal distance, the widget is not wanted anymore
-                self.handler.removeWidget(self.horizontal)
-            WHSplitter.split_move=None
-        return super(WHSplitter, self).on_touch_up(touch)
-
-
-class HandlerCarousel(Carousel):
-
-    def __init__(self, *args, **kwargs):
-        super(HandlerCarousel, self).__init__(
-            *args,
-            direction='right',
-            loop=True,
-            **kwargs)
-        self._former_slide = None
-        self.bind(current_slide=self.onSlideChange)
-        self._slides_update_lock = False
-
-    def changeWidget(self, new_widget):
-        """Change currently displayed widget
-
-        slides widgets will be updated
-        """
-        # slides update need to be blocked to avoid the update in onSlideChange
-        # which would mess the removal of current widgets
-        self._slides_update_lock = True
-        current = self.current_slide
-        for w in self.slides:
-            if w == current or w == new_widget:
-                continue
-            if isinstance(w, quick_widgets.QuickWidget):
-                G.host.widgets.deleteWidget(w)
-        self.clear_widgets()
-        self.add_widget(new_widget)
-        self._slides_update_lock = False
-        self.updateHiddenSlides()
-
-    def onSlideChange(self, handler, new_slide):
-        if isinstance(self._former_slide, quick_widgets.QuickWidget):
-            G.host.removeVisibleWidget(self._former_slide)
-        self._former_slide = new_slide
-        if isinstance(new_slide, quick_widgets.QuickWidget):
-            G.host.addVisibleWidget(new_slide)
-            self.updateHiddenSlides()
-
-    def hiddenList(self, visible_list):
-        """return widgets of same class as holded one which are hidden
-
-        @param visible_list(list[QuickWidget]): widgets visible
-        @return (iter[QuickWidget]): widgets hidden
-        """
-        added = [(w.targets, w.profiles) for w in visible_list]  # we want to avoid recreated widgets
-        for w in G.host.widgets.getWidgets(self.current_slide.__class__, profiles=self.current_slide.profiles):
-            if w in visible_list or (w.targets, w.profiles) in added:
-                continue
-            yield w
-
-    def widgets_sort(self, widget):
-        """method used as key to sort the widgets
-
-        order of the widgets when changing slide is affected
-        @param widget(QuickWidget): widget to sort
-        @return: a value which will be used for sorting
-        """
-        try:
-            return unicode(widget.target).lower()
-        except AttributeError:
-            return unicode(list(widget.targets)[0]).lower()
-
-    def updateHiddenSlides(self):
-        """adjust carousel slides according to visible widgets"""
-        if self._slides_update_lock:
-            return
-        if not isinstance(self.current_slide, quick_widgets.QuickWidget):
-            return
-        # lock must be used here to avoid recursions
-        self._slides_update_lock = True
-        visible_list = G.host.getVisibleList(self.current_slide.__class__)
-        hidden = list(self.hiddenList(visible_list))
-        slides_sorted =  sorted(hidden + [self.current_slide], key=self.widgets_sort)
-        to_remove = set(self.slides).difference({self.current_slide})
-        for w in to_remove:
-            self.remove_widget(w)
-        if hidden:
-            # no need to add more than two widgets (next and previous),
-            # as the list will be updated on each new visible widget
-            current_idx = slides_sorted.index(self.current_slide)
-            try:
-                next_slide = slides_sorted[current_idx+1]
-            except IndexError:
-                next_slide = slides_sorted[0]
-            self.add_widget(G.host.getOrClone(next_slide))
-            if len(hidden)>1:
-                previous_slide = slides_sorted[current_idx-1]
-                self.add_widget(G.host.getOrClone(previous_slide))
-
-        if len(self.slides) == 1:
-            # we block carousel with high scroll_distance to avoid swiping
-            # when the is not other instance of the widget
-            self.scroll_distance=2**32
-            self.scroll_timeout=0
-        else:
-            self.scroll_distance = CAROUSEL_SCROLL_DISTANCE
-            self.scroll_timeout=CAROUSEL_SCROLL_TIMEOUT
-        self._slides_update_lock = False
-
-
-class WidgetsHandler(BoxLayout):
-
-    def __init__(self, wid=None, **kw):
-        if wid is None:
-            wid=self.default_widget
-        self.vert_wid = self.hor_wid = None
-        BoxLayout.__init__(self, orientation="vertical", **kw)
-        self.blh = BoxLayout(orientation="horizontal")
-        self.blv = BoxLayout(orientation="vertical")
-        self.blv.add_widget(WHSplitter(self))
-        self.carousel = HandlerCarousel()
-        self.blv.add_widget(self.carousel)
-        self.blh.add_widget(WHSplitter(self, horizontal=False))
-        self.blh.add_widget(self.blv)
-        self.add_widget(self.blh)
-        self.changeWidget(wid)
-
-    @property
-    def default_widget(self):
-        return G.host.default_wid['factory'](G.host.default_wid, None, None)
-
-    @property
-    def cagou_widget(self):
-        """get holded CagouWidget"""
-        return self.carousel.current_slide
-
-    def changeWidget(self, new_widget):
-        self.carousel.changeWidget(new_widget)
-
-    def removeWidget(self, vertical):
-        if vertical and self.vert_wid is not None:
-            self.remove_widget(self.vert_wid)
-            self.vert_wid.onDelete()
-            self.vert_wid = None
-        elif self.hor_wid is not None:
-            self.blh.remove_widget(self.hor_wid)
-            self.hor_wid.onDelete()
-            self.hor_wid = None
-
-    def setWidgetSize(self, vertical, size):
-        if vertical:
-            if self.vert_wid is None:
-                self.vert_wid = WidgetsHandler(self.default_widget, size_hint=(1, None))
-                self.add_widget(self.vert_wid, len(self.children))
-            self.vert_wid.height=size
-        else:
-            if self.hor_wid is None:
-                self.hor_wid = WidgetsHandler(self.default_widget, size_hint=(None, 1))
-                self.blh.add_widget(self.hor_wid, len(self.blh.children))
-            self.hor_wid.width=size
-
-    def onDelete(self):
-        # when this handler is deleted, we need to delete the holded CagouWidget
-        cagou_widget = self.cagou_widget
-        if isinstance(cagou_widget, quick_widgets.QuickWidget):
-            G.host.removeVisibleWidget(cagou_widget)
--- a/src/cagou/core/xmlui.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,561 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Cagou: a SàT frontend
-# 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 _
-from .constants import Const as C
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools import xmlui
-from kivy.uix.scrollview import ScrollView
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.gridlayout import GridLayout
-from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
-from kivy.uix.textinput import TextInput
-from kivy.uix.label import Label
-from kivy.uix.button import Button
-from kivy.uix.togglebutton import ToggleButton
-from kivy.uix.widget import Widget
-from kivy.uix.dropdown import DropDown
-from kivy.uix.switch import Switch
-from kivy import properties
-from cagou import G
-
-
-## Widgets ##
-
-
-class TextInputOnChange(object):
-
-    def __init__(self):
-        self._xmlui_onchange_cb = None
-        self._got_focus = False
-
-    def _xmluiOnChange(self, callback):
-        self._xmlui_onchange_cb = callback
-
-    def on_focus(self, instance, focus):
-        # we need to wait for first focus, else initial value
-        # will trigger a on_text
-        if not self._got_focus and focus:
-            self._got_focus = True
-
-    def on_text(self, instance, new_text):
-        log.debug("on_text: %s" % new_text)
-        if self._xmlui_onchange_cb is not None and self._got_focus:
-            self._xmlui_onchange_cb(self)
-
-
-class EmptyWidget(xmlui.EmptyWidget, Widget):
-
-    def __init__(self, _xmlui_parent):
-        Widget.__init__(self)
-
-
-class TextWidget(xmlui.TextWidget, Label):
-
-    def __init__(self, xmlui_parent, value):
-        Label.__init__(self, text=value)
-
-
-class LabelWidget(xmlui.LabelWidget, TextWidget):
-    pass
-
-
-class JidWidget(xmlui.JidWidget, TextWidget):
-    pass
-
-
-class StringWidget(xmlui.StringWidget, TextInput, TextInputOnChange):
-
-    def __init__(self, xmlui_parent, value, read_only=False):
-        TextInput.__init__(self, text=value, multiline=False)
-        TextInputOnChange.__init__(self)
-        self.readonly = read_only
-
-    def _xmluiSetValue(self, value):
-        self.text = value
-
-    def _xmluiGetValue(self):
-        return self.text
-
-
-class JidInputWidget(xmlui.JidInputWidget, StringWidget):
-    pass
-
-
-class ButtonWidget(xmlui.ButtonWidget, Button):
-
-    def __init__(self, _xmlui_parent, value, click_callback):
-        Button.__init__(self)
-        self.text = value
-        self.callback = click_callback
-
-    def _xmluiOnClick(self, callback):
-        self.callback = callback
-
-    def on_release(self):
-        self.callback(self)
-
-
-class DividerWidget(xmlui.DividerWidget, Widget):
-    # FIXME: not working properly + only 'line' is handled
-    style = properties.OptionProperty('line',
-        options=['line', 'dot', 'dash', 'plain', 'blank'])
-
-    def __init__(self, _xmlui_parent, style="line"):
-        Widget.__init__(self, style=style)
-
-
-class ListWidgetItem(ToggleButton):
-    value = properties.StringProperty()
-
-    def on_release(self):
-        super(ListWidgetItem, self).on_release()
-        parent = self.parent
-        while parent is not None and not isinstance(parent, DropDown):
-            parent = parent.parent
-
-        if parent is not None and parent.attach_to is not None:
-            parent.select(self)
-
-    @property
-    def selected(self):
-        return self.state == 'down'
-
-    @selected.setter
-    def selected(self, value):
-        self.state = 'down' if value else 'normal'
-
-
-class ListWidget(xmlui.ListWidget, Button):
-
-    def __init__(self, _xmlui_parent, options, selected, flags):
-        Button.__init__(self)
-        self.text = _(u"open list")
-        self._dropdown = DropDown()
-        self._dropdown.auto_dismiss = False
-        self._dropdown.bind(on_select = self.on_select)
-        self.multi = 'single' not in flags
-        self._dropdown.dismiss_on_select = not self.multi
-        self._values = []
-        for option in options:
-            self.addValue(option)
-        self._xmluiSelectValues(selected)
-        self._on_change = None
-
-    @property
-    def items(self):
-        return self._dropdown.children[0].children
-
-    def on_touch_down(self, touch):
-        # we simulate auto-dismiss ourself because dropdown
-        # will dismiss even if attached button is touched
-        # resulting in a dismiss just before a toggle in on_release
-        # so the dropbox would always be opened, we don't want that!
-        if super(ListWidget, self).on_touch_down(touch):
-            return True
-        if self._dropdown.parent:
-            self._dropdown.dismiss()
-
-    def on_release(self):
-        if self._dropdown.parent is not None:
-            # we want to close a list already opened
-            self._dropdown.dismiss()
-        else:
-            self._dropdown.open(self)
-
-    def on_select(self, drop_down, item):
-        if not self.multi:
-            self._xmluiSelectValues([item.value])
-        if self._on_change is not None:
-            self._on_change(self)
-
-    def addValue(self, option, selected=False):
-        """add a value in the list
-
-        @param option(tuple): value, label in a tuple
-        """
-        self._values.append(option)
-        item = ListWidgetItem()
-        item.value, item.text = option
-        item.selected = selected
-        self._dropdown.add_widget(item)
-
-    def _xmluiSelectValue(self, value):
-        self._xmluiSelectValues([value])
-
-    def _xmluiSelectValues(self, values):
-        for item in self.items:
-            item.selected = item.value in values
-            if item.selected and not self.multi:
-                self.text = item.text
-
-    def _xmluiGetSelectedValues(self):
-        return [item.value for item in self.items if item.selected]
-
-    def _xmluiAddValues(self, values, select=True):
-        values = set(values).difference([c.value for c in self.items])
-        for v in values:
-            self.addValue(v, select)
-
-    def _xmluiOnChange(self, callback):
-        self._on_change = callback
-
-
-class JidsListWidget(ListWidget):
-    # TODO: real list dedicated to jids
-
-    def __init__(self, _xmlui_parent, jids, flags):
-        ListWidget.__init__(self, _xmlui_parent, [(j,j) for j in jids], [], flags)
-
-
-class PasswordWidget(xmlui.PasswordWidget, TextInput, TextInputOnChange):
-
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        TextInput.__init__(self, password=True, multiline=False,
-            text=value, readonly=read_only, size=(100,25), size_hint=(1,None))
-        TextInputOnChange.__init__(self)
-
-    def _xmluiSetValue(self, value):
-        self.text = value
-
-    def _xmluiGetValue(self):
-        return self.text
-
-
-class BoolWidget(xmlui.BoolWidget, Switch):
-
-    def __init__(self, _xmlui_parent, state, read_only=False):
-        Switch.__init__(self, active=state)
-        if read_only:
-            self.disabled = True
-
-    def _xmluiSetValue(self, value):
-        self.active = value
-
-    def _xmluiGetValue(self):
-        return C.BOOL_TRUE if self.active else C.BOOL_FALSE
-
-    def _xmluiOnChange(self, callback):
-        self.bind(active=lambda instance, value: callback(instance))
-
-
-class IntWidget(xmlui.IntWidget, TextInput, TextInputOnChange):
-
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        TextInput.__init__(self, text=value, input_filter='int', multiline=False)
-        TextInputOnChange.__init__(self)
-        if read_only:
-            self.disabled = True
-
-    def _xmluiSetValue(self, value):
-        self.text = value
-
-    def _xmluiGetValue(self):
-        return self.text
-
-
-## Containers ##
-
-
-class VerticalContainer(xmlui.VerticalContainer, GridLayout):
-
-    def __init__(self, xmlui_parent):
-        self.xmlui_parent = xmlui_parent
-        GridLayout.__init__(self)
-
-    def _xmluiAppend(self, widget):
-        self.add_widget(widget)
-
-
-class PairsContainer(xmlui.PairsContainer, GridLayout):
-
-    def __init__(self, xmlui_parent):
-        self.xmlui_parent = xmlui_parent
-        GridLayout.__init__(self)
-
-    def _xmluiAppend(self, widget):
-        self.add_widget(widget)
-
-
-class LabelContainer(PairsContainer, xmlui.LabelContainer):
-    pass
-
-
-class TabsPanelContainer(TabbedPanelItem):
-
-    def _xmluiAppend(self, widget):
-        self.add_widget(widget)
-
-
-class TabsContainer(xmlui.TabsContainer, TabbedPanel):
-
-    def __init__(self, xmlui_parent):
-        self.xmlui_parent = xmlui_parent
-        xmlui_panel = xmlui_parent
-        while not isinstance(xmlui_panel, XMLUIPanel):
-            xmlui_panel = xmlui_panel.xmlui_parent
-        xmlui_panel.addPostTreat(self._postTreat)
-        TabbedPanel.__init__(self, do_default_tab=False)
-
-    def _xmluiAddTab(self, label, selected):
-        tab = TabsPanelContainer(text=label)
-        self.add_widget(tab)
-        return tab
-
-    def _postTreat(self):
-        """bind minimum height of tabs' content so self.height is adapted"""
-        # we need to do this in postTreat because contents exists after UI construction
-        for t in self.tab_list:
-            t.content.bind(minimum_height=self._updateHeight)
-
-    def _updateHeight(self, instance, height):
-        """Called after UI is constructed (so height can be calculated)"""
-        # needed because TabbedPanel doesn't have a minimum_height property
-        self.height = max([t.content.minimum_height for t in self.tab_list]) + self.tab_height + 5
-
-
-class AdvancedListRow(GridLayout):
-    global_index = 0
-    index = properties.ObjectProperty()
-    selected = properties.BooleanProperty(False)
-
-    def __init__(self, **kwargs):
-        self.global_index = AdvancedListRow.global_index
-        AdvancedListRow.global_index += 1
-        super(AdvancedListRow, self).__init__(**kwargs)
-
-    def on_touch_down(self, touch):
-        if self.collide_point(*touch.pos):
-            parent = self.parent
-            while parent is not None and not isinstance(parent, AdvancedListContainer):
-                parent = parent.parent
-            if parent is None:
-                log.error(u"Can't find parent AdvancedListContainer")
-            else:
-                if parent.selectable:
-                    self.selected = parent._xmluiToggleSelected(self)
-
-        return super(AdvancedListRow, self).on_touch_down(touch)
-
-
-class AdvancedListContainer(xmlui.AdvancedListContainer, GridLayout):
-
-    def __init__(self, xmlui_parent, columns, selectable='no'):
-        self.xmlui_parent = xmlui_parent
-        GridLayout.__init__(self)
-        self._columns = columns
-        self.selectable = selectable != 'no'
-        self._current_row = None
-        self._selected = []
-        self._xmlui_select_cb = None
-
-    def _xmluiToggleSelected(self, row):
-        """inverse selection status of an AdvancedListRow
-
-        @param row(AdvancedListRow): row to (un)select
-        @return (bool): True if row is selected
-        """
-        try:
-            self._selected.remove(row)
-        except ValueError:
-            self._selected.append(row)
-            if self._xmlui_select_cb is not None:
-                self._xmlui_select_cb(self)
-            return True
-        else:
-            return False
-
-    def _xmluiAppend(self, widget):
-        if self._current_row is None:
-            log.error(u"No row set, ignoring append")
-            return
-        self._current_row.add_widget(widget)
-
-    def _xmluiAddRow(self, idx):
-        self._current_row = AdvancedListRow()
-        self._current_row.cols = self._columns
-        self._current_row.index = idx
-        self.add_widget(self._current_row)
-
-    def _xmluiGetSelectedWidgets(self):
-        return self._selected
-
-    def _xmluiGetSelectedIndex(self):
-        if not self._selected:
-            return None
-        return self._selected[0].index
-
-    def _xmluiOnSelect(self, callback):
-        """ Call callback with widget as only argument """
-        self._xmlui_select_cb = callback
-
-## Dialogs ##
-
-
-class NoteDialog(xmlui.NoteDialog):
-
-    def __init__(self, _xmlui_parent, title, message, level):
-        xmlui.NoteDialog.__init__(self, _xmlui_parent)
-        self.title, self.message, self.level = title, message, level
-
-    def _xmluiShow(self):
-        G.host.addNote(self.title, self.message, self.level)
-
-
-class FileDialog(xmlui.FileDialog, BoxLayout):
-    message = properties.ObjectProperty()
-
-    def __init__(self, _xmlui_parent, title, message, level, filetype):
-        xmlui.FileDialog.__init__(self, _xmlui_parent)
-        BoxLayout.__init__(self)
-        self.message.text = message
-        if filetype == C.XMLUI_DATA_FILETYPE_DIR:
-            self.file_chooser.dirselect = True
-
-    def _xmluiShow(self):
-        G.host.addNotifUI(self)
-
-    def _xmluiClose(self):
-        # FIXME: notif UI is not removed if dialog is not shown yet
-        G.host.closeUI()
-
-    def onSelect(self, path):
-        try:
-            path = path[0]
-        except IndexError:
-            path = None
-        if not path:
-            self._xmluiCancelled()
-        else:
-            self._xmluiValidated({'path': path})
-
-    def show(self, *args, **kwargs):
-        assert kwargs["force"]
-        G.host.showUI(self)
-
-
-## Factory ##
-
-
-class WidgetFactory(object):
-
-    def __getattr__(self, attr):
-        if attr.startswith("create"):
-            cls = globals()[attr[6:]]
-            return cls
-
-
-## Core ##
-
-
-class Title(Label):
-
-    def __init__(self, *args, **kwargs):
-        kwargs['size'] = (100, 25)
-        kwargs['size_hint'] = (1,None)
-        super(Title, self).__init__(*args, **kwargs)
-
-
-class FormButton(Button):
-    pass
-
-
-class XMLUIPanelGrid(GridLayout):
-    pass
-
-class XMLUIPanel(xmlui.XMLUIPanel, ScrollView):
-    widget_factory = WidgetFactory()
-
-    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, ignore=None, profile=C.PROF_KEY_NONE):
-        ScrollView.__init__(self)
-        self.close_cb = None
-        self._grid = XMLUIPanelGrid()
-        self._post_treats = []  # list of callback to call after UI is constructed
-        ScrollView.add_widget(self, self._grid)
-        xmlui.XMLUIPanel.__init__(self,
-                                  host,
-                                  parsed_xml,
-                                  title=title,
-                                  flags=flags,
-                                  callback=callback,
-                                  ignore=ignore,
-                                  profile=profile)
-
-    def add_widget(self, wid):
-        self._grid.add_widget(wid)
-
-    def setCloseCb(self, close_cb):
-        self.close_cb = close_cb
-
-    def _xmluiClose(self):
-        if self.close_cb is not None:
-            self.close_cb(self)
-        else:
-            G.host.closeUI()
-
-    def onParamChange(self, ctrl):
-        super(XMLUIPanel, self).onParamChange(ctrl)
-        self.save_btn.disabled = False
-
-    def addPostTreat(self, callback):
-        self._post_treats.append(callback)
-
-    def _postTreatCb(self):
-        for cb in self._post_treats:
-            cb()
-        del self._post_treats
-
-    def _saveButtonCb(self, button):
-        button.disabled = True
-        self.onSaveParams(button)
-
-    def constructUI(self, parsed_dom):
-        xmlui.XMLUIPanel.constructUI(self, parsed_dom, self._postTreatCb)
-        if self.xmlui_title:
-            self.add_widget(Title(text=self.xmlui_title))
-        self.add_widget(self.main_cont)
-        if self.type == 'form':
-            submit_btn = FormButton(text=_(u"Submit"))
-            submit_btn.bind(on_press=self.onFormSubmitted)
-            self.add_widget(submit_btn)
-            if not 'NO_CANCEL' in self.flags:
-                cancel_btn = FormButton(text=_(u"Cancel"))
-                cancel_btn.bind(on_press=self.onFormCancelled)
-                self.add_widget(cancel_btn)
-        elif self.type == 'param':
-            self.save_btn = FormButton(text=_(u"Save"), disabled=True)
-            self.save_btn.bind(on_press=self._saveButtonCb)
-            self.add_widget(self.save_btn)
-        self.add_widget(Widget())  # to have elements on the top
-
-    def show(self, *args, **kwargs):
-        if not self.user_action and not kwargs.get("force", False):
-            G.host.addNotifUI(self)
-        else:
-            G.host.showUI(self)
-
-
-class XMLUIDialog(xmlui.XMLUIDialog):
-    dialog_factory = WidgetFactory()
-
-
-xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel)
-xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog)
-create = xmlui.create
Binary file src/cagou/images/button.png has changed
Binary file src/cagou/images/button_selected.png has changed
--- a/src/cagou/kv/cagou_widget.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-# 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/>.
-
-
-<HeaderWidgetChoice>:
-    canvas.before:
-        Color:
-            rgba: 1, 1, 1, 1
-        BorderImage:
-            pos: self.pos
-            size: self.size
-            source: 'atlas://data/images/defaulttheme/button'
-    size_hint_y: None
-    height: dp(44)
-    Image:
-        size_hint: None, 1
-        source: root.plugin_info['icon_medium']
-        allow_stretch: True
-        width: self.texture_size[0]*self.height/(self.texture_size[1] or 1)
-    Label:
-        size_hint: 1, 1
-        text: root.plugin_info['name']
-        bold: True
-        text_size: self.size
-        halign: "center"
-        valign: "middle"
-
-<HeaderWidgetSelector>:
-    size_hint: 0.3, None
-    auto_width: False
-    canvas.before:
-        Color:
-            rgba: 0, 0, 0, 1
-        Rectangle:
-            pos: self.pos
-            size: self.size
-
-<CagouWidget>:
-    header_input: header_input
-    header_box: header_box
-    BoxLayout:
-        id: header_box
-        size_hint: 1, None
-        height: dp(32)
-        HeaderWidgetCurrent:
-            on_release: root.selector.open(self)
-            source: root.plugin_info['icon_small']
-            size_hint: None, 1
-            allow_stretch: True
-            width: self.texture_size[0]*self.height/(self.texture_size[1] or 1)
-        TextInput:
-            id: header_input
-            multiline: False
-            on_text_validate: root.onHeaderInput()
-            on_text: root.onHeaderInputComplete(*args)
--- a/src/cagou/kv/common.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-# 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/>.
-
-
-<JidWidget>:
-    size_hint: None, None
-    height: dp(70)
-    canvas.before:
-        Color:
-            rgba: 0.2, 0.2, 0.2, 1
-        Rectangle:
-            pos: self.pos
-            size: self.size
-    Image:
-        source: root.getImage(self)
-        size_hint: None, None
-        size: dp(64), dp(64)
-    Label:
-        bold: True
-        text: root.jid
-        text_size: self.size
-        halign: 'left'
-        valign: 'middle'
-        padding_x: dp(20)
--- a/src/cagou/kv/menu.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-# 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/>.
-
-#:import _ sat.core.i18n._
-
-<AboutContent>:
-    text_size: self.size
-    halign: "center"
-    valign: "middle"
-
-<AboutPopup>:
-    title_align: "center"
-    size_hint: 0.8, 0.8
-
-<MenuItem>:
-    # following is need to fix a bug in contextmenu
-    size_hint: 1, None
-
-<MenusWidget>:
-    height: self.children[0].height if self.children else 30
-
-<MainMenu>:
-    cancel_handler_widget: self.parent
-
-<TransferMenu>:
-    items_layout: items_layout
-    orientation: "vertical"
-    pos_hint: {"top": 0.5}
-    size_hint: 1, 0.5
-    canvas.before:
-        Color:
-            rgba: 0, 0, 0, 1
-        Rectangle:
-            pos: self.pos
-            size: self.size
-    BoxLayout:
-        size_hint: 1, None
-        height: dp(50)
-        ToggleButton:
-            id: upload_btn
-            text: _(u"upload")
-            group: "transfer"
-            state: "down"
-        ToggleButton:
-            id: send_btn
-            text: _(u"send")
-            group: "transfer"
-    Label:
-        size_hint: 1, 0.3
-        text: root.transfer_txt if upload_btn.state == 'down' else root.send_txt
-        text_size: self.size
-        halign: 'center'
-        valign: 'top'
-    ScrollView:
-        do_scroll_x: False
-        StackLayout:
-            size_hint: 1, None
-            padding: 20, 0
-            spacing: 15, 5
-            id: items_layout
-
-<TransferItem>:
-    orientation: "vertical"
-    size_hint: None, None
-    size: dp(50), dp(90)
-    IconButton:
-        source: root.plug_info['icon_medium']
-        allow_stretch: True
-        size_hint: 1, None
-        height: dp(50)
-    Label:
-        text: root.plug_info['name']
-        text_size: self.size
-        halign: "center"
-        valign: "top"
--- a/src/cagou/kv/profile_manager.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-# 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/>.
-
-
-<ProfileManager>:
-    Label:
-        text: "Profile Manager"
-        size_hint: 1,0.05
-
-<PMLabel@Label>:
-    size_hint: 1, None
-    height: sp(30)
-
-<PMInput@TextInput>:
-    multiline: False
-    size_hint: 1, None
-    height: sp(30)
-    write_tab: False
-
-<PMButton@Button>:
-    size_hint: 1, 0.2
-
-
-<NewProfileScreen>:
-    profile_name: profile_name
-    jid: jid
-    password: password
-
-    BoxLayout:
-        orientation: "vertical"
-
-        Label:
-            text: "Creation of a new profile"
-            bold: True
-            size_hint: 1, 0.1
-        Label:
-            text: root.error_msg
-            bold: True
-            size_hint: 1, 0.1
-            color: 1,0,0,1
-        GridLayout:
-            cols: 2
-
-            PMLabel:
-                text: "Profile name"
-            PMInput:
-                id: profile_name
-
-            PMLabel:
-                text: "JID"
-            PMInput:
-                id: jid
-
-            PMLabel:
-                text: "Password"
-            PMInput:
-                id: password
-                password: True
-
-            Widget:
-                size_hint: 1, 0.2
-
-            Widget:
-                size_hint: 1, 0.2
-
-            PMButton:
-                text: "OK"
-                on_press: root.doCreate()
-
-            PMButton:
-                text: "Cancel"
-                on_press:
-                    root.pm.screen_manager.transition.direction = 'right'
-                    root.pm.screen_manager.current = 'profiles'
-
-            Widget:
-
-
-<DeleteProfilesScreen>:
-    BoxLayout:
-        orientation: "vertical"
-
-        Label:
-            text: "Are you sure you want to delete the following profiles?"
-            size_hint: 1, 0.1
-
-        Label:
-            text: u'\n'.join([i.text for i in root.pm.profiles_screen.list_adapter.selection])
-            bold: True
-
-        Label:
-            text: u'/!\\ WARNING: this operation is irreversible'
-            color: 1,0,0,1
-            bold: True
-            size_hint: 1, 0.2
-
-        GridLayout:
-            cols: 2
-
-            Button:
-                text: "Delete"
-                size_hint: 1, 0.2
-                on_press: root.doDelete()
-
-            Button:
-                text: "Cancel"
-                size_hint: 1, 0.2
-                on_press:
-                    root.pm.screen_manager.transition.direction = 'right'
-                    root.pm.screen_manager.current = 'profiles'
-
-            Widget:
-
-
-<ProfilesScreen>:
-    layout: layout
-    BoxLayout:
-        id: layout
-        orientation: 'vertical'
-
-        Label:
-            text: "Select a profile or create a new one"
-            size_hint: 1,0.05
-
-        GridLayout:
-            cols: 2
-            size_hint: 1, 0.1
-            Button:
-                size_hint: 1, 0.1
-                text: "New"
-                on_press:
-                    root.pm.screen_manager.transition.direction = 'left'
-                    root.pm.screen_manager.current = 'new_profile'
-            Button:
-                disabled: not root.list_adapter.selection
-                text: "Delete"
-                size_hint: 1, 0.1
-                on_press:
-                    root.pm.screen_manager.transition.direction = 'left'
-                    root.pm.screen_manager.current = 'delete_profiles'
-
-
-<ConnectButton>:
-    text: "Connect"
-    size_hint: 1, 0.1
-    disabled: not self.profile_screen.list_adapter.selection
-    on_press: self.pm._onConnectProfiles()
-
-
-<ProfileItem>:
-    # FIXME: using cagou/images path for now, will use atlas later
-    background_normal: "cagou/images/button_selected.png" if self.is_selected else "cagou/images/button.png"
-    deselected_color: 1,1,1,1
-    selected_color: 1,1,1,1
-    color: 0,0,0,1
--- a/src/cagou/kv/root_widget.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-# 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/>.
-
-#:import IconButton cagou.core.common.IconButton
-
-# <NotifIcon>:
-#     source: app.expand("{media}/icons/muchoslava/png/cagou_profil_bleu_32.png")
-#     size_hint: None, None
-#     size: self.texture_size
-
-<Note>:
-    text: self.message
-
-<NoteDrop>:
-    canvas.before:
-        Color:
-            rgba: 1, 1, 1, 1
-        BorderImage:
-            pos: self.pos
-            size: self.size
-            source: 'atlas://data/images/defaulttheme/button'
-    size_hint: 1, None
-    text_size: self.width, None
-    halign: 'center'
-    height: self.texture_size[1]
-    padding: dp(2), dp(10)
-
-<NotesDrop>:
-    clear_btn: clear_btn.__self__
-    auto_width: False
-    size_hint: 0.8, None
-    canvas.before:
-        Color:
-            rgba: 0.8, 0.8, 0.8, 1
-        Rectangle:
-            pos: self.pos
-            size: self.size
-    Button:
-        id: clear_btn
-        text: "clear"
-        bold: True
-        size_hint: 1, None
-        height: dp(50)
-        on_release: del root.notes[:]; root.dismiss()
-
-<RootHeadWidget>:
-    manager: manager
-    notifs_icon: notifs_icon
-    size_hint: 1, None
-    height: dp(35)
-    IconButton:
-        source: app.expand("{media}/icons/muchoslava/png/cagou_profil_bleu_32.png") if root.notes else app.expand("{media}/misc/black.png")
-        allow_stretch: True
-        size_hint: None, 1
-        width: self.norm_image_size[0]
-        on_release: root.notes_drop.open(self) if root.notes else None
-    ScreenManager:
-        id: manager
-    NotifsIcon:
-        id: notifs_icon
-        allow_stretch: True
-        source: app.expand("{media}/icons/muchoslava/png/cagou_profil_bleu_32.png") if self.notifs else app.expand("{media}/misc/black.png")
-        size_hint: None, 1
-        width: self.norm_image_size[0]
-
-<RootMenus>:
-    size_hint: 1, None
-    pos_hint: {'top': 1}
-
-<CagouRootWidget>:
-    root_body: root_body
-    root_menus: root_menus
-    # main body
-    RootBody:
-        id: root_body
-        orientation: "vertical"
-        size_hint: 1, None
-        height: root.height - root_menus.height
-    # general menus
-    # need to be added at the end so it's drawed above other widgets
-    RootMenus:
-        id: root_menus
--- a/src/cagou/kv/simple_xhtml.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-# 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/>.
-
-
-<SimpleXHTMLWidgetEscapedText>:
-    size_hint: None, None
-    size: self.texture_size
-
-<SimpleXHTMLWidgetText>:
-    size_hint: None, None
-    size: self.texture_size
-
-<SimpleXHTMLWidgetImage>:
-    size_hint: None, None
--- a/src/cagou/kv/widgets_handler.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-# 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/>.
-
-<WHSplitter>:
-    border: (3, 3, 3, 3)
-    horizontal_suff: '_h' if self.horizontal else ''
-    background_normal: 'atlas://data/images/defaulttheme/splitter{}{}'.format('_disabled' if self.disabled else '', self.horizontal_suff)
-    background_down: 'atlas://data/images/defaulttheme/splitter_down{}{}'.format('_disabled' if self.disabled else '', self.horizontal_suff)
-    size_hint: (1, None) if self.horizontal else (None, 1)
-    size: (100, self.thickness) if self.horizontal else (self.thickness, 100)
-    Image:
-        pos: root.pos
-        size: root.size
-        allow_stretch: True
-        source: 'atlas://data/images/defaulttheme/splitter_grip' + root.horizontal_suff
--- a/src/cagou/kv/xmlui.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,128 +0,0 @@
-# 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/>.
-
-#:set common_height 30
-#:set button_height 50
-
-<EmptyWidget,TextWidget,LabelWidget,JidWidget,StringWidget,PasswordWidget,JidInputWidget>:
-    size_hint: 1, None
-    height: dp(common_height)
-
-
-<ButtonWidget>:
-    size_hint: 1, None
-    height: dp(button_height)
-
-
-<BoolWidget>:
-    size_hint: 1, 1
-
-
-<DividerWidget>:
-    size_hint: 1, None
-    height: dp(20)
-    canvas.before:
-        Color:
-            rgba: 1, 1, 1, 0.8
-        Line
-            points: 0, dp(10), self.width, dp(10)
-            width: dp(3)
-
-
-<ListWidgetItem>:
-    size_hint_y: None
-    height: dp(button_height)
-
-
-<ListWidget>:
-    size_hint: 1, None
-    height: dp(button_height)
-
-
-<AdvancedListRow>:
-    canvas.before:
-        Color:
-            rgba: 1, 1, 1, 0.2 if self.global_index%2 else 0.1
-        Rectangle:
-            pos: self.pos
-            size: self.size
-    size_hint: 1, None
-    height: self.minimum_height
-    rows: 1
-    canvas.after:
-        Color:
-            rgba: 0, 0, 1, 0.5 if self.selected else 0
-        Rectangle:
-            pos: self.pos
-            size: self.size
-
-
-<AdvancedListContainer>:
-    cols: 1
-    size_hint: 1, None
-    height: self.minimum_height
-
-
-<VerticalContainer>:
-    cols: 1
-    size_hint: 1, None
-    height: self.minimum_height
-
-
-<PairsContainer>:
-    cols: 2
-    size_hint: 1, None
-    height: self.minimum_height
-
-
-<TabsContainer>:
-    size_hint: 1, None
-    height: 100
-
-
-<FormButton>:
-    size_hint: 1, None
-    height: dp(button_height)
-
-
-<FileDialog>:
-    orientation: "vertical"
-    message: message
-    file_chooser: file_chooser
-    Label:
-        id: message
-        size_hint: 1, None
-        text_size: root.width, None
-        size: self.texture_size
-    FileChooserListView:
-        id: file_chooser
-    Button:
-        size_hint: 1, None
-        height: dp(50)
-        text: "choose"
-        on_release: root.onSelect(file_chooser.selection)
-    Button:
-        size_hint: 1, None
-        height: dp(50)
-        text: "cancel"
-        on_release: root.onCancel()
-
-
-
-<XMLUIPanelGrid>:
-    cols: 1
-    size_hint: 1, None
-    height: self.minimum_height
--- a/src/cagou/plugins/plugin_transfer_android_gallery.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-import sys
-import tempfile
-import os
-import os.path
-if sys.platform=="android":
-    from jnius import autoclass
-    from android import activity, mActivity
-
-    Intent = autoclass('android.content.Intent')
-    OpenableColumns = autoclass('android.provider.OpenableColumns')
-    PHOTO_GALLERY = 1
-    RESULT_OK = -1
-
-
-
-PLUGIN_INFO = {
-    "name": _(u"gallery"),
-    "main": "AndroidGallery",
-    "platforms": ('android',),
-    "external": True,
-    "description": _(u"upload a photo from photo gallery"),
-    "icon_medium": u"{media}/icons/muchoslava/png/gallery_50.png",
-}
-
-
-class AndroidGallery(object):
-
-    def __init__(self, callback, cancel_cb):
-        self.callback = callback
-        self.cancel_cb = cancel_cb
-        activity.bind(on_activity_result=self.on_activity_result)
-        intent = Intent()
-        intent.setType('image/*')
-        intent.setAction(Intent.ACTION_GET_CONTENT)
-        mActivity.startActivityForResult(intent, PHOTO_GALLERY);
-
-    def on_activity_result(self, requestCode, resultCode, data):
-        # TODO: move file dump to a thread or use async callbacks during file writting
-        if requestCode == PHOTO_GALLERY and resultCode == RESULT_OK:
-            if data is None:
-                log.warning(u"No data found in activity result")
-                self.cancel_cb(self, None)
-                return
-            uri = data.getData()
-
-            # we get filename in the way explained at https://developer.android.com/training/secure-file-sharing/retrieve-info.html
-            cursor = mActivity.getContentResolver().query(uri, None, None, None, None )
-            name_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
-            cursor.moveToFirst()
-            filename = cursor.getString(name_idx)
-
-            # we save data in a temporary file that we send to callback
-            # the file will be removed once upload is done (or if an error happens)
-            input_stream = mActivity.getContentResolver().openInputStream(uri)
-            tmp_dir = tempfile.mkdtemp()
-            tmp_file = os.path.join(tmp_dir, filename)
-            def cleaning():
-                os.unlink(tmp_file)
-                os.rmdir(tmp_dir)
-                log.debug(u'temporary file cleaned')
-            buff = bytearray(4096)
-            with open(tmp_file, 'wb') as f:
-                while True:
-                    ret = input_stream.read(buff, 0, 4096)
-                    if ret != -1:
-                        f.write(buff)
-                    else:
-                        break
-            input_stream.close()
-            self.callback(tmp_file, cleaning)
-        else:
-            self.cancel_cb(self, None)
--- a/src/cagou/plugins/plugin_transfer_android_photo.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-import sys
-import os
-import os.path
-import time
-if sys.platform == "android":
-    from plyer import camera
-    from jnius import autoclass
-    Environment = autoclass('android.os.Environment')
-else:
-    import tempfile
-
-
-PLUGIN_INFO = {
-    "name": _(u"take photo"),
-    "main": "AndroidPhoto",
-    "platforms": ('android',),
-    "external": True,
-    "description": _(u"upload a photo from photo application"),
-    "icon_medium": u"{media}/icons/muchoslava/png/camera_off_50.png",
-}
-
-
-class AndroidPhoto(object):
-
-    def __init__(self, callback, cancel_cb):
-        self.callback = callback
-        self.cancel_cb = cancel_cb
-        filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg", time.gmtime())
-        tmp_dir = self.getTmpDir()
-        tmp_file = os.path.join(tmp_dir, filename)
-        log.debug(u"Picture will be saved to {}".format(tmp_file))
-        camera.take_picture(tmp_file, self.callback)
-        # we don't delete the file, as it is nice to keep it locally
-
-    def getTmpDir(self):
-        if sys.platform == "android":
-            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
-            return dcim_path
-        else:
-            return tempfile.mkdtemp()
--- a/src/cagou/plugins/plugin_transfer_android_video.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-import sys
-import os
-import os.path
-import time
-if sys.platform == "android":
-    from plyer import camera
-    from jnius import autoclass
-    Environment = autoclass('android.os.Environment')
-else:
-    import tempfile
-
-
-PLUGIN_INFO = {
-    "name": _(u"take video"),
-    "main": "AndroidVideo",
-    "platforms": ('android',),
-    "external": True,
-    "description": _(u"upload a video from video application"),
-    "icon_medium": u"{media}/icons/muchoslava/png/film_camera_off_50.png",
-}
-
-
-class AndroidVideo(object):
-
-    def __init__(self, callback, cancel_cb):
-        self.callback = callback
-        self.cancel_cb = cancel_cb
-        filename = time.strftime("%Y-%m-%d_%H:%M:%S.mpg", time.gmtime())
-        tmp_dir = self.getTmpDir()
-        tmp_file = os.path.join(tmp_dir, filename)
-        log.debug(u"Video will be saved to {}".format(tmp_file))
-        camera.take_video(tmp_file, self.callback)
-        # we don't delete the file, as it is nice to keep it locally
-
-    def getTmpDir(self):
-        if sys.platform == "android":
-            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
-            return dcim_path
-        else:
-            return tempfile.mkdtemp()
--- a/src/cagou/plugins/plugin_transfer_file.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# 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/>.
-
-#:import expanduser os.path.expanduser
-#:import platform kivy.utils.platform
-
-
-<FileTransmitter>:
-    orientation: "vertical"
-    FileChooserListView:
-        id: filechooser
-        rootpath: "/" if platform == 'android' else expanduser('~')
-    Button:
-        text: "choose"
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.onTransmitOK(filechooser)
-    Button:
-        text: "cancel"
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.cancel_cb(root)
--- a/src/cagou/plugins/plugin_transfer_file.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-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": "FileTransmitter",
-    "description": _(u"transmit a local file"),
-    "icon_medium": u"{media}/icons/muchoslava/png/fichier_50.png",
-}
-
-
-class FileTransmitter(BoxLayout):
-    callback = properties.ObjectProperty()
-    cancel_cb = properties.ObjectProperty()
-
-    def onTransmitOK(self, filechooser):
-        if filechooser.selection:
-            file_path = filechooser.selection[0]
-            self.callback(file_path)
--- a/src/cagou/plugins/plugin_transfer_voice.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-# 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/>.
-
-#:import _ sat.core.i18n._
-#:import IconButton cagou.core.common.IconButton
-
-
-<VoiceRecorder>:
-    orientation: "vertical"
-    counter: counter
-    Label:
-        size_hint: 1, 0.4
-        text_size: self.size
-        halign: 'center'
-        valign: 'top'
-        text: _(u"Push the microphone button to start the record, then push it again to stop it.\nWhen you are satisfied, click on the transmit button")
-    Label:
-        id: counter
-        size_hint: 1, None
-        height: dp(60)
-        bold: True
-        font_size: sp(40)
-        text_size: self.size
-        text: u"{}:{:02}".format(root.time/60, root.time%60)
-        halign: 'center'
-        valign: 'middle'
-    BoxLayout:
-        size_hint: 1, None
-        height: dp(60)
-        Widget
-        IconButton:
-            source: app.expand("{media}/icons/muchoslava/png/") + ("micro_on_50.png" if root.recording else "micro_off_50.png")
-            allow_stretch: True
-            size_hint: None, None
-            size: dp(60), dp(60)
-            on_release: root.switchRecording()
-        IconButton:
-            opacity: 0 if root.recording or not root.time and not root.playing else 1
-            source: app.expand("{media}/icons/muchoslava/png/") + ("stop_50.png" if root.playing else "play_50.png")
-            allow_stretch: True
-            size_hint: None, None
-            size: dp(60), dp(60)
-            on_release: root.playRecord()
-        Widget
-    Widget:
-        size_hint: 1, None
-        height: dp(50)
-    Button:
-        text: _("transmit")
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.callback(root.audio.file_path)
-    Button:
-        text: _("cancel")
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.cancel_cb(root)
-    Widget
--- a/src/cagou/plugins/plugin_transfer_voice.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from kivy.uix.boxlayout import BoxLayout
-import sys
-import time
-from kivy.clock import Clock
-from kivy import properties
-if sys.platform == "android":
-    from plyer import audio
-
-
-PLUGIN_INFO = {
-    "name": _(u"voice"),
-    "main": "VoiceRecorder",
-    "platforms": ["android"],
-    "description": _(u"transmit a voice record"),
-    "icon_medium": u"{media}/icons/muchoslava/png/micro_off_50.png",
-}
-
-
-class VoiceRecorder(BoxLayout):
-    callback = properties.ObjectProperty()
-    cancel_cb = properties.ObjectProperty()
-    recording = properties.BooleanProperty(False)
-    playing = properties.BooleanProperty(False)
-    time = properties.NumericProperty(0)
-
-    def __init__(self, **kwargs):
-        super(VoiceRecorder, self).__init__(**kwargs)
-        self._started_at = None
-        self._counter_timer = None
-        self._play_timer = None
-        self.record_time = None
-        self.audio = audio
-        self.audio.file_path = "/sdcard/cagou_record.3gp"
-
-    def _updateTimer(self, dt):
-        self.time = int(time.time() - self._started_at)
-
-    def switchRecording(self):
-        if self.playing:
-            self._stopPlaying()
-        if self.recording:
-            try:
-                audio.stop()
-            except Exception as e:
-                # an exception can happen if record is pressed
-                # repeatedly in a short time (not a normal use)
-                log.warning(u"Exception on stop: {}".format(e))
-            self._counter_timer.cancel()
-            self.time = self.time + 1
-        else:
-            audio.start()
-            self._started_at = time.time()
-            self.time = 0
-            self._counter_timer = Clock.schedule_interval(self._updateTimer, 1)
-
-        self.recording = not self.recording
-
-    def _stopPlaying(self, dummy=None):
-        if self.record_time is None:
-            log.error("_stopPlaying should no be called when record_time is None")
-            return
-        audio.stop()
-        self.playing = False
-        self.time = self.record_time
-        if self._counter_timer is not None:
-            self._counter_timer.cancel()
-
-    def playRecord(self):
-        if self.recording:
-            return
-        if self.playing:
-            self._stopPlaying()
-        else:
-            try:
-                audio.play()
-            except Exception as e:
-                # an exception can happen in the same situation
-                # as for audio.stop() above (i.e. bad record)
-                log.warning(u"Exception on play: {}".format(e))
-                self.time = 0
-                return
-
-            self.playing = True
-            self.record_time = self.time
-            Clock.schedule_once(self._stopPlaying, self.time + 1)
-            self._started_at = time.time()
-            self.time = 0
-            self._counter_timer =  Clock.schedule_interval(self._updateTimer, 0.5)
--- a/src/cagou/plugins/plugin_wid_chat.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-# 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/>.
-
-#:import TransferMenu cagou.core.menu.TransferMenu
-#:import SimpleXHTMLWidget cagou.core.simple_xhtml.SimpleXHTMLWidget
-#:import _ sat.core.i18n._
-#:import C cagou.core.constants.Const
-
-
-<MessAvatar>:
-    size_hint: None, None
-    size: dp(30), dp(30)
-
-<MessagesWidget>:
-    cols: 1
-    padding: dp(10)
-    spacing: dp(5)
-    size_hint: 1, None
-    height: self.minimum_height
-
-<MessageWidget>:
-    cols: 1
-    mess_xhtml: mess_xhtml
-    padding: dp(10)
-    spacing: dp(5)
-    size_hint: 1, None
-    height: self.minimum_height
-    on_width: self.widthAdjust()
-    avatar: avatar
-    delivery: delivery
-    BoxLayout:
-        id: header_box
-        size_hint: 1, None
-        height: avatar.height if root.mess_data.type != C.MESS_TYPE_INFO else 0
-        opacity: 1 if root.mess_data.type != C.MESS_TYPE_INFO else 0
-        MessAvatar:
-            id: avatar
-        Label:
-            id: time_label
-            text_size: None, None
-            size_hint: None, None
-            size: self.texture_size
-            padding: dp(5), 0
-            text: u"{}, {}".format(root.mess_data.nick, root.mess_data.time_text)
-        Label:
-            id: delivery
-            text_size: None, None
-            size_hint: None, None
-            size: self.texture_size
-            padding: dp(5), 0
-            # XXX: DejaVuSans font is needed as check mark is not in Roboto
-            # this can be removed when Kivy will be able to handle fallback mechanism
-            # which will allow us to use fonts with more unicode characters
-            font_name: "DejaVuSans"
-            text: u''
-            color: 0, 1, 0, 1
-    BoxLayout:
-        # BoxLayout is needed here, else GridLayout won't let the Label choose its width
-        size_hint: 1, None
-        height: mess_xhtml.height
-        on_size: root.widthAdjust()
-        SimpleXHTMLWidget:
-            canvas.before:
-                Color:
-                    rgba: 1, 1, 1, 1
-                BorderImage:
-                    source: app.expand("{media}/misc/black.png") if root.mess_data.type == "info" else app.expand("{media}/misc/borders/{}.jpg", "blue" if root.mess_data.own_mess else "gray")
-                    pos: self.pos
-                    size: self.content_width, self.height
-            id: mess_xhtml
-            size_hint: 0.8, None
-            height: self.minimum_height
-            xhtml: root.message_xhtml or self.escape(root.message or u' ')
-            color: (0.74,0.74,0.24,1) if root.mess_data.type == "info" else (0, 0, 0, 1)
-            padding: root.mess_padding
-            bold: True if root.mess_data.type == "info" else False
-
-<Chat>:
-    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(40)
-        message_input: message_input
-        MessageInputWidget:
-            id: message_input
-            size_hint: 1, 1
-            hint_text: _(u"Enter your message here")
-            on_text_validate: root.onSend(args[0])
-        IconButton
-            # transfer 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: TransferMenu(callback=root.onTransferOK).show(self)
-
-<EncryptionButton>:
-    size_hint: None, 1
-    width: dp(30)
-    allow_stretch: True
-    source: self.getIconSource()
-
-<OtrButton@Button>:
-    size_hint: None, None
-    size: self.texture_size
-    padding: dp(5), dp(10)
-
-<OtrMenu>:
-    size_hint_x: None
-    width: start_btn.width
-    auto_width: False
-    canvas.before:
-        Color:
-            rgba: 0, 0, 0, 1
-        Rectangle:
-            pos: self.pos
-            size: self.size
-    OtrButton:
-        id: start_btn
-        text: _(u"Start/Refresh encrypted session")
-        on_release: root.otr_start()
-    OtrButton:
-        text: _(u"Finish encrypted session")
-        on_release: root.otr_end()
-    OtrButton:
-        text: _(u"Authenticate destinee")
-        on_release: root.otr_authenticate()
--- a/src/cagou/plugins/plugin_wid_chat.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,460 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from cagou.core.constants import Const as C
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.gridlayout import GridLayout
-from kivy.uix.textinput import TextInput
-from kivy.metrics import dp
-from kivy import properties
-from sat_frontends.quick_frontend import quick_widgets
-from sat_frontends.quick_frontend import quick_chat
-from sat_frontends.tools import jid
-from cagou.core import cagou_widget
-from cagou.core.image import Image
-from cagou.core.common import IconButton, JidWidget
-from kivy.uix.dropdown import DropDown
-from cagou import G
-import mimetypes
-
-
-PLUGIN_INFO = {
-    "name": _(u"chat"),
-    "main": "Chat",
-    "description": _(u"instant messaging with one person or a group"),
-    "icon_small": u"{media}/icons/muchoslava/png/chat_new_32.png",
-    "icon_medium": u"{media}/icons/muchoslava/png/chat_new_44.png"
-}
-
-# following const are here temporary, they should move to quick frontend
-OTR_STATE_UNTRUSTED = 'untrusted'
-OTR_STATE_TRUSTED = 'trusted'
-OTR_STATE_TRUST = (OTR_STATE_UNTRUSTED, OTR_STATE_TRUSTED)
-OTR_STATE_UNENCRYPTED = 'unencrypted'
-OTR_STATE_ENCRYPTED = 'encrypted'
-OTR_STATE_ENCRYPTION = (OTR_STATE_UNENCRYPTED, OTR_STATE_ENCRYPTED)
-
-
-class MessAvatar(Image):
-    pass
-
-
-class MessageWidget(GridLayout):
-    mess_data = properties.ObjectProperty()
-    mess_xhtml = properties.ObjectProperty()
-    mess_padding = (dp(5), dp(5))
-    avatar = properties.ObjectProperty()
-    delivery = properties.ObjectProperty()
-
-    def __init__(self, **kwargs):
-        # self must be registered in widgets before kv is parsed
-        kwargs['mess_data'].widgets.add(self)
-        super(MessageWidget, self).__init__(**kwargs)
-        avatar_path = self.mess_data.avatar
-        if avatar_path is not None:
-            self.avatar.source = avatar_path
-
-    @property
-    def chat(self):
-        """return parent Chat instance"""
-        return self.mess_data.parent
-
-    @property
-    def message(self):
-        """Return currently displayed message"""
-        return self.mess_data.main_message
-
-    @property
-    def message_xhtml(self):
-        """Return currently displayed message"""
-        return self.mess_data.main_message_xhtml
-
-    def widthAdjust(self):
-        """this widget grows up with its children"""
-        pass
-        # parent = self.mess_xhtml.parent
-        # padding_x = self.mess_padding[0]
-        # text_width, text_height = self.mess_xhtml.texture_size
-        # if text_width > parent.width:
-        #     self.mess_xhtml.text_size = (parent.width - padding_x, None)
-        #     self.text_max = text_width
-        # elif self.mess_xhtml.text_size[0] is not None and text_width  < parent.width - padding_x:
-        #     if text_width < self.text_max:
-        #         self.mess_xhtml.text_size = (None, None)
-        #     else:
-        #         self.mess_xhtml.text_size = (parent.width  - padding_x, None)
-
-    def update(self, update_dict):
-        if 'avatar' in update_dict:
-            self.avatar.source = update_dict['avatar']
-        if 'status' in update_dict:
-            status = update_dict['status']
-            self.delivery.text =  u'\u2714' if status == 'delivered' else u''
-
-
-class MessageInputBox(BoxLayout):
-    pass
-
-
-class MessageInputWidget(TextInput):
-
-    def _key_down(self, key, repeat=False):
-        displayed_str, internal_str, internal_action, scale = key
-        if internal_action == 'enter':
-            self.dispatch('on_text_validate')
-        else:
-            super(MessageInputWidget, self)._key_down(key, repeat)
-
-
-class MessagesWidget(GridLayout):
-    pass
-
-
-class EncryptionButton(IconButton):
-
-    def __init__(self, chat, **kwargs):
-        """
-        @param chat(Chat): Chat instance
-        """
-        self.chat = chat
-        # for now we do a simple ContextMenu as we have only OTR
-        self.otr_menu = OtrMenu(chat)
-        super(EncryptionButton, self).__init__(**kwargs)
-        self.bind(on_release=self.otr_menu.open)
-
-    def getIconSource(self):
-        """get path of icon"""
-        # TODO: use a more generic method to get icon name
-        if self.chat.otr_state_encryption == OTR_STATE_UNENCRYPTED:
-            icon_name = 'cadenas_ouvert'
-        else:
-            if self.chat.otr_state_trust == OTR_STATE_TRUSTED:
-                icon_name = 'cadenas_ferme'
-            else:
-                icon_name = 'cadenas_ferme_pas_authenthifie'
-
-        return G.host.app.expand("{media}/icons/muchoslava/png/" + icon_name + "_30.png")
-
-
-class OtrMenu(DropDown):
-
-    def __init__(self, chat, **kwargs):
-        """
-        @param chat(Chat): Chat instance
-        """
-        self.chat = chat
-        super(OtrMenu, self).__init__(**kwargs)
-
-    def otr_start(self):
-        self.dismiss()
-        G.host.launchMenu(
-            C.MENU_SINGLE,
-            (u"otr", u"start/refresh"),
-            {u'jid': unicode(self.chat.target)},
-            None,
-            C.NO_SECURITY_LIMIT,
-            self.chat.profile
-            )
-
-    def otr_end(self):
-        self.dismiss()
-        G.host.launchMenu(
-            C.MENU_SINGLE,
-            (u"otr", u"end session"),
-            {u'jid': unicode(self.chat.target)},
-            None,
-            C.NO_SECURITY_LIMIT,
-            self.chat.profile
-            )
-
-    def otr_authenticate(self):
-        self.dismiss()
-        G.host.launchMenu(
-            C.MENU_SINGLE,
-            (u"otr", u"authenticate"),
-            {u'jid': unicode(self.chat.target)},
-            None,
-            C.NO_SECURITY_LIMIT,
-            self.chat.profile
-            )
-
-
-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)
-        self.otr_state_encryption = OTR_STATE_UNENCRYPTED
-        self.otr_state_trust = OTR_STATE_UNTRUSTED
-        cagou_widget.CagouWidget.__init__(self)
-        if type_ == C.CHAT_ONE2ONE:
-            self.encryption_btn = EncryptionButton(self)
-            self.headerInputAddExtra(self.encryption_btn)
-        self.header_input.hint_text = u"{}".format(target)
-        self.host.addListener('progressError', self.onProgressError, profiles)
-        self.host.addListener('progressFinished', self.onProgressFinished, profiles)
-        self._waiting_pids = {}  # waiting progress ids
-        self.postInit()
-        # completion attribtues
-        self._hi_comp_data = None
-        self._hi_comp_last = None
-        self._hi_comp_dropdown = DropDown()
-        self._hi_comp_allowed = True
-
-    @classmethod
-    def factory(cls, plugin_info, target, profiles):
-        profiles = list(profiles)
-        if len(profiles) > 1:
-            raise NotImplementedError(u"Multi-profiles is not available yet for chat")
-        if target is None:
-            target = G.host.profiles[profiles[0]].whoami
-        return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=profiles)
-
-    ## header ##
-
-    def changeWidget(self, jid_):
-        """change current widget for a new one with given jid
-
-        @param jid_(jid.JID): jid of the widget to create
-        """
-        plugin_info = G.host.getPluginInfo(main=Chat)
-        factory = plugin_info['factory']
-        G.host.switchWidget(self, factory(plugin_info, jid_, profiles=[self.profile]))
-        self.header_input.text = ''
-
-    def onHeaderInput(self):
-        text = self.header_input.text.strip()
-        try:
-            if text.count(u'@') != 1 or text.count(u' '):
-                raise ValueError
-            jid_ = jid.JID(text)
-        except ValueError:
-            log.info(u"entered text is not a jid")
-            return
-
-        def discoCb(disco):
-            # TODO: check if plugin XEP-0045 is activated
-            if "conference" in [i[0] for i in disco[1]]:
-                G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, callback=self._mucJoinCb, errback=self._mucJoinEb)
-            else:
-                self.changeWidget(jid_)
-
-        def discoEb(failure):
-            log.warning(u"Disco failure, ignore this text: {}".format(failure))
-
-        G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, errback=discoEb)
-
-    def onHeaderInputCompleted(self, input_wid, completed_text):
-        self._hi_comp_allowed = False
-        input_wid.text = completed_text
-        self._hi_comp_allowed = True
-        self._hi_comp_dropdown.dismiss()
-        self.onHeaderInput()
-
-    def onHeaderInputComplete(self, wid, text):
-        if not self._hi_comp_allowed:
-            return
-        text = text.lstrip()
-        if not text:
-            self._hi_comp_data = None
-            self._hi_comp_last = None
-            return
-
-        profile = list(self.profiles)[0]
-
-        if self._hi_comp_data is None:
-            # first completion, we build the initial list
-            comp_data = self._hi_comp_data = []
-            self._hi_comp_last = ''
-            for jid_, jid_data in G.host.contact_lists[profile].all_iter:
-                comp_data.append((jid_, jid_data))
-            comp_data.sort(key=lambda datum: datum[0])
-        else:
-            comp_data = self._hi_comp_data
-
-        # XXX: dropdown is rebuilt each time backspace is pressed or if the text is changed,
-        #      it works OK, but some optimisation may be done here
-        dropdown = self._hi_comp_dropdown
-
-        if not text.startswith(self._hi_comp_last) or not self._hi_comp_last:
-            # text has changed or backspace has been pressed, we restart
-            dropdown.clear_widgets()
-
-            for jid_, jid_data in comp_data:
-                nick = jid_data.get(u'nick', u'')
-                if text in jid_.bare or text in nick.lower():
-                    btn = JidWidget(
-                        jid = jid_.bare,
-                        profile = profile,
-                        size_hint = (0.5, None),
-                        nick = nick,
-                        on_release=lambda dummy, txt=jid_.bare: self.onHeaderInputCompleted(wid, txt)
-                        )
-                    dropdown.add_widget(btn)
-        else:
-            # more chars, we continue completion by removing unwanted widgets
-            to_remove = []
-            for c in dropdown.children[0].children:
-                if text not in c.jid and text not in (c.nick or ''):
-                    to_remove.append(c)
-            for c in to_remove:
-                dropdown.remove_widget(c)
-
-        dropdown.open(wid)
-        self._hi_comp_last = text
-
-    def messageDataConverter(self, idx, mess_id):
-        return {"mess_data": self.messages[mess_id]}
-
-    def _onHistoryPrinted(self):
-        """Refresh or scroll down the focus after the history is printed"""
-        # self.adapter.data = self.messages
-        for mess_data in self.messages.itervalues():
-            self.appendMessage(mess_data)
-        super(Chat, self)._onHistoryPrinted()
-
-    def createMessage(self, message):
-        self.appendMessage(message)
-
-    def appendMessage(self, mess_data):
-        self.messages_widget.add_widget(MessageWidget(mess_data=mess_data))
-
-    def onSend(self, input_widget):
-        G.host.messageSend(
-            self.target,
-            {'': input_widget.text}, # TODO: handle language
-            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
-            profile_key=self.profile
-            )
-        input_widget.text = ''
-
-    def onProgressFinished(self, progress_id, metadata, profile):
-        try:
-            callback, cleaning_cb = self._waiting_pids.pop(progress_id)
-        except KeyError:
-            return
-        if cleaning_cb is not None:
-            cleaning_cb()
-        callback(metadata, profile)
-
-    def onProgressError(self, progress_id, err_msg, profile):
-        try:
-            dummy, cleaning_cb = self._waiting_pids[progress_id]
-        except KeyError:
-            return
-        else:
-            del self._waiting_pids[progress_id]
-            if cleaning_cb is not None:
-                cleaning_cb()
-        # TODO: display message to user
-        log.warning(u"Can't transfer file: {}".format(err_msg))
-
-    def fileTransferDone(self, metadata, profile):
-        log.debug("file transfered: {}".format(metadata))
-        extra = {}
-
-        # FIXME: Q&D way of getting file type, upload plugins shouls give it
-        mime_type = mimetypes.guess_type(metadata['url'])[0]
-        if mime_type is not None:
-            if mime_type.split(u'/')[0] == 'image':
-                # we generate url ourselves, so this formatting is safe
-                extra['xhtml'] = u"<img src='{url}' />".format(**metadata)
-
-        G.host.messageSend(
-            self.target,
-            {'': metadata['url']},
-            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
-            extra = extra,
-            profile_key=profile
-            )
-
-    def fileTransferCb(self, progress_data, cleaning_cb):
-        try:
-            progress_id = progress_data['progress']
-        except KeyError:
-            xmlui = progress_data['xmlui']
-            G.host.showUI(xmlui)
-        else:
-            self._waiting_pids[progress_id] = (self.fileTransferDone, cleaning_cb)
-
-    def onTransferOK(self, file_path, cleaning_cb, transfer_type):
-        if transfer_type == C.TRANSFER_UPLOAD:
-            G.host.bridge.fileUpload(
-                file_path,
-                "",
-                "",
-                {"ignore_tls_errors": C.BOOL_TRUE},  # FIXME: should not be the default
-                self.profile,
-                callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb)
-                )
-        elif transfer_type == C.TRANSFER_SEND:
-            if self.type == C.CHAT_GROUP:
-                log.warning(u"P2P transfer is not possible for group chat")
-                # TODO: show an error dialog to user, or better hide the send button for MUC
-            else:
-                jid_ = self.target
-                if not jid_.resource:
-                    jid_ = G.host.contact_lists[self.profile].getFullJid(jid_)
-                G.host.bridge.fileSend(unicode(jid_), file_path, "", "", profile=self.profile)
-                # TODO: notification of sending/failing
-        else:
-            raise log.error(u"transfer of type {} are not handled".format(transfer_type))
-
-
-    def _mucJoinCb(self, joined_data):
-        joined, room_jid_s, occupants, user_nick, subject, profile = joined_data
-        self.host.mucRoomJoinedHandler(*joined_data[1:])
-        jid_ = jid.JID(room_jid_s)
-        self.changeWidget(jid_)
-
-    def _mucJoinEb(self, failure):
-        log.warning(u"Can't join room: {}".format(failure))
-
-    def _onDelete(self):
-        self.host.removeListener('progressFinished', self.onProgressFinished)
-        self.host.removeListener('progressError', self.onProgressError)
-        return super(Chat, self).onDelete()
-
-    def onOTRState(self, state, dest_jid, profile):
-        assert profile in self.profiles
-        if state in OTR_STATE_ENCRYPTION:
-            self.otr_state_encryption = state
-        elif state in OTR_STATE_TRUST:
-            self.otr_state_trust = state
-        else:
-            log.error(_(u"Unknown OTR state received: {}".format(state)))
-            return
-        self.encryption_btn.source = self.encryption_btn.getIconSource()
-
-    def onDelete(self, force=False):
-        if force==True:
-            return self._onDelete()
-        if len(list(G.host.widgets.getWidgets(self.__class__, self.target, profiles=self.profiles))) > 1:
-            # we don't keep duplicate widgets
-            return self._onDelete()
-        return False
-
-
-PLUGIN_INFO["factory"] = Chat.factory
-quick_widgets.register(quick_chat.QuickChat, Chat)
--- a/src/cagou/plugins/plugin_wid_contact_list.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-# 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/>.
-
-<ContactListView>:
-    row_height: dp(50)
-
-<ContactItem>:
-    padding: dp(10), dp(3)
-    size_hint: 1, None
-    height: dp(50)
-    Avatar:
-        source: root.data.get('avatar', app.default_avatar)
-        size_hint: None, 1
-        width: dp(60)
-    Label:
-        id: jid_label
-        padding: dp(5), 0
-        text: root.jid
-        text_size: self.size
-        bold: True
-        valign: "middle"
--- a/src/cagou/plugins/plugin_wid_contact_list.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
-from sat_frontends.tools import jid
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.listview import ListView
-from kivy.adapters.listadapter import ListAdapter
-from kivy.metrics import dp
-from kivy import properties
-from cagou.core import cagou_widget
-from cagou.core import image
-from cagou import G
-
-
-PLUGIN_INFO = {
-    "name": _(u"contacts"),
-    "main": "ContactList",
-    "description": _(u"list of contacts"),
-    "icon_small": u"{media}/icons/muchoslava/png/contact_list_new_32.png",
-    "icon_medium": u"{media}/icons/muchoslava/png/contact_list_new_44.png"
-}
-
-
-class Avatar(image.Image):
-    pass
-
-
-class ContactItem(BoxLayout):
-    data = properties.DictProperty()
-    jid = properties.StringProperty('')
-
-    def __init__(self, **kwargs):
-        super(ContactItem, self).__init__(**kwargs)
-
-    def on_touch_down(self, touch):
-        if self.collide_point(*touch.pos):
-            # XXX: for now clicking on an item launch the corresponding Chat widget
-            #      behaviour should change in the future
-            try:
-                # FIXME: Q&D way to get chat plugin, should be replaced by a clean method
-                #        in host
-                plg_infos = [p for p in G.host.getPluggedWidgets() if 'chat' in p['import_name']][0]
-            except IndexError:
-                log.warning(u"No plugin widget found to display chat")
-            else:
-                factory = plg_infos['factory']
-                G.host.switchWidget(self, factory(plg_infos, jid.JID(self.jid), profiles=iter(G.host.profiles)))
-
-
-class ContactListView(ListView):
-    pass
-
-
-class ContactList(QuickContactList, cagou_widget.CagouWidget):
-
-    def __init__(self, host, target, profiles):
-        QuickContactList.__init__(self, G.host, profiles)
-        cagou_widget.CagouWidget.__init__(self)
-        self.adapter = ListAdapter(data={},
-                                   cls=ContactItem,
-                                   args_converter=self.contactDataConverter,
-                                   selection_mode='multiple',
-                                   allow_empty_selection=True,
-                                  )
-        self.add_widget(ContactListView(adapter=self.adapter))
-        self.postInit()
-        self.update()
-
-    def onHeaderInputComplete(self, wid, text):
-        # FIXME: this is implementation dependent, need to be done properly
-        items = self.children[0].children[0].children[0].children
-
-        for item in items:
-            if text not in item.ids.jid_label.text:
-                item.height = 0
-                item.opacity = 0
-            else:
-                item.height = dp(50)
-                item.opacity = 1
-
-    def contactDataConverter(self, idx, bare_jid):
-        return {"jid": bare_jid, "data": self._items_cache[bare_jid]}
-
-    def update(self, entities=None, type_=None, profile=None):
-        log.debug("update: %s %s %s" % (entities, type_, profile))
-        # FIXME: for now we update on each event
-        # if entities is None and type_ is None:
-        self._items_cache = self.items_sorted
-        self.adapter.data = self.items_sorted.keys()
-
--- a/src/cagou/plugins/plugin_wid_settings.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-# 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/>.
--- a/src/cagou/plugins/plugin_wid_settings.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from sat.core.constants import Const as C
-from sat_frontends.quick_frontend import quick_widgets
-from kivy.uix.label import Label
-from kivy.uix.widget import Widget
-from cagou.core import cagou_widget
-from cagou import G
-
-
-PLUGIN_INFO = {
-    "name": _(u"settings"),
-    "main": "Settings",
-    "description": _(u"Cagou/SàT settings"),
-    "icon_small": u"{media}/icons/muchoslava/png/settings_32.png",
-    "icon_medium": u"{media}/icons/muchoslava/png/settings_44.png"
-}
-
-
-class Settings(quick_widgets.QuickWidget, cagou_widget.CagouWidget):
-
-    def __init__(self, host, target, profiles):
-        quick_widgets.QuickWidget.__init__(self, G.host, target, profiles)
-        cagou_widget.CagouWidget.__init__(self)
-        # the Widget() avoid CagouWidget header to be down at the beginning
-        # then up when the UI is loaded
-        self.loading_widget = Widget()
-        self.add_widget(self.loading_widget)
-        G.host.bridge.getParamsUI(-1, C.APP_NAME, self.profile, callback=self.getParamsUICb, errback=self.getParamsUIEb)
-
-    def changeWidget(self, widget):
-        self.clear_widgets([self.loading_widget])
-        del self.loading_widget
-        self.add_widget(widget)
-
-    def getParamsUICb(self, xmlui):
-        G.host.actionManager({"xmlui": xmlui}, ui_show_cb=self.changeWidget, profile=self.profile)
-
-    def getParamsUIEb(self, failure):
-        self.changeWidget(Label(
-            text=_(u"Can't load parameters!"),
-            bold=True,
-            color=(1,0,0,1)))
-        G.host.showDialog(u"Can't load params UI", failure, "error")
--- a/src/cagou/plugins/plugin_wid_widget_selector.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-# 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/>.
-
-<WidgetSelItem>:
-    size_hint: (1, None)
-    height: dp(40)
-    Widget:
-    Image:
-        source: root.plugin_info["icon_medium"]
-        allow_stretch: True
-        keep_ratio: True
-        width: self.texture_size[0]
-    Label:
-        text: root.plugin_info["name"]
-        bold: True
-        font_size: sp(20)
-    Widget:
--- a/src/cagou/plugins/plugin_wid_widget_selector.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from cagou.core.constants import Const as C
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.listview import ListView
-from kivy.adapters.listadapter import ListAdapter
-from kivy import properties
-from kivy.uix.behaviors import ButtonBehavior
-from cagou.core import cagou_widget
-from cagou import G
-
-
-PLUGIN_INFO = {
-    "name": _(u"widget selector"),
-    "import_name": C.WID_SELECTOR,
-    "main": "WidgetSelector",
-    "description": _(u"show available widgets and allow to select one"),
-    "icon_small": u"{media}/icons/muchoslava/png/selector_new_32.png",
-    "icon_medium": u"{media}/icons/muchoslava/png/selector_new_44.png"
-}
-
-
-class WidgetSelItem(ButtonBehavior, BoxLayout):
-    plugin_info = properties.DictProperty()
-
-    def __init__(self, **kwargs):
-        super(WidgetSelItem, self).__init__(**kwargs)
-
-    def select(self, *args):
-        log.debug(u"widget selection: {}".format(self.plugin_info["name"]))
-        factory = self.plugin_info["factory"]
-        G.host.switchWidget(self, factory(self.plugin_info, None, profiles=iter(G.host.profiles)))
-
-    def deselect(self, *args):
-        pass
-
-
-class WidgetSelector(cagou_widget.CagouWidget):
-
-    def __init__(self):
-        super(WidgetSelector, self).__init__()
-        self.adapter = ListAdapter(
-            data=G.host.getPluggedWidgets(except_cls=self.__class__),
-            cls=WidgetSelItem,
-            args_converter=self.dataConverter,
-            selection_mode='single',
-            allow_empty_selection=True,
-           )
-        self.add_widget(ListView(adapter=self.adapter))
-
-    @classmethod
-    def factory(cls, plugin_info, target, profiles):
-        return cls()
-
-    def dataConverter(self, idx, plugin_info):
-        return {"plugin_info": plugin_info}
-
-
-PLUGIN_INFO["factory"] = WidgetSelector.factory
--- a/src/libs/garden/README	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-Files in this directory are from Kivy garden (http://kivy-garden.github.io/), and are included here to make distribution more easy.
-These modules may be sometime modified, we propose our patch upstream if this is the case.
-Files in this directory hierarchy (this directory and all subdirectories) all follow Kivy licence (MIT), check the corresponding LICENSE files.
-
-This licence *does not apply outside of this "garden" directory and subdirectories*. Cagou code itself is licenced under AGPLv3+ as specified in the LICENSE file in the root directory.
--- a/src/libs/garden/garden.contextmenu/.gitignore	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
--- a/src/libs/garden/garden.contextmenu/LICENSE	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2015 Kivy Garden
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
--- a/src/libs/garden/garden.contextmenu/README.md	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,221 +0,0 @@
-# garden.contextmenu
-
-Collection of classes for easy creating **context** and **application** menus.
-
-## Context Menu
-
-![Example of context menu](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/context-menu-01.png)
-
-Context menu is represented by `ContextMenu` widget that wraps all menu items as `ContextMenuTextItem` widgets. Context menus can be nested, each `ContextMenuTextItem` can contain maximum one `ContextMenu` widget.
-
-```python
-import kivy
-from kivy.app import App
-from kivy.lang import Builder
-import kivy.garden.contextmenu
-
-kv = """
-FloatLayout:
-    id: layout
-    Label:
-        pos: 10, self.parent.height - self.height - 10
-        text: "Left click anywhere outside the context menu to close it"
-        size_hint: None, None
-        size: self.texture_size
-
-    Button:
-        size_hint: None, None
-        pos_hint: {"center_x": 0.5, "center_y": 0.8 }
-        size: 300, 40
-        text: "Click me to show the context menu"
-        on_release: context_menu.show(*app.root_window.mouse_pos)
-
-    ContextMenu:
-        id: context_menu
-        visible: False
-        cancel_handler_widget: layout
-
-        ContextMenuTextItem:
-            text: "SubMenu #2"
-        ContextMenuTextItem:
-            text: "SubMenu #3"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "SubMenu #5"
-                ContextMenuTextItem:
-                    text: "SubMenu #6"
-                    ContextMenu:
-                        ContextMenuTextItem:
-                            text: "SubMenu #9"
-                        ContextMenuTextItem:
-                            text: "SubMenu #10"
-                        ContextMenuTextItem:
-                            text: "SubMenu #11"
-                        ContextMenuTextItem:
-                            text: "Hello, World!"
-                            on_release: app.say_hello(self.text)
-                        ContextMenuTextItem:
-                            text: "SubMenu #12"
-                ContextMenuTextItem:
-                    text: "SubMenu #7"
-        ContextMenuTextItem:
-            text: "SubMenu #4"
-"""
-
-class MyApp(App):
-    def build(self):
-        self.title = 'Simple context menu example'
-        return Builder.load_string(kv)
-
-    def say_hello(self, text):
-        print(text)
-        self.root.ids['context_menu'].hide()
-
-if __name__ == '__main__':
-    MyApp().run()
-```
-
-Arrows that symbolize that an item has sub menu is created automatically. `ContextMenuTextItem` inherits from [ButtonBehavior](http://kivy.org/docs/api-kivy.uix.behaviors.html#kivy.uix.behaviors.ButtonBehavior) so you can use `on_release` to bind actions to it.
-
-The root context menu can use `cancel_handler_widget` parameter. This adds `on_touch_down` event to it that closes the menu when you click anywhere outside the menu.
-
-
-## Application Menu
-
-![Example of application menu](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/app-menu-01.png)
-
-Creating application menus is very similar to context menus. Use `AppMenu` and `AppMenuTextItem` widgets to create the top level menu. Then each `AppMenuTextItem` can contain one `ContextMenu` widget as we saw above. `AppMenuTextItem` without `ContextMenu` are disabled by default
-
-```python
-import kivy
-from kivy.app import App
-from kivy.lang import Builder
-import kivy.garden.contextmenu
-
-kv = """
-FloatLayout:
-    id: layout
-    AppMenu:
-        id: app_menu
-        top: root.height
-        cancel_handler_widget: layout
-
-        AppMenuTextItem:
-            text: "Menu #1"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "Item #11"
-                ContextMenuTextItem:
-                    text: "Item #12"
-        AppMenuTextItem:
-            text: "Menu Menu Menu #2"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "Item #21"
-                ContextMenuTextItem:
-                    text: "Item #22"
-                ContextMenuTextItem:
-                    text: "ItemItemItem #23"
-                ContextMenuTextItem:
-                    text: "Item #24"
-                    ContextMenu:
-                        ContextMenuTextItem:
-                            text: "Item #241"
-                        ContextMenuTextItem:
-                            text: "Hello, World!"
-                            on_release: app.say_hello(self.text)
-                        # ...
-                ContextMenuTextItem:
-                    text: "Item #5"
-        AppMenuTextItem:
-            text: "Menu Menu #3"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "SubMenu #31"
-                ContextMenuDivider:
-                ContextMenuTextItem:
-                    text: "SubMenu #32"
-                # ...
-        AppMenuTextItem:
-            text: "Menu #4"
-    # ...
-    # The rest follows as usually
-"""
-
-class MyApp(App):
-    def build(self):
-        self.title = 'Simple app menu example'
-        return Builder.load_string(kv)
-
-    def say_hello(self, text):
-        print(text)
-        self.root.ids['app_menu'].close_all()
-
-if __name__ == '__main__':
-    MyApp().run()
-```
-
-## All classes
-
-`garden.contextmenu` provides you with a set of classes and mixins for creating your own customised menu items for both context and application menus.
-
-### context_menu.AbstractMenu
-
-Mixin class that represents basic functionality for all menus. It cannot be used by itself and needs to be extended with a layout. Provides `cancel_handler_widget` property. See [AppMenu](https://github.com/kivy-garden/garden.contextmenu/blob/master/app_menu.py) or [ContextMenu](https://github.com/kivy-garden/garden.contextmenu/blob/master/context_menu.py).
-
-### context_menu.ContextMenu
-
-Implementation of a context menu.
-
-### context_menu.AbstractMenuItem
-
-Mixin class that represents a single menu item. Needs to be extended to be any useful. It's a base class for all menu items for both context and application menus.
-
-If you want to extend this class you need to override the `content_width` property which tells the parent `ContextMenu` what is the expected width of this item. It needs to know this to set it's own width.
-
-### context_menu.ContextMenuItem
-
-Single context menu item. Automatically draws an arrow if contains a `ContextMenu` children. If you want to create a custom menu item extend this class.
-
-### context_menu.AbstractMenuItemHoverable
-
-Mixin class that makes any class that inherits `ContextMenuItem` to change background color on mouse hover.
-
-### context_menu.ContextMenuText
-
-Menu item with `Label` widget without any extra functionality.
-
-### context_menu.ContextMenuDivider
-
-Menu widget that splits two parts of a context/app menu.
-
-![Example of ContextMenuDivider without text](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/menu-divider-01.png)
-
-It also contains an instance of `Label` which is not visible if you don't set it any text.
-
-```python
-ContextMenuTextItem:
-    text: "SubMenu #33"
-ContextMenuDivider:
-    text: "More options"
-ContextMenuTextItem:
-    text: "SubMenu #34"
-```
-
-![Example of ContextMenuDivider with text](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/menu-divider-02.png)
-
-### context_menu.ContextMenuTextItem
-
-Menu item with text. You'll be most of the time just fine using this class for all your menu items. You can also see it used in [all examples here](https://github.com/kivy-garden/garden.contextmenu/tree/master/examples).  Contains a `Label` widget and copies `text`, `font_size` and `color` properties to it automatically.
-
-### app_menu.AppMenu
-
-Application menu widget. By default it fills the entire parent's width.
-
-### app_menu.AppMenuTextItem
-
-Application menu item width text. Contains a `Label` widget and copies `text`, `font_size` and `color` properties to it automatically.
-
-# License
-
-garden.contextmenu is licensed under MIT license.
\ No newline at end of file
--- a/src/libs/garden/garden.contextmenu/__init__.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-from .context_menu import ContextMenu, \
-    AbstractMenu, \
-    AbstractMenuItem, \
-    AbstractMenuItemHoverable, \
-    ContextMenuItem, \
-    ContextMenuDivider, \
-    ContextMenuText, \
-    ContextMenuTextItem
-
-from .app_menu import AppMenu, \
-    AppMenuTextItem
\ No newline at end of file
--- a/src/libs/garden/garden.contextmenu/app_menu.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-<AppMenu>:
-    height: dp(30)
-    size_hint: 1, None
-
-    canvas.before:
-        Color:
-            rgb: 0.2, 0.2, 0.2
-        Rectangle:
-            pos: self.pos
-            size: self.size
-
-
-<AppMenuTextItem>:
-    disabled: True
-    size_hint: None, None
-    on_children: self._check_submenu()
-    font_size: '15sp'
-    background_normal: ""
-    background_down: ""
-    background_color: (0.2, 0.71, 0.9, 1.0) if self.state == 'down' else (0.2, 0.2, 0.2, 1.0)
-    background_disabled_normal: ""
-    background_disabled_down: ""
-    border: (0, 0, 0, 0)
-    size: self.texture_size[0], dp(30)
-    padding_x: dp(10)
--- a/src/libs/garden/garden.contextmenu/app_menu.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-from kivy.uix.relativelayout import RelativeLayout
-from kivy.uix.stacklayout import StackLayout
-from kivy.uix.behaviors import ToggleButtonBehavior
-from kivy.uix.togglebutton import ToggleButton
-from kivy.lang import Builder
-import kivy.properties as kp
-import os
-
-from .context_menu import AbstractMenu, AbstractMenuItem, AbstractMenuItemHoverable
-
-
-class AppMenu(StackLayout, AbstractMenu):
-    bounding_box = kp.ObjectProperty(None)
-
-    def __init__(self, *args, **kwargs):
-        super(AppMenu, self).__init__(*args, **kwargs)
-        self.hovered_menu_item = None
-
-    def update_height(self):
-        max_height = 0
-        for widget in self.menu_item_widgets:
-            if widget.height > max_height:
-                max_height = widget.height
-        return max_height
-
-    def on_children(self, obj, new_children):
-        for w in new_children:
-            # bind events that update app menu height when any of its children resize
-            w.bind(on_size=self.update_height)
-            w.bind(on_height=self.update_height)
-
-    def get_context_menu_root_parent(self):
-        return self
-
-    def self_or_submenu_collide_with_point(self, x, y):
-        collide_widget = None
-
-        # Iterate all siblings and all children
-        for widget in self.menu_item_widgets:
-            widget_pos = widget.to_window(0, 0)
-            if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled:
-                if self.hovered_menu_item is None:
-                    self.hovered_menu_item = widget
-
-                if self.hovered_menu_item != widget:
-                    self.hovered_menu_item = widget
-                    for sibling in widget.siblings:
-                        sibling.state = 'normal'
-
-                    if widget.state == 'normal':
-                        widget.state = 'down'
-                        widget.on_release()
-
-                    for sib in widget.siblings:
-                        sib.hovered = False
-            elif widget.get_submenu() is not None and not widget.get_submenu().visible:
-                widget.state = 'normal'
-
-        return collide_widget
-
-    def close_all(self):
-        for submenu in [w.get_submenu() for w in self.menu_item_widgets if w.get_submenu() is not None]:
-            submenu.hide()
-        for w in self.menu_item_widgets:
-            w.state = 'normal'
-
-    def hide_app_menus(self, obj, pos):
-        if not self.collide_point(pos.x, pos.y):
-            for w in [w for w in self.menu_item_widgets if not w.disabled and w.get_submenu().visible]:
-                submenu = w.get_submenu()
-                if submenu.self_or_submenu_collide_with_point(pos.x, pos.y) is None:
-                    self.close_all()
-                    self._cancel_hover_timer()
-
-
-class AppMenuTextItem(ToggleButton, AbstractMenuItem):
-    label = kp.ObjectProperty(None)
-    text = kp.StringProperty('')
-    font_size = kp.NumericProperty(14)
-    color = kp.ListProperty([1, 1, 1, 1])
-
-    def on_release(self):
-        submenu = self.get_submenu()
-
-        if self.state == 'down':
-            root = self._root_parent
-            submenu.bounding_box_widget = root.bounding_box if root.bounding_box else root.parent
-
-            submenu.bind(visible=self.on_visible)
-            submenu.show(self.x, self.y - 1)
-
-            for sibling in self.siblings:
-                if sibling.get_submenu() is not None:
-                    sibling.state = 'normal'
-                    sibling.get_submenu().hide()
-
-            self.parent._setup_hover_timer()
-        else:
-            self.parent._cancel_hover_timer()
-            submenu.hide()
-
-    def on_visible(self, *args):
-        submenu = self.get_submenu()
-        if self.width > submenu.get_max_width():
-            submenu.width = self.width
-
-    def _check_submenu(self):
-        super(AppMenuTextItem, self)._check_submenu()
-        self.disabled = (self.get_submenu() is None)
-
-    # def on_mouse_down(self):
-    #     print('on_mouse_down')
-    #     return True
-
-
-_path = os.path.dirname(os.path.realpath(__file__))
-Builder.load_file(os.path.join(_path, 'app_menu.kv'))
--- a/src/libs/garden/garden.contextmenu/context_menu.kv	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +0,0 @@
-<ContextMenu>:
-    cols: 1
-    size_hint: None, None
-    spacing: 0, 0
-    spacer: _spacer
-    on_visible: self._on_visible(args[1])
-    on_parent: self._on_visible(self.visible)
-
-    Widget:
-        id: _spacer
-        size_hint: 1, None
-        height: dp(3)
-        canvas.before:
-            Color:
-                rgb: 0.2, 0.71, 0.9
-            Rectangle:
-                pos: self.pos
-                size: self.size
-
-
-<ContextMenuItem>:
-    size_hint: None, None
-    submenu_arrow: _submenu_arrow
-    on_children: self._check_submenu()
-    on_parent: self._check_submenu()
-    canvas.before:
-        Color:
-            rgb: (0.15, 0.15, 0.15)
-        Rectangle:
-            pos: 0,0
-            size: self.size
-
-    Widget:
-        id: _submenu_arrow
-        size_hint: None, None
-        width: dp(6)
-        height: dp(11)
-        pos: self.parent.width - self.width - dp(5), (self.parent.height - self.height) / 2
-        canvas.before:
-            Translate:
-                xy: self.pos
-            Color:
-                rgb: (0.35, 0.35, 0.35) if self.disabled else (1, 1, 1)
-            Triangle:
-                points: [0,0, self.width,self.height/2, 0,self.height]
-            Translate:
-                xy: (-self.pos[0], -self.pos[1])
-
-
-<ContextMenuText>:
-    label: _label
-    width: self.parent.width if self.parent else 0
-    height: dp(26)
-    font_size: '15sp'
-
-    Label:
-        pos: 0,0
-        id: _label
-        text: self.parent.text
-        color: self.parent.color
-        font_size: self.parent.font_size
-        padding: dp(10), 0
-        halign: 'left'
-        valign: 'middle'
-        size: self.texture_size
-        size_hint: None, 1
-
-
-<AbstractMenuItemHoverable>:
-    on_hovered: self._on_hovered(args[1])
-    canvas.before:
-        Color:
-            rgb: (0.25, 0.25, 0.25) if self.hovered and not self.disabled else (0.15, 0.15, 0.15)
-        Rectangle:
-            pos: 0,0
-            size: self.size
-
-
-<ContextMenuDivider>:
-    font_size: '10sp'
-    height: dp(20) if len(self.label.text) > 0 else dp(1)
-    canvas.before:
-        Color:
-            rgb: (0.25, 0.25, 0.25)
-        Rectangle:
-            pos: 0,self.height - 1
-            size: self.width, 1
-
-
-<ContextMenuButton@Button>:
-    size_hint: None, None
-    font_size: '12sp'
-    height: dp(20)
-    background_normal: ""
-    background_down: ""
-    background_color: 0.2, 0.71, 0.9, 1.0
-    border: (0, 0, 0, 0)
-    on_press: self.background_color = 0.10, 0.6, 0.8, 1.0
-    on_release: self.background_color = 0.2, 0.71, 0.9, 1.0
-
-
-<ContextMenuToggleButton@ToggleButton>:
-    size_hint: None, None
-    font_size: '12sp'
-    size: dp(30), dp(20)
-    background_normal: ""
-    background_down: ""
-    background_color: (0.2, 0.71, 0.9, 1.0) if self.state == 'down' else (0.25, 0.25, 0.25, 1.0)
-    border: (0, 0, 0, 0)
-    on_press: self.background_color = 0.10, 0.6, 0.8, 1.0
-    on_release: self.background_color = 0.2, 0.71, 0.9, 1.0
-
-
-<ContextMenuSmallLabel@Label>:
-    size: self.texture_size[0], dp(18)
-    size_hint: None, None
-    font_size: '12sp'
-
-
-<ContextMenuTextInput@TextInput>:
-    size_hint: None, None
-    height: dp(22)
-    font_size: '12sp'
-    padding: dp(7), dp(3)
-    multiline: False
--- a/src/libs/garden/garden.contextmenu/context_menu.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,282 +0,0 @@
-from kivy.uix.gridlayout import GridLayout
-from kivy.uix.relativelayout import RelativeLayout
-from kivy.core.window import Window
-from kivy.uix.behaviors import ButtonBehavior
-from kivy.lang import Builder
-from kivy.clock import Clock
-from functools import partial
-
-import kivy.properties as kp
-import os
-
-
-class AbstractMenu(object):
-    cancel_handler_widget = kp.ObjectProperty(None)
-    bounding_box_widget = kp.ObjectProperty(None)
-
-    def __init__(self, *args, **kwargs):
-        self.clock_event = None
-
-    def add_item(self, widget):
-        self.add_widget(widget)
-
-    def add_text_item(self, text, on_release=None):
-        item = ContextMenuTextItem(text=text)
-        if on_release:
-            item.bind(on_release=on_release)
-        self.add_item(item)
-
-    def get_height(self):
-        height = 0
-        for widget in self.children:
-            height += widget.height
-        return height
-
-    def hide_submenus(self):
-        for widget in self.menu_item_widgets:
-            widget.hovered = False
-            widget.hide_submenu()
-
-    def self_or_submenu_collide_with_point(self, x, y):
-        raise NotImplementedError()
-
-    def on_cancel_handler_widget(self, obj, widget):
-        self.cancel_handler_widget.bind(on_touch_down=self.hide_app_menus)
-
-    def hide_app_menus(self, obj, pos):
-        raise NotImplementedError()
-
-    @property
-    def menu_item_widgets(self):
-        """
-        Return all children that are subclasses of ContextMenuItem
-        """
-        return [w for w in self.children if issubclass(w.__class__, AbstractMenuItem)]
-
-    def _setup_hover_timer(self):
-        if self.clock_event is None:
-            self.clock_event = Clock.schedule_interval(partial(self._check_mouse_hover), 0.05)
-
-    def _check_mouse_hover(self, obj):
-        self.self_or_submenu_collide_with_point(*Window.mouse_pos)
-
-    def _cancel_hover_timer(self):
-        if self.clock_event:
-            self.clock_event.cancel()
-            self.clock_event = None
-
-
-class ContextMenu(GridLayout, AbstractMenu):
-    visible = kp.BooleanProperty(False)
-    spacer = kp.ObjectProperty(None)
-
-    def __init__(self, *args, **kwargs):
-        super(ContextMenu, self).__init__(*args, **kwargs)
-        self.orig_parent = None
-        # self._on_visible(False)
-
-    def hide(self):
-        self.visible = False
-
-    def show(self, x=None, y=None):
-        self.visible = True
-        self._add_to_parent()
-        self.hide_submenus()
-
-        root_parent = self.bounding_box_widget if self.bounding_box_widget is not None else self.get_context_menu_root_parent()
-        if root_parent is None:
-            return
-
-        point_relative_to_root = root_parent.to_local(*self.to_window(x, y))
-
-        # Choose the best position to open the menu
-        if x is not None and y is not None:
-            if point_relative_to_root[0] + self.width < root_parent.width:
-                pos_x = x
-            else:
-                pos_x = x - self.width
-                if issubclass(self.parent.__class__, AbstractMenuItem):
-                    pos_x -= self.parent.width
-
-            if point_relative_to_root[1] - self.height < 0:
-                pos_y = y
-                if issubclass(self.parent.__class__, AbstractMenuItem):
-                    pos_y -= self.parent.height + self.spacer.height
-            else:
-                pos_y = y - self.height
-
-            self.pos = pos_x, pos_y
-
-    def self_or_submenu_collide_with_point(self, x, y):
-        queue = self.menu_item_widgets
-        collide_widget = None
-
-        # Iterate all siblings and all children
-        while len(queue) > 0:
-            widget = queue.pop(0)
-            submenu = widget.get_submenu()
-            if submenu is not None and widget.hovered:
-                queue += submenu.menu_item_widgets
-
-            widget_pos = widget.to_window(0, 0)
-            if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled:
-                widget.hovered = True
-
-                collide_widget = widget
-                for sib in widget.siblings:
-                    sib.hovered = False
-            elif submenu and submenu.visible:
-                widget.hovered = True
-            else:
-                widget.hovered = False
-
-        return collide_widget
-
-    def _on_visible(self, new_visibility):
-        if new_visibility:
-            self.size = self.get_max_width(), self.get_height()
-            self._add_to_parent()
-            # @todo: Do we need to remove self from self.parent.__context_menus? Probably not.
-
-        elif self.parent and not new_visibility:
-            self.orig_parent = self.parent
-
-            '''
-            We create a set that holds references to all context menus in the parent widget.
-            It's necessary to keep at least one reference to this context menu. Otherwise when
-            removed from parent it might get de-allocated by GC.
-            '''
-            if not hasattr(self.parent, '_ContextMenu__context_menus'):
-                self.parent.__context_menus = set()
-            self.parent.__context_menus.add(self)
-
-            self.parent.remove_widget(self)
-            self.hide_submenus()
-            self._cancel_hover_timer()
-
-    def _add_to_parent(self):
-        if not self.parent:
-            self.orig_parent.add_widget(self)
-            self.orig_parent = None
-
-            # Create the timer on the outer most menu object
-            if self._get_root_context_menu() == self:
-                self._setup_hover_timer()
-
-    def get_max_width(self):
-        max_width = 0
-        for widget in self.menu_item_widgets:
-            width = widget.content_width if widget.content_width is not None else widget.width
-            if width is not None and width > max_width:
-                max_width = width
-
-        return max_width
-
-    def get_context_menu_root_parent(self):
-        """
-        Return the bounding box widget for positioning sub menus. By default it's root context menu's parent.
-        """
-        if self.bounding_box_widget is not None:
-            return self.bounding_box_widget
-        root_context_menu = self._get_root_context_menu()
-        return root_context_menu.bounding_box_widget if root_context_menu.bounding_box_widget else root_context_menu.parent
-
-    def _get_root_context_menu(self):
-        """
-        Return the outer most context menu object
-        """
-        root = self
-        while issubclass(root.parent.__class__, ContextMenuItem) \
-                or issubclass(root.parent.__class__, ContextMenu):
-            root = root.parent
-        return root
-
-    def hide_app_menus(self, obj, pos):
-        return self.self_or_submenu_collide_with_point(pos.x, pos.y) is None and self.hide()
-
-
-class AbstractMenuItem(object):
-    submenu = kp.ObjectProperty(None)
-
-    def get_submenu(self):
-        return self.submenu if self.submenu != "" else None
-
-    def show_submenu(self, x=None, y=None):
-        if self.get_submenu():
-            self.get_submenu().show(*self._root_parent.to_local(x, y))
-
-    def hide_submenu(self):
-        submenu = self.get_submenu()
-        if submenu:
-            submenu.visible = False
-            submenu.hide_submenus()
-
-    def _check_submenu(self):
-        if self.parent is not None and len(self.children) > 0:
-            submenus = [w for w in self.children if issubclass(w.__class__, ContextMenu)]
-            if len(submenus) > 1:
-                raise Exception('Menu item (ContextMenuItem) can have maximum one submenu (ContextMenu)')
-            elif len(submenus) == 1:
-                self.submenu = submenus[0]
-
-    @property
-    def siblings(self):
-        return [w for w in self.parent.children if issubclass(w.__class__, AbstractMenuItem) and w != self]
-
-    @property
-    def content_width(self):
-        return None
-
-    @property
-    def _root_parent(self):
-        return self.parent.get_context_menu_root_parent()
-
-
-class ContextMenuItem(RelativeLayout, AbstractMenuItem):
-    submenu_arrow = kp.ObjectProperty(None)
-
-    def _check_submenu(self):
-        super(ContextMenuItem, self)._check_submenu()
-        if self.get_submenu() is None:
-            self.submenu_arrow.opacity = 0
-        else:
-            self.submenu_arrow.opacity = 1
-
-
-class AbstractMenuItemHoverable(object):
-    hovered = kp.BooleanProperty(False)
-
-    def _on_hovered(self, new_hovered):
-        if new_hovered:
-            spacer_height = self.parent.spacer.height if self.parent.spacer else 0
-            self.show_submenu(self.width, self.height + spacer_height)
-        else:
-            self.hide_submenu()
-
-
-class ContextMenuText(ContextMenuItem):
-    label = kp.ObjectProperty(None)
-    submenu_postfix = kp.StringProperty(' ...')
-    text = kp.StringProperty('')
-    font_size = kp.NumericProperty(14)
-    color = kp.ListProperty([1,1,1,1])
-
-    def __init__(self, *args, **kwargs):
-        super(ContextMenuText, self).__init__(*args, **kwargs)
-
-    @property
-    def content_width(self):
-        # keep little space for eventual arrow for submenus
-        return self.label.texture_size[0] + 10
-
-
-class ContextMenuDivider(ContextMenuText):
-    pass
-
-
-class ContextMenuTextItem(ButtonBehavior, ContextMenuText, AbstractMenuItemHoverable):
-    pass
-
-
-_path = os.path.dirname(os.path.realpath(__file__))
-Builder.load_file(os.path.join(_path, 'context_menu.kv'))
Binary file src/libs/garden/garden.contextmenu/doc/app-menu-01.png has changed
Binary file src/libs/garden/garden.contextmenu/doc/context-menu-01.png has changed
Binary file src/libs/garden/garden.contextmenu/doc/menu-divider-01.png has changed
Binary file src/libs/garden/garden.contextmenu/doc/menu-divider-02.png has changed
--- a/src/libs/garden/garden.contextmenu/examples/simple_app_menu.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-import kivy
-from kivy.app import App
-from kivy.lang import Builder
-from kivy.logger import Logger
-import logging
-
-kivy.require('1.9.0')
-# Logger.setLevel(logging.DEBUG)
-
-import kivy.garden.contextmenu
-
-kv = """
-FloatLayout:
-    id: layout
-    AppMenu:
-        id: app_menu
-        top: root.height
-        cancel_handler_widget: layout
-
-        AppMenuTextItem:
-            text: "Menu #1"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "Item #11"
-                ContextMenuTextItem:
-                    text: "Item #12"
-        AppMenuTextItem:
-            text: "Menu Menu Menu #2"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "Item #21"
-                ContextMenuTextItem:
-                    text: "Item #22"
-                ContextMenuTextItem:
-                    text: "ItemItemItem #23"
-                ContextMenuTextItem:
-                    text: "Item #24"
-                    ContextMenu:
-                        ContextMenuTextItem:
-                            text: "Item #241"
-                        ContextMenuTextItem:
-                            text: "Hello, World!"
-                            on_release: app.say_hello(self.text)
-                        ContextMenuTextItem:
-                            text: "Item #243"
-                        ContextMenuTextItem:
-                            text: "Item #244"
-                ContextMenuTextItem:
-                    text: "Item #5"
-        AppMenuTextItem:
-            text: "Menu Menu #3"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "SubMenu #31"
-                ContextMenuTextItem:
-                    text: "SubMenu #32"
-                ContextMenuTextItem:
-                    text: "SubMenu #33"
-                ContextMenuDivider:
-                ContextMenuTextItem:
-                    text: "SubMenu #34"
-        AppMenuTextItem:
-            text: "Menu #4"
-
-    Label:
-        pos: 10, 10
-        text: "Left click anywhere outside the context menu to close it"
-        size_hint: None, None
-        size: self.texture_size
-"""
-
-
-class MyApp(App):
-
-    def build(self):
-        self.title = 'Simple app menu example'
-        return Builder.load_string(kv)
-
-    def say_hello(self, text):
-        print(text)
-        self.root.ids['app_menu'].close_all()
-
-if __name__ == '__main__':
-    MyApp().run()
\ No newline at end of file
--- a/src/libs/garden/garden.contextmenu/examples/simple_context_menu.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-import kivy
-from kivy.app import App
-from kivy.lang import Builder
-
-kivy.require('1.9.0')
-
-import kivy.garden.contextmenu
-
-
-kv = """
-FloatLayout:
-    id: layout
-    Label:
-        pos: 10, self.parent.height - self.height - 10
-        text: "Left click anywhere outside the context menu to close it"
-        size_hint: None, None
-        size: self.texture_size
-
-    Button:
-        size_hint: None, None
-        pos_hint: {"center_x": 0.5, "center_y": 0.8 }
-        size: 300, 40
-        text: "Click me to show the context menu"
-        on_release: context_menu.show(*app.root_window.mouse_pos)
-
-    ContextMenu:
-        id: context_menu
-        visible: False
-        cancel_handler_widget: layout
-
-        ContextMenuTextItem:
-            text: "SubMenu #2"
-        ContextMenuTextItem:
-            text: "SubMenu #3"
-            ContextMenu:
-                ContextMenuTextItem:
-                    text: "SubMenu #5"
-                ContextMenuTextItem:
-                    text: "SubMenu #6"
-                    ContextMenu:
-                        ContextMenuTextItem:
-                            text: "SubMenu #9"
-                        ContextMenuTextItem:
-                            text: "SubMenu #10"
-                        ContextMenuTextItem:
-                            text: "SubMenu #11"
-                        ContextMenuTextItem:
-                            text: "Hello, World!"
-                            on_release: app.say_hello(self.text)
-                        ContextMenuTextItem:
-                            text: "SubMenu #12"
-                ContextMenuTextItem:
-                    text: "SubMenu #7"
-        ContextMenuTextItem:
-            text: "SubMenu #4"
-"""
-
-class MyApp(App):
-
-    def build(self):
-        self.title = 'Simple context menu example'
-        return Builder.load_string(kv)
-
-    def say_hello(self, text):
-        print(text)
-        self.root.ids['context_menu'].hide()
-
-
-if __name__ == '__main__':
-    MyApp().run()
\ No newline at end of file
--- a/src/main.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#!/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/>.
-
-import cagou
-
-if __name__ == "__main__":
-    cagou.run()
--- a/src/platform/android/sat.conf	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-[DEFAULT]
-bridge = pb
-log_level = debug
-log_fmt = #[%%(levelname)s][%%(name)s] %%(message)s
-
-[cagou]
-log_level = debug
-log_fmt = [%%(levelname)s][%%(name)s] %%(message)s
--- a/src/service/main.py	Thu Apr 05 15:59:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-#!/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/>.
-
-import sys
-import os
-# we want the service to access the modules from parent dir (sat, etc.)
-os.chdir('..')
-sys.path.insert(0, '')
-from sat.core.constants import Const as C
-from sat.core import log_config
-# SàT log conf must be done before calling Kivy
-log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
-# if this module is called, we should be on android,
-# but just in case...
-from kivy import utils as kivy_utils
-if kivy_utils.platform == "android":
-    # sys.platform is "linux" on android by default
-    # so we change it to allow backend to detect android
-    sys.platform = "android"
-    C.PLUGIN_EXT = "pyo"
-from sat.core import sat_main
-from twisted.internet import reactor
-
-sat = sat_main.SAT()
-reactor.run()