changeset 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents 1e3b1f9ad6e2 (current diff) 584d45bb36d9 (diff)
children dd1a148bd3d8
files frontends/src/constants.py frontends/src/primitivus/card_game.py frontends/src/quick_frontend/quick_card_game.py frontends/src/quick_frontend/quick_chat_list.py frontends/src/wix/__init__.py frontends/src/wix/card_game.py frontends/src/wix/chat.py frontends/src/wix/constants.py frontends/src/wix/contact_list.py frontends/src/wix/main_window.py frontends/src/wix/profile.py frontends/src/wix/profile_manager.py frontends/src/wix/quiz_game.py frontends/src/wix/wix frontends/src/wix/xmlui.py setup.py src/core/xmpp.py src/plugins/plugin_xep_0060.py src/test/helpers.py
diffstat 75 files changed, 5095 insertions(+), 4998 deletions(-) [+]
line wrap: on
line diff
--- a/INSTALL	Thu Feb 05 11:59:26 2015 +0100
+++ b/INSTALL	Wed Mar 18 10:52:28 2015 +0100
@@ -50,9 +50,6 @@
 	- to launch Primitivus, enter:
 $ primitivus
 	then create a profile.
-	- to launch Wix, enter
-$ wix
-	then create a profile.
 	- to use jp, follow its help:
 $ jp --help
 
--- a/MANIFEST.in	Thu Feb 05 11:59:26 2015 +0100
+++ b/MANIFEST.in	Wed Mar 18 10:52:28 2015 +0100
@@ -5,6 +5,6 @@
 global-include CHANGELOG COPYING* INSTALL README*
 global-include *.sh
 include src/sat.*
-include frontends/src/jp/jp frontends/src/primitivus/primitivus frontends/src/wix/wix
+include frontends/src/jp/jp frontends/src/primitivus/primitivus
 include src/bridge/bridge_constructor/mediawiki_template.tpl
 prune src/bridge/bridge_constructor/generated
--- a/README	Thu Feb 05 11:59:26 2015 +0100
+++ b/README	Wed Mar 18 10:52:28 2015 +0100
@@ -28,7 +28,6 @@
 
 For the moment, the frontends are:
 
-* wix: based on WxWidgets, a minimal and old fashioned desktop frontend
 * primitivus: a console interface client, for screen lovers
 * jp: the powerful command line toolkit.
 * libervia: the web frontend. It's in a different package, check libervia on the wiki: http://wiki.goffi.org/wiki/Libervia
@@ -81,7 +80,6 @@
 SàT is the acronym (yes another one :( ) in tribute to the song Salut à Toi from the Bérurier Noir band. I recommend also the excellent cover from Les Ogres de Barback.
 jp stands for "Jabber coPy" or "JumP".
 Primitivus is based on Urwid, and, according to their FAQ, "ur" is a German prefix for "ancestral or primal" (primitivus is a latin word).
-wix come of course from the WX of wxwidgets and just sounds cool :).
 
 
 ** /!\ PRIVACY WARNING /!\ **
@@ -107,9 +105,6 @@
 wokkel:
 SàT use a library with high level enhancements on top of twisted, which is called wokkel (http://wokkel.ik.nu). Lot of thanks to Ralph Meijer and all other contributors.
 
-wxWidgets & wxPython:
-The Wix frontend is made with wxWidgets (www.wxwidgets.org) and its python version wxPython (wxPython), which is a really handy UI toolkit who works on most popular platforms. It can be found on probably all major Gnu/Linux distributions.
-
 Urwid:
 Primitivus is based on Urwid (http://excess.org/urwid/) which saved me a lot of time. It's really a great library to easily make a sophisticated interface.
 
@@ -162,7 +157,7 @@
 
 - Matteo Cypriani <mcy@lm7.fr>: jp's mainloop update + doc improvements + various fixes. He's also the other co-maintainer of the Debian package.
 
-- Olly Betts <olly@survex.com>: icon fix in Wix
+- Olly Betts <olly@survex.com>: icon fix in Wix [N.B: Wix has been removed]
 
 Many thanks to them.
 
--- a/README4PACKAGERS	Thu Feb 05 11:59:26 2015 +0100
+++ b/README4PACKAGERS	Wed Mar 18 10:52:28 2015 +0100
@@ -49,12 +49,6 @@
 
 Suggested: Python X Library (for user notifications in a graphical session)
 
----------------------------------
-Dependencies for the Wix frontend
----------------------------------
-
-wxPython
-
 --------------------------------------
 Dependencies for the Libervia frontend
 --------------------------------------
@@ -107,7 +101,7 @@
 Packages and versions
 #####################
 
-We have packaged Wix, Primitivus, and Jp with the core backend, as we think theses frontends should be shipped with each SàT distribution. But allowing them to be installed separately would be good: for example, Wix will be useless for a server or non-graphical workstation.
+We have packaged Primitivus and Jp with the core backend, as we think theses frontends should be shipped with each SàT distribution.
 
 Libervia is distributed as a separate package because of its specificities (it's a huge project by itself, it uses Pyjamas Python => JS Compiler, etc.). But it's part of the same project.
 
--- a/README4TRANSLATORS	Thu Feb 05 11:59:26 2015 +0100
+++ b/README4TRANSLATORS	Wed Mar 18 10:52:28 2015 +0100
@@ -5,7 +5,7 @@
 To translate a file, you can use a dedicated tool as the excellent gtranslator:
 
 - use the template .po file (e.g. sat.po) and name it to your translated language (e.g. fr.po for french); you can preferably generate a new template directly from the source with the following command (eventually adapted, the following command use zsh's globbing syntax), launched from root sat dir after having emptied the build directory:
-> xgettext -L python -d sat --keyword=D_ -p i18n  **/*(.py|.tac|primitivus|wix)(.) **/jp(.)
+> xgettext -L python -d sat --keyword=D_ -p i18n  **/*(.py|.tac|primitivus)(.) **/jp(.)
 
 - then to start a new translation, copy i18n/sat.po to your language file, e.g. cd i18n; cp sat.po fr.po
 
--- a/frontends/src/bridge/DBus.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/bridge/DBus.py	Wed Mar 18 10:52:28 2015 +0100
@@ -102,10 +102,10 @@
                 async = False
 
                 if kwargs:
-                    if 'callback' in kwargs and 'errback' in kwargs:
+                    if 'callback' in kwargs:
                         async = True
                         _callback = kwargs.pop('callback')
-                        _errback = kwargs.pop('errback')
+                        _errback = kwargs.pop('errback', lambda failure: log.error(unicode(failure)))
                 elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]):
                     async = True
                     args = list(args)
@@ -122,138 +122,511 @@
                 return method(*args, **kwargs)
 
             return getPluginMethod
-    def addContact(self, entity_jid, profile_key="@DEFAULT@"):
-        return self.db_core_iface.addContact(entity_jid, profile_key)
+
+    def addContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.addContact(entity_jid, profile_key, **kwargs)
 
     def asyncConnect(self, profile_key="@DEFAULT@", password='', callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.asyncConnect(profile_key, password, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.asyncCreateProfile(profile, password, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def asyncDeleteProfile(self, profile, callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.asyncDeleteProfile(profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def asyncGetParamA(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return unicode(self.db_core_iface.asyncGetParamA(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler))
 
-    def confirmationAnswer(self, id, accepted, data, profile):
-        return self.db_core_iface.confirmationAnswer(id, accepted, data, profile)
+    def confirmationAnswer(self, id, accepted, data, profile, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.confirmationAnswer(id, accepted, data, profile, **kwargs)
 
-    def delContact(self, entity_jid, profile_key="@DEFAULT@"):
-        return self.db_core_iface.delContact(entity_jid, profile_key)
+    def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.delContact(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def discoInfos(self, entity_jid, profile_key, callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.discoInfos(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def discoItems(self, entity_jid, profile_key, callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.discoItems(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def disconnect(self, profile_key="@DEFAULT@"):
-        return self.db_core_iface.disconnect(profile_key)
+    def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.disconnect(profile_key, **kwargs)
 
-    def getConfig(self, section, name):
-        return unicode(self.db_core_iface.getConfig(section, name))
+    def getConfig(self, section, name, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.getConfig(section, name, **kwargs))
+
+    def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.getContacts(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def getContacts(self, profile_key="@DEFAULT@"):
-        return self.db_core_iface.getContacts(profile_key)
+    def getContactsFromGroup(self, group, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getContactsFromGroup(group, profile_key, **kwargs)
 
-    def getContactsFromGroup(self, group, profile_key="@DEFAULT@"):
-        return self.db_core_iface.getContactsFromGroup(group, profile_key)
+    def getEntitiesData(self, jids, keys, profile, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getEntitiesData(jids, keys, profile, **kwargs)
 
-    def getEntityData(self, jid, keys, profile):
-        return self.db_core_iface.getEntityData(jid, keys, profile)
+    def getEntityData(self, jid, keys, profile, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getEntityData(jid, keys, profile, **kwargs)
 
     def getHistory(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.getHistory(from_jid, to_jid, limit, between, search, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def getLastResource(self, contact_jid, profile_key="@DEFAULT@"):
-        return unicode(self.db_core_iface.getLastResource(contact_jid, profile_key))
+    def getMainResource(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.getMainResource(contact_jid, profile_key, **kwargs))
 
-    def getMenuHelp(self, menu_id, language):
-        return unicode(self.db_core_iface.getMenuHelp(menu_id, language))
+    def getMenuHelp(self, menu_id, language, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.getMenuHelp(menu_id, language, **kwargs))
 
-    def getMenus(self, language, security_limit):
-        return self.db_core_iface.getMenus(language, security_limit)
+    def getMenus(self, language, security_limit, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getMenus(language, security_limit, **kwargs)
 
-    def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@"):
-        return unicode(self.db_core_iface.getParamA(name, category, attribute, profile_key))
+    def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.getParamA(name, category, attribute, profile_key, **kwargs))
 
     def getParams(self, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return unicode(self.db_core_iface.getParams(security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler))
 
-    def getParamsCategories(self, ):
-        return self.db_core_iface.getParamsCategories()
+    def getParamsCategories(self, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getParamsCategories(**kwargs)
 
     def getParamsForCategory(self, category, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return unicode(self.db_core_iface.getParamsForCategory(category, security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler))
 
     def getParamsUI(self, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return unicode(self.db_core_iface.getParamsUI(security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler))
 
-    def getPresenceStatuses(self, profile_key="@DEFAULT@"):
-        return self.db_core_iface.getPresenceStatuses(profile_key)
+    def getPresenceStatuses(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getPresenceStatuses(profile_key, **kwargs)
 
-    def getProfileName(self, profile_key="@DEFAULT@"):
-        return unicode(self.db_core_iface.getProfileName(profile_key))
+    def getProfileName(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.getProfileName(profile_key, **kwargs))
 
-    def getProfilesList(self, ):
-        return self.db_core_iface.getProfilesList()
+    def getProfilesList(self, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getProfilesList(**kwargs)
 
-    def getProgress(self, id, profile):
-        return self.db_core_iface.getProgress(id, profile)
+    def getProgress(self, id, profile, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getProgress(id, profile, **kwargs)
 
     def getReady(self, callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.getReady(timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def getVersion(self, ):
-        return unicode(self.db_core_iface.getVersion())
+    def getVersion(self, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.getVersion(**kwargs))
 
-    def getWaitingConf(self, profile_key):
-        return self.db_core_iface.getWaitingConf(profile_key)
+    def getWaitingConf(self, profile_key, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getWaitingConf(profile_key, **kwargs)
 
-    def getWaitingSub(self, profile_key="@DEFAULT@"):
-        return self.db_core_iface.getWaitingSub(profile_key)
+    def getWaitingSub(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.getWaitingSub(profile_key, **kwargs)
 
-    def isConnected(self, profile_key="@DEFAULT@"):
-        return self.db_core_iface.isConnected(profile_key)
+    def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.isConnected(profile_key, **kwargs)
 
     def launchAction(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.launchAction(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def loadParamsTemplate(self, filename):
-        return self.db_core_iface.loadParamsTemplate(filename)
+    def loadParamsTemplate(self, filename, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.loadParamsTemplate(filename, **kwargs)
 
-    def paramsRegisterApp(self, xml, security_limit=-1, app=''):
-        return self.db_core_iface.paramsRegisterApp(xml, security_limit, app)
+    def paramsRegisterApp(self, xml, security_limit=-1, app='', callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.paramsRegisterApp(xml, security_limit, app, **kwargs)
 
-    def saveParamsTemplate(self, filename):
-        return self.db_core_iface.saveParamsTemplate(filename)
+    def saveParamsTemplate(self, filename, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.saveParamsTemplate(filename, **kwargs)
 
     def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
-        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"):
-        return self.db_core_iface.setParam(name, value, category, security_limit, profile_key)
-
-    def setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"):
-        return self.db_core_iface.setPresence(to_jid, show, statuses, profile_key)
+    def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.setParam(name, value, category, security_limit, profile_key, **kwargs)
 
-    def subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
-        return self.db_core_iface.subscription(sub_type, entity, profile_key)
+    def setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.setPresence(to_jid, show, statuses, profile_key, **kwargs)
 
-    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
-        return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key)
+    def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.subscription(sub_type, entity, profile_key, **kwargs)
+
+    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
--- a/frontends/src/constants.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# generic module for SàT frontends
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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 constants
-from sat.core.i18n import _, D_
-
-try:
-    from collections import OrderedDict  # only available from python 2.7
-except ImportError:
-    try:
-        from ordereddict import OrderedDict
-    except ImportError:
-        pass  # libervia can not import external libraries
-
-
-def getPresence():
-    """We cannot do it directly in the Const class, if it is not encapsulated
-    in a method we get a JS runtime SyntaxError: "missing ) in parenthetical".
-    # TODO: merge this definition with those in primitivus.constants and wix.constants
-    """
-    try:
-        presence = OrderedDict([("", _("Online")),
-                                ("chat", _("Free for chat")),
-                                ("away", _("Away from keyboard")),
-                                ("dnd", _("Do not disturb")),
-                                ("xa", _("Extended away"))])
-    except TypeError:
-        presence = {"": _("Online"),
-                    "chat": _("Free for chat"),
-                    "away": _("Away from keyboard"),
-                    "dnd": _("Do not disturb"),
-                    "xa": _("Extended away")
-                    }
-    return presence
-
-
-class Const(constants.Const):
-
-    PRESENCE = getPresence()
-
-    # from plugin_misc_text_syntaxes
-    SYNTAX_XHTML = "XHTML"
-    SYNTAX_CURRENT = "@CURRENT@"
-    SYNTAX_TEXT = "text"
-
-    # XMLUI
-    SAT_FORM_PREFIX = "SAT_FORM_"
-    SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_"  # used to have unique elements names
-    XMLUI_STATUS_VALIDATED = "validated"
-    XMLUI_STATUS_CANCELLED = constants.Const.XMLUI_DATA_CANCELLED
-
-    # MUC
-    ALL_OCCUPANTS = 1
-    MUC_USER_STATES = {
-        "active": u'✔',
-        "inactive": u'☄',
-        "gone": u'✈',
-        "composing": u'✎',
-        "paused": u"⦷"
-    }
-
-    # Roster
-    GROUP_NOT_IN_ROSTER = D_('Not in roster')
--- a/frontends/src/jp/base.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/jp/base.py	Wed Mar 18 10:52:28 2015 +0100
@@ -245,11 +245,11 @@
         callback()
 
     def get_full_jid(self, param_jid):
-        """Return the full jid if possible (add last resource when find a bare jid)"""
+        """Return the full jid if possible (add main resource when find a bare jid)"""
         _jid = JID(param_jid)
         if not _jid.resource:
             #if the resource is not given, we try to add the last known resource
-            last_resource = self.bridge.getLastResource(param_jid, self.profile)
+            last_resource = self.bridge.getMainResource(param_jid, self.profile)
             if last_resource:
                 return "%s/%s" % (_jid.bare, last_resource)
         return param_jid
--- a/frontends/src/primitivus/card_game.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,354 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat.core.i18n import _
-import urwid
-from urwid_satext import sat_widgets
-from sat_frontends.tools.games import TarotCard
-from sat_frontends.quick_frontend.quick_card_game import QuickCardGame
-from sat_frontends.primitivus import xmlui
-from sat_frontends.primitivus.keys import action_key_map as a_key
-
-
-class CardDisplayer(urwid.Text):
-    """Show a card"""
-    signals = ['click']
-
-    def __init__(self, card):
-        self.__selected = False
-        self.card = card
-        urwid.Text.__init__(self, card.getAttrText())
-
-    def selectable(self):
-        return True
-
-    def keypress(self, size, key):
-        if key == a_key['CARD_SELECT']:
-            self.select(not self.__selected)
-            self._emit('click')
-        return key
-
-    def mouse_event(self, size, event, button, x, y, focus):
-        if urwid.is_mouse_event(event) and button == 1:
-            self.select(not self.__selected)
-            self._emit('click')
-            return True
-
-        return False
-
-    def select(self, state=True):
-        self.__selected = state
-        attr, txt = self.card.getAttrText()
-        if self.__selected:
-            attr += '_selected'
-        self.set_text((attr, txt))
-        self._invalidate()
-
-    def isSelected(self):
-        return self.__selected
-
-    def getCard(self):
-        return self.card
-
-    def render(self, size, focus=False):
-        canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus))
-        if focus:
-            canvas.set_cursor((0, 0))
-        return canvas
-
-
-class Hand(urwid.WidgetWrap):
-    """Used to display several cards, and manage a hand"""
-    signals = ['click']
-
-    def __init__(self, hand=[], selectable=False, on_click=None, user_data=None):
-        """@param hand: list of Card"""
-        self.__selectable = selectable
-        self.columns = urwid.Columns([], dividechars=1)
-        if on_click:
-            urwid.connect_signal(self, 'click', on_click, user_data)
-        if hand:
-            self.update(hand)
-        urwid.WidgetWrap.__init__(self, self.columns)
-
-    def selectable(self):
-        return self.__selectable
-
-    def keypress(self, size, key):
-
-        if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]:
-            return self.columns.keypress(size, key)
-        else:
-            #No card displayed, we still have to manage the clicks
-            if key == a_key['CARD_SELECT']:
-                self._emit('click', None)
-            return key
-
-    def getSelected(self):
-        """Return a list of selected cards"""
-        _selected = []
-        for wid in self.columns.widget_list:
-            if isinstance(wid, CardDisplayer) and wid.isSelected():
-                _selected.append(wid.getCard())
-        return _selected
-
-    def update(self, hand):
-        """Update the hand displayed in this widget
-        @param hand: list of Card"""
-        try:
-            del self.columns.widget_list[:]
-            del self.columns.column_types[:]
-        except IndexError:
-            pass
-        self.columns.contents.append((urwid.Text(''), ('weight', 1, False)))
-        for card in hand:
-            widget = CardDisplayer(card)
-            self.columns.widget_list.append(widget)
-            self.columns.column_types.append(('fixed', 3))
-            urwid.connect_signal(widget, 'click', self.__onClick)
-        self.columns.contents.append((urwid.Text(''), ('weight', 1, False)))
-        self.columns.focus_position = 1
-
-    def __onClick(self, card_wid):
-        self._emit('click', card_wid)
-
-
-class Card(TarotCard):
-    """This class is used to represent a card, logically
-    and give a text representation with attributes"""
-    SIZE = 3  # size of a displayed card
-
-    def __init__(self, suit, value):
-        """@param file: path of the PNG file"""
-        TarotCard.__init__(self, (suit, value))
-
-    def getAttrText(self):
-        """return text representation of the card with attributes"""
-        try:
-            value = "%02i" % int(self.value)
-        except ValueError:
-            value = self.value[0].upper() + self.value[1]
-        if self.suit == "atout":
-            if self.value == "excuse":
-                suit = 'c'
-            else:
-                suit = 'A'
-            color = 'neutral'
-        elif self.suit == "pique":
-            suit = u'♠'
-            color = 'black'
-        elif self.suit == "trefle":
-            suit = u'♣'
-            color = 'black'
-        elif self.suit == "coeur":
-            suit = u'♥'
-            color = 'red'
-        elif self.suit == "carreau":
-            suit = u'♦'
-            color = 'red'
-        if self.bout:
-            color = 'special'
-        return ('card_%s' % color, u"%s%s" % (value, suit))
-
-    def getWidget(self):
-        """Return a widget representing the card"""
-        return CardDisplayer(self)
-
-
-class Table(urwid.FlowWidget):
-    """Represent the cards currently on the table"""
-
-    def __init__(self):
-        self.top = self.left = self.bottom = self.right = None
-
-    def putCard(self, location, card):
-        """Put a card on the table
-        @param location: where to put the card (top, left, bottom or right)
-        @param card: Card to play or None"""
-        assert location in ['top', 'left', 'bottom', 'right']
-        assert isinstance(card, Card) or card == None
-        if [getattr(self, place) for place in ['top', 'left', 'bottom', 'right']].count(None) == 0:
-            #If the table is full of card, we remove them
-            self.top = self.left = self.bottom = self.right = None
-        setattr(self, location, card)
-        self._invalidate()
-
-    def rows(self, size, focus=False):
-        return self.display_widget(size, focus).rows(size, focus)
-
-    def render(self, size, focus=False):
-        return self.display_widget(size, focus).render(size, focus)
-
-    def display_widget(self, size, focus):
-        cards = {}
-        max_col, = size
-        separator = " - "
-        margin = max((max_col - Card.SIZE) / 2, 0) * ' '
-        margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * ' '
-        for location in ['top', 'left', 'bottom', 'right']:
-            card = getattr(self, location)
-            cards[location] = card.getAttrText() if card else Card.SIZE * ' '
-        render_wid = [urwid.Text([margin, cards['top']]),
-                      urwid.Text([margin_center, cards['left'], separator, cards['right']]),
-                      urwid.Text([margin, cards['bottom']])]
-        return urwid.Pile(render_wid)
-
-
-class CardGame(QuickCardGame, urwid.WidgetWrap):
-    """Widget for card games"""
-
-    def __init__(self, parent, referee, players, player_nick):
-        QuickCardGame.__init__(self, parent, referee, players, player_nick)
-        self.loadCards()
-        self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), 'center')])
-        #self.parent.host.debug()
-        self.table = Table()
-        self.center = urwid.Columns([('fixed', len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))),
-                                urwid.Filler(self.table),
-                                ('fixed', len(self.right_nick), urwid.Filler(urwid.Text(self.right_nick)))
-                               ])
-        """urwid.Pile([urwid.Padding(self.top_card_wid,'center'),
-                             urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)),
-                                            urwid.Padding(self.center_cards_wid,'center'),
-                                            ('fixed',len(self.right_nick),urwid.Text(self.right_nick))
-                                           ]),
-                             urwid.Padding(self.bottom_card_wid,'center')
-                             ])"""
-        self.hand_wid = Hand(selectable=True, on_click=self.onClick)
-        self.main_frame = urwid.Frame(self.center, header=self.top, footer=self.hand_wid, focus_part='footer')
-        urwid.WidgetWrap.__init__(self, self.main_frame)
-        self.parent.host.bridge.tarotGameReady(player_nick, referee, self.parent.host.profile)
-
-    def loadCards(self):
-        """Load all the cards in memory"""
-        QuickCardGame.loadCards(self)
-        for value in map(str, range(1, 22)) + ['excuse']:
-            card = Card('atout', value)
-            self.cards[card.suit, card.value] = card
-            self.deck.append(card)
-        for suit in ["pique", "coeur", "carreau", "trefle"]:
-            for value in map(str, range(1, 11)) + ["valet", "cavalier", "dame", "roi"]:
-                card = Card(suit, value)
-                self.cards[card.suit, card.value] = card
-                self.deck.append(card)
-
-    def newGame(self, hand):
-        """Start a new game, with given hand"""
-        if hand is []:  # reset the display after the scores have been showed
-            self.resetRound()
-            for location in ['top', 'left', 'bottom', 'right']:
-                self.table.putCard(location, None)
-            self.parent.host.redraw()
-            self.parent.host.bridge.tarotGameReady(self.player_nick, self.referee, self.parent.host.profile)
-            return
-        QuickCardGame.newGame(self, hand)
-        self.hand_wid.update(self.hand)
-        self.parent.host.redraw()
-
-    def contratSelected(self, data):
-        """Called when the contrat has been choosed
-        @param data: form result"""
-        contrat = data[0][1]
-        QuickCardGame.contratSelected(self, contrat)
-
-    def chooseContrat(self, xml_data):
-        """Called when the player has to select his contrat
-        @param xml_data: SàT xml representation of the form"""
-        form = xmlui.create(self.parent.host, xml_data, title=_('Please choose your contrat'), flags=['NO_CANCEL'])
-        form.show(valign='top')
-
-    def showCards(self, game_stage, cards, data):
-        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
-        QuickCardGame.showCards(self, game_stage, cards, data)
-        self.center.widget_list[1] = urwid.Filler(Hand(self.to_show))
-        self.parent.host.redraw()
-
-    def myTurn(self):
-        QuickCardGame.myTurn(self)
-
-    def showScores(self, xml_data, winners, loosers):
-        """Called when the round is over, display the scores
-        @param xml_data: SàT xml representation of the form"""
-        if not winners and not loosers:
-            title = _("Draw game")
-        else:
-            title = _('You win \o/') if self.player_nick in winners else _('You loose :(')
-        form = xmlui.create(self.parent.host, xml_data, title=title, flags=['NO_CANCEL'])
-        form.show()
-
-    def invalidCards(self, phase, played_cards, invalid_cards):
-        """Invalid cards have been played
-        @param phase: phase of the game
-        @param played_cards: all the cards played
-        @param invalid_cards: cards which are invalid"""
-        QuickCardGame.invalidCards(self, phase, played_cards, invalid_cards)
-        self.hand_wid.update(self.hand)
-        if self._autoplay == None:  # No dialog if there is autoplay
-            self.parent.host.notify(_('Cards played are invalid !'))
-        self.parent.host.redraw()
-
-    def cardsPlayed(self, player, cards):
-        """A card has been played by player"""
-        QuickCardGame.cardsPlayed(self, player, cards)
-        self.table.putCard(self.getPlayerLocation(player), self.played[player])
-        self._checkState()
-        self.parent.host.redraw()
-
-    def _checkState(self):
-        if isinstance(self.center.widget_list[1].original_widget, Hand):  # if we have a hand displayed
-            self.center.widget_list[1] = urwid.Filler(self.table)  # we show again the table
-            if self.state == "chien":
-                self.to_show = []
-                self.state = "wait"
-            elif self.state == "wait_for_ecart":
-                self.state = "ecart"
-                self.hand.extend(self.to_show)
-                self.hand.sort()
-                self.to_show = []
-                self.hand_wid.update(self.hand)
-
-    ##EVENTS##
-    def onClick(self, hand, card_wid):
-        """Called when user do an action on the hand"""
-        if not self.state in ['play', 'ecart', 'wait_for_ecart']:
-            #it's not our turn, we ignore the click
-            card_wid.select(False)
-            return
-        self._checkState()
-        if self.state == "ecart":
-            if len(self.hand_wid.getSelected()) == 6:
-                pop_up_widget = sat_widgets.ConfirmDialog(_("Do you put these cards in chien ?"), yes_cb=self.onEcartDone, no_cb=self.parent.host.removePopUp)
-                self.parent.host.showPopUp(pop_up_widget)
-        elif self.state == "play":
-            card = card_wid.getCard()
-            self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], self.parent.host.profile)
-            self.hand.remove(card)
-            self.hand_wid.update(self.hand)
-            self.state = "wait"
-
-    def onEcartDone(self, button):
-        """Called when player has finished his écart"""
-        ecart = []
-        for card in self.hand_wid.getSelected():
-            ecart.append((card.suit, card.value))
-            self.hand.remove(card)
-        self.hand_wid.update(self.hand)
-        self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, self.parent.host.profile)
-        self.state = "wait"
-        self.parent.host.removePopUp()
--- a/frontends/src/primitivus/chat.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/chat.py	Wed Mar 18 10:52:28 2015 +0100
@@ -23,13 +23,15 @@
 import urwid
 from urwid_satext import sat_widgets
 from urwid_satext.files_management import FileDialog
+from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.quick_frontend.quick_chat import QuickChat
-from sat_frontends.primitivus.card_game import CardGame
-from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
+from sat_frontends.quick_frontend import quick_games
+from sat_frontends.primitivus import game_tarot
 from sat_frontends.primitivus.constants import Const as C
 from sat_frontends.primitivus.keys import action_key_map as a_key
+from sat_frontends.primitivus.widget import PrimitivusWidget
 import time
-from sat_frontends.tools.jid  import JID
+from sat_frontends.tools import jid
 
 
 class ChatText(urwid.FlowWidget):
@@ -80,19 +82,27 @@
         return txt_widget
 
 
-class Chat(urwid.WidgetWrap, QuickChat):
+class Chat(PrimitivusWidget, QuickChat):
 
-    def __init__(self, target, host, type_='one2one'):
-        self.target = target
-        QuickChat.__init__(self, target, host, type_)
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+        QuickChat.__init__(self, host, target, type_, profiles=profiles)
         self.content = urwid.SimpleListWalker([])
         self.text_list = urwid.ListBox(self.content)
         self.chat_widget = urwid.Frame(self.text_list)
         self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
         self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
         self.pile = urwid.Pile([self.chat_colums])
-        urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile))
-        self.setType(type_)
+        PrimitivusWidget.__init__(self, self.pile, self.target)
+
+        # we must adapt the behaviour with the type
+        if type_ == C.CHAT_ONE2ONE:
+            self.historyPrint(profile=self.profile)
+        elif type_ == C.CHAT_GROUP:
+            if len(self.chat_colums.contents) == 1:
+                present_widget = self._buildPresentList()
+                self.present_panel = sat_widgets.VerticalSeparator(present_widget)
+                self._appendPresentPanel()
+
         self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00  %Y")) #struct_time of day changing time
         self.show_timestamp = True
         self.show_short_nick = False
@@ -101,12 +111,12 @@
 
     def keypress(self, size, key):
         if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel
-            if self.type == 'group':
+            if self.type == C.CHAT_GROUP:
                 widgets = [widget for (widget, options) in self.chat_colums.contents]
                 if self.present_panel in widgets:
-                    self.__removePresentPanel()
+                    self._removePresentPanel()
                 else:
-                    self.__appendPresentPanel()
+                    self._appendPresentPanel()
         elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp
             self.show_timestamp = not self.show_timestamp
             for wid in self.content:
@@ -115,10 +125,6 @@
             self.show_short_nick = not self.show_short_nick
             for wid in self.content:
                 wid._invalidate()
-        elif key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
-            show = not isinstance(self._w, sat_widgets.LabelLine)
-            self.showDecoration(show)
-            self._invalidate()
         elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance
             if self.subject:
                 self.show_title = (self.show_title + 1) % 3
@@ -130,67 +136,30 @@
                     self.chat_widget.header = None
                 self._invalidate()
 
-
         return super(Chat, self).keypress(size, key)
 
     def getMenu(self):
         """Return Menu bar"""
         menu = sat_widgets.Menu(self.host.loop)
-        if self.type == 'group':
+        if self.type == C.CHAT_GROUP:
             self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare})
             game = _("Game")
             menu.addMenu(game, "Tarot", self.onTarotRequest)
-        elif self.type == 'one2one':
-            self.host.addMenus(menu, C.MENU_SINGLE, {'jid': unescapePrivate(self.target)})
+        elif self.type == C.CHAT_ONE2ONE:
+            self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target})
             menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest)
         return menu
 
-    def setType(self, type_):
-        QuickChat.setType(self, type_)
-        if type_ == 'one2one':
-            self.historyPrint(profile=self.host.profile)
-        elif type_ == 'group':
-            if len(self.chat_colums.contents) == 1:
-                present_widget = self.__buildPresentList()
-                self.present_panel = sat_widgets.VerticalSeparator(present_widget)
-                self.__appendPresentPanel()
-
-    def __getDecoration(self, widget):
-        return sat_widgets.LabelLine(widget, self.__getSurrendedText())
-
-    def __getSurrendedText(self):
-        """Get the text to be displayed as the dialog title."""
-        if not hasattr(self, "surrended_text"):
-            self.__setSurrendedText()
-        return self.surrended_text
-
-    def __setSurrendedText(self, state=None):
-        """Set the text to be displayed as the dialog title
-        @param stat: chat state of the contact
-        """
-        text = unicode(unescapePrivate(self.target))
-        if state:
-            text += " (" + state + ")"
-        self.surrended_text = sat_widgets.SurroundedText(text)
-
-    def showDecoration(self, show=True):
-        """Show/Hide the decoration around the chat window"""
-        if show:
-            main_widget = self.__getDecoration(self.pile)
-        else:
-            main_widget = self.pile
-        self._w = main_widget
-
-    def updateChatState(self, state, nick=None):
-        """Set the chat state (XEP-0085) of the contact. Leave nick to None
-        to set the state for a one2one conversation, or give a nickname or
-        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
-        @param state: the new chat state
-        @param nick: None for one2one, the MUC user nick or C.ALL_OCCUPANTS
-        """
-        if nick:
-            assert(self.type == 'group')
-            occupants = self.occupants if nick == C.ALL_OCCUPANTS else [nick]
+    def updateChatState(self, from_jid, state):
+        if self.type == C.CHAT_GROUP:
+            if from_jid == C.ENTITY_ALL:
+                occupants = self.occupants
+            else:
+                nick = from_jid.resource
+                if not nick:
+                    log.debug("no nick found for chatstate")
+                    return
+                occupants = [nick]
             options = self.present_wid.getAllValues()
             for index in xrange(0, len(options)):
                 nick = options[index].value
@@ -199,46 +168,51 @@
             self.present_wid.changeValues(options)
             self.host.redraw()
         else:
-            assert(self.type == 'one2one')
-            self.__setSurrendedText(state)
-            self.showDecoration()
-            self.host.redraw()
+            self.title_dynamic = '({})'.format(state)
 
     def _presentClicked(self, list_wid, clicked_wid):
-        assert(self.type == 'group')
+        assert self.type == C.CHAT_GROUP
         nick = clicked_wid.getValue().value
         if nick == self.getUserNick():
-            #We ignore click on our own nick
+            #We ignore clicks on our own nick
             return
-        #we have a click on a nick, we add the private conversation to the contact_list
-        full_jid = JID("%s/%s" % (self.target.bare, nick))
-        new_jid = escapePrivate(full_jid)
-        if new_jid not in self.host.contact_list:
-            self.host.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
+        contact_list = self.host.contact_lists[self.profile]
+        full_jid = jid.JID("%s/%s" % (self.target.bare, nick))
+
+        #we have a click on a nick, we need to create the widget if it doesn't exists
+        self.getOrCreatePrivateWidget(full_jid)
 
         #now we select the new window
-        self.host.contact_list.setFocus(full_jid, True)
+        contact_list.setFocus(full_jid, True)
 
-    def __buildPresentList(self):
+    def _buildPresentList(self):
         self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked)
         return self.present_wid
 
-    def __appendPresentPanel(self):
+    def _appendPresentPanel(self):
         self.chat_colums.contents.append((self.present_panel,('weight', 2, False)))
 
-    def __removePresentPanel(self):
+    def _removePresentPanel(self):
         for widget, options in self.chat_colums.contents:
             if widget is self.present_panel:
                 self.chat_colums.contents.remove((widget, options))
                 break
 
-    def __appendGamePanel(self, widget):
+    def addGamePanel(self, widget):
+        """Insert a game panel to this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
         assert (len(self.pile.contents) == 1)
-        self.pile.contents.insert(0,(widget,('weight', 1)))
-        self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1))))
+        self.pile.contents.insert(0, (widget, ('weight', 1)))
+        self.pile.contents.insert(1, (urwid.Filler(urwid.Divider('-'), ('fixed', 1))))
         self.host.redraw()
 
-    def __removeGamePanel(self):
+    def removeGamePanel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
         assert (len(self.pile.contents) == 3)
         del self.pile.contents[0]
         self.host.redraw()
@@ -252,12 +226,11 @@
         self.chat_widget.header = urwid.AttrMap(self.subj_wid,'title')
         self.host.redraw()
 
-    def setPresents(self, param_nicks):
-        """Set the users presents in the contact list for a group chat
-        @param nicks: list of nicknames
+    def setPresents(self, nicks):
+        """Set the occupants of a group chat.
+
+        @param nicks (list[unicode]): sorted list of nicknames
         """
-        nicks = [unicode(nick) for nick in param_nicks] #FIXME: should be done in DBus bridge
-        nicks.sort()
         QuickChat.setPresents(self, nicks)
         self.present_wid.changeValues(nicks)
         self.host.redraw()
@@ -290,13 +263,22 @@
             self.text_list.focus_position = len(self.content) - 1  # scroll down
         self.host.redraw()
 
-    def printMessage(self, from_jid, msg, profile, timestamp=None):
-        assert isinstance(from_jid, JID)
+    def onPrivateCreated(self, widget):
+        self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
+
+    def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
+        assert isinstance(from_jid, jid.JID)
+        if extra is None:
+            extra = {}
         try:
-            jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp)
+            timestamp = float(extra['timestamp'])
+        except KeyError:
+            timestamp=None
+        try:
+            nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
         except TypeError:
+            # None is returned, the message is managed
             return
-
         new_text = ChatText(self, timestamp, nick, mymess, msg)
 
         if timestamp and self.content:
@@ -323,7 +305,7 @@
             # all messages and not only with the messages coming from the history.
             self._notify(from_jid, msg)
 
-    def printInfo(self, msg, type_='normal', timestamp=None):
+    def printInfo(self, msg, type_='normal', extra=None):
         """Print general info
         @param msg: message to print
         @type_: one of:
@@ -331,6 +313,12 @@
             me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
         @param timestamp (float): number of seconds since epoch
         """
+        if extra is None:
+            extra = {}
+        try:
+            timestamp = float(extra['timestamp'])
+        except KeyError:
+            timestamp=None
         _widget = ChatText(self, timestamp, None, False, msg, is_info=True)
         self.content.append(_widget)
         self._notify(msg=msg)
@@ -348,30 +336,18 @@
             self.text_list.focus_position = len(self.content) - 1
         self.host.redraw()
         if not self.host.x_notify.hasFocus():
-            if self.type == "one2one":
+            if self.type == C.CHAT_ONE2ONE:
                 self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid)
             elif self.getUserNick().lower() in msg.lower():
                 self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': from_jid, 'room': self.target})
 
-    def startGame(self, game_type, referee, players):
-        """Configure the chat window to start a game"""
-        if game_type=="Tarot":
-            self.tarot_wid = CardGame(self, referee, players, self.nick)
-            self.__appendGamePanel(self.tarot_wid)
-
-    def getGame(self, game_type):
-        """Return class managing the game type"""
-        #TODO: check that the game is launched, and manage errors
-        if game_type=="Tarot":
-            return self.tarot_wid
-
     #MENU EVENTS#
     def onTarotRequest(self, menu):
         # TODO: move this to plugin_misc_tarot with dynamic menu
         if len(self.occupants) != 4:
             self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp))
         else:
-            self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile)
+            self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.profile)
 
     def onSendFileRequest(self, menu):
         # TODO: move this to core with dynamic menus
@@ -388,11 +364,15 @@
             self.host.showDialog(_(u"File has a unicode error in its name, it's not yet managed by SàT"), title=_("Can't send file"), type_="error")
             return
         #FIXME: check last_resource: what if self.target.resource exists ?
-        last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile)
+        last_resource = self.host.bridge.getMainResource(unicode(self.target.bare), self.profile)
         if last_resource:
-            full_jid = JID("%s/%s" % (self.target.bare, last_resource))
+            full_jid = jid.JID("%s/%s" % (self.target.bare, last_resource))
         else:
             full_jid = self.target
-        progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.host.profile)
+        progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile)
         self.host.addProgress(progress_id,filepath)
         self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent"))
+
+
+quick_widgets.register(QuickChat, Chat)
+quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)
--- a/frontends/src/primitivus/constants.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/constants.py	Wed Mar 18 10:52:28 2015 +0100
@@ -68,6 +68,8 @@
                ('show_dnd_focus', 'dark red, bold', 'default'),
                ('show_xa', 'dark red', 'default'),
                ('show_xa_focus', 'dark red, bold', 'default'),
+               ('resource', 'light blue', 'default'),
+               ('resource_main', 'dark blue', 'default'),
                ('status', 'yellow', 'default'),
                ('status_focus', 'yellow, bold', 'default'),
                ('param_selected','default, bold', 'dark red'),
@@ -85,3 +87,15 @@
 
     CONFIG_SECTION = APP_NAME.lower()
     CONFIG_OPT_KEY_PREFIX = "KEY_"
+
+    MENU_ID_MAIN = "MAIN_MENU"
+    MENU_ID_WIDGET = "WIDGET_MENU"
+
+    MODE_NORMAL = 'NORMAL'
+    MODE_INSERTION = 'INSERTION'
+    MODE_COMMAND = 'COMMAND'
+
+    GROUP_DATA_FOLDED = 'folded'
+
+    # contacts and contact list
+    ALERT_HEADER='(*) '
--- a/frontends/src/primitivus/contact_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/contact_list.py	Wed Mar 18 10:52:28 2015 +0100
@@ -21,36 +21,25 @@
 import urwid
 from urwid_satext import sat_widgets
 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
-from sat_frontends.quick_frontend.quick_utils import unescapePrivate
-from sat_frontends.tools.jid import JID
 from sat_frontends.primitivus.status import StatusBar
 from sat_frontends.primitivus.constants import Const as C
 from sat_frontends.primitivus.keys import action_key_map as a_key
+from sat_frontends.primitivus.widget import PrimitivusWidget
+from sat_frontends.tools import jid
 from sat.core import log as logging
 log = logging.getLogger(__name__)
 
 
-class ContactList(urwid.WidgetWrap, QuickContactList):
+class ContactList(PrimitivusWidget, QuickContactList):
     signals = ['click','change']
 
-    def __init__(self, host, on_click=None, on_change=None, user_data=None):
-        QuickContactList.__init__(self)
-        self.host = host
-        self.selected = None
-        self.groups={}
-        self.alert_jid=set()
-        self.show_status = False
-        self.show_disconnected = False
-        self.show_empty_groups = True
-        # TODO: this may lead to two successive UI refresh and needs an optimization
-        self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=host.profile, callback=self.showEmptyGroups)
-        self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=host.profile, callback=self.showOfflineContacts)
+    def __init__(self, host, on_click=None, on_change=None, user_data=None, profile=None):
+        QuickContactList.__init__(self, host, profile)
 
         #we now build the widget
-        self.host.status_bar = StatusBar(host)
-        self.frame = sat_widgets.FocusFrame(self.__buildList(), None, self.host.status_bar)
-        self.main_widget = sat_widgets.LabelLine(self.frame, sat_widgets.SurroundedText(_("Contacts")))
-        urwid.WidgetWrap.__init__(self, self.main_widget)
+        self.status_bar = StatusBar(host)
+        self.frame = sat_widgets.FocusFrame(self._buildList(), None, self.status_bar)
+        PrimitivusWidget.__init__(self, self.frame, _(u'Contacts'))
         if on_click:
             urwid.connect_signal(self, 'click', on_click, user_data)
         if on_change:
@@ -59,16 +48,14 @@
     def update(self):
         """Update display, keep focus"""
         widget, position = self.frame.body.get_focus()
-        self.frame.body = self.__buildList()
+        self.frame.body = self._buildList()
         if position:
             try:
                 self.frame.body.focus_position = position
             except IndexError:
                 pass
-        self.host.redraw()
-
-    def update_jid(self, jid):
-        self.update()
+        self._invalidate()
+        self.host.redraw() # FIXME: check if can be avoided
 
     def keypress(self, size, key):
         # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
@@ -81,18 +68,18 @@
             self.show_status = not self.show_status
             self.update()
         elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts
-            self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.host.profile)
+            self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.profile)
+        elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources
+            self.showResources(not self.show_resources)
+            self.update()
         return super(ContactList, self).keypress(size, key)
 
-    def __contains__(self, jid):
-        for group in self.groups:
-            if jid.bare in self.groups[group][1]:
-                return True
-        return False
+    # modify the contact list
 
     def setFocus(self, text, select=False):
         """give focus to the first element that matches the given text. You can also
         pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode).
+
         @param text: contact group name, contact or muc userhost, muc private dialog jid
         @param select: if True, the element is also clicked
         """
@@ -103,12 +90,8 @@
                     # contact group
                     value = widget.getValue()
                 elif isinstance(widget, sat_widgets.SelectableText):
-                    if  widget.data.startswith(C.PRIVATE_PREFIX):
-                        # muc private dialog
-                        value = widget.getValue()
-                    else:
-                        # contact or muc
-                        value = widget.data
+                    # contact or muc
+                    value = widget.data
                 else:
                     # Divider instance
                     continue
@@ -116,243 +99,214 @@
                 if text.strip() == value.strip():
                     self.frame.body.focus_position = idx
                     if select:
-                        self.__contactClicked(widget, True)
+                        self._contactClicked(False, widget, True)
                     return
             except AttributeError:
                 pass
             idx += 1
 
-    def putAlert(self, jid):
-        """Put an alert on the jid to get attention from user (e.g. for new message)"""
-        self.alert_jid.add(jid.bare)
+        log.debug(u"Not element found for {} in setFocus".format(text))
+
+    def specialResourceVisible(self, entity):
+        """Assure a resource of a special entity is visible and clickable
+
+        Mainly used to display private conversation in MUC rooms
+        @param entity: full jid of the resource to show
+        """
+        assert isinstance(entity, jid.JID)
+        if entity not in self._special_extras:
+            self._special_extras.add(entity)
+            self.update()
+
+    # events
+
+    def _groupClicked(self, group_wid):
+        group = group_wid.getValue()
+        data = self.getGroupData(group)
+        data[C.GROUP_DATA_FOLDED] =  not data.setdefault(C.GROUP_DATA_FOLDED, False)
+        self.setFocus(group)
+        self.update()
+
+    def _contactClicked(self, use_bare_jid, contact_wid, selected):
+        """Method called when a contact is clicked
+
+        @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget.
+            If True, all jids in self._alerts with the same bare jid has contact_wid.data will be removed
+        @param contact_wid: widget of the contact, must have the entity set in data attribute
+        @param selected: boolean returned by the widget, telling if it is selected
+        """
+        entity = contact_wid.data
+        if use_bare_jid:
+            to_remove = set()
+            for alert_entity in self._alerts:
+                if alert_entity.bare == entity.bare:
+                    to_remove.add(alert_entity)
+            self._alerts.difference_update(to_remove)
+        else:
+            self._alerts.discard(entity)
+        self.host.modeHint(C.MODE_INSERTION)
+        self.update()
+        self._emit('click', entity)
+
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
+        super(ContactList, self).onPresenceUpdate(entity, show, priority, statuses, profile)
+        self.update()
+
+    def onNickUpdate(self, entity, new_nick, profile):
         self.update()
 
-    def __groupClicked(self, group_wid):
-        group = self.groups[group_wid.getValue()]
-        group[0] = not group[0]
-        self.update()
-        self.setFocus(group_wid.getValue())
+    # Methods to build the widget
+
+    def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None):
+        """Build one contact markup data
 
-    def __contactClicked(self, contact_wid, selected):
-        self.selected = contact_wid.data
-        for widget in self.frame.body.body:
-            if widget.__class__ == sat_widgets.SelectableText:
-                widget.setState(widget.data == self.selected, invisible=True)
-        if self.selected in self.alert_jid:
-            self.alert_jid.remove(self.selected)
-        self.host.modeHint('INSERTION')
-        self.update()
-        self._emit('click')
+        @param entity (jid.JID): entity to build
+        @param keys (iterable): value to markup, in preferred order.
+            The first available key will be used.
+            If key starts with "cache_", it will be checked in cache,
+            else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
+            If nothing full or keys is None, full entity is used.
+        @param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons
+        @param with_alert (bool): if True, show alert if entity is in self._alerts
+        @param with_show_attr (bool): if True, show color corresponding to presence status
+        @param markup_prepend (list): markup to prepend to the generated one before building the widget
+        @param markup_append (list): markup to append to the generated one before building the widget
+        @return (list): markup data are expected by Urwid text widgets
+        """
+        markup = []
+        if use_bare_jid:
+            alerts = {entity.bare for entity in self._alerts}
+            selected = {entity.bare for entity in self._selected}
+        else:
+            alerts = self._alerts
+            selected = self._selected
+        if keys is None:
+            entity_txt = entity
+        else:
+            cache = self.getCache(entity)
+            for key in keys:
+                if key.startswith('cache_'):
+                    entity_txt = cache.get(key[6:])
+                else:
+                    entity_txt = getattr(entity, key)
+                if entity_txt:
+                    break
+            if not entity_txt:
+                entity_txt = entity
 
-    def __buildContact(self, content, contacts):
-        """Add contact representation in widget list
+        if with_show_attr:
+            show = self.getCache(entity, C.PRESENCE_SHOW)
+            if show is None:
+                show = C.PRESENCE_UNAVAILABLE
+            show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default'))
+            markup.insert(0, u"{} ".format(show_icon))
+        else:
+            entity_attr = 'default'
+
+        if with_alert and entity in alerts:
+            entity_attr = 'alert'
+            header = C.ALERT_HEADER
+        else:
+            header = ''
+
+        markup.append((entity_attr, entity_txt))
+        if markup_prepend:
+            markup.insert(0, markup_prepend)
+        if markup_append:
+            markup.extend(markup_append)
+
+        widget = sat_widgets.SelectableText(markup,
+                                            selected = entity in selected,
+                                            header = header)
+        widget.data = entity
+        widget.comp = entity_txt.lower() # value to use for sorting
+        urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid])
+        return widget
+
+    def _buildEntities(self, content, entities):
+        """Add entity representation in widget list
+
         @param content: widget list, e.g. SimpleListWalker
-        @param contacts (list): list of JID userhosts"""
-        if not contacts:
+        @param entities (iterable): iterable of JID to display
+        """
+        if not entities:
             return
         widgets = []  # list of built widgets
 
-        for contact in contacts:
-            if contact.startswith(C.PRIVATE_PREFIX):
-                contact_disp = ('alert' if contact in self.alert_jid else "show_normal", unescapePrivate(contact))
-                show_icon = ''
-                status = ''
+        for entity in entities:
+            if entity in self._specials or not self.entityToShow(entity):
+                continue
+            markup_extra = []
+            if self.show_resources:
+                for resource in self.getCache(entity, C.CONTACT_RESOURCES):
+                    resource_disp = ('resource_main' if resource == self.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n  " + resource)
+                    markup_extra.append(resource_disp)
+                    if self.show_status:
+                        status = self.getCache(jid.JID('%s/%s' % (entity, resource)), 'status')
+                        status_disp = ('status', "\n    " + status) if status else ""
+                        markup_extra.append(status_disp)
+
+
             else:
-                jid = JID(contact)
-                name = self.getCache(jid, 'name')
-                nick = self.getCache(jid, 'nick')
-                status = self.getCache(jid, 'status')
-                show = self.getCache(jid, 'show')
-                if show is None:
-                    show = "unavailable"
-                if not self.contactToShow(contact):
-                    continue
-                show_icon, show_attr = C.PRESENCE.get(show, ('', 'default'))
-                contact_disp = ('alert' if contact in self.alert_jid else show_attr, nick or name or jid.node or jid.bare)
-            display = [show_icon + " ", contact_disp]
-            if self.show_status:
-                status_disp = ('status', "\n  " + status) if status else ""
-                display.append(status_disp)
-            header = '(*) ' if contact in self.alert_jid else ''
-            widget = sat_widgets.SelectableText(display,
-                                                selected=contact == self.selected,
-                                                header=header)
-            widget.data = contact
-            widget.comp = contact_disp[1].lower()  # value to use for sorting
+                if self.show_status:
+                    status = self.getCache(entity, 'status')
+                    status_disp = ('status', "\n  " + status) if status else ""
+                    markup_extra.append(status_disp)
+            widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), use_bare_jid=True, markup_append=markup_extra)
             widgets.append(widget)
 
         widgets.sort(key=lambda widget: widget.comp)
 
         for widget in widgets:
             content.append(widget)
-            urwid.connect_signal(widget, 'change', self.__contactClicked)
 
-    def __buildSpecials(self, content):
+    def _buildSpecials(self, content):
         """Build the special entities"""
-        specials = self.specials.keys()
+        specials = list(self._specials)
         specials.sort()
-        for special in specials:
-            jid=JID(special)
-            name = self.getCache(jid, 'name')
-            nick = self.getCache(jid, 'nick')
-            special_disp = ('alert' if special in self.alert_jid else 'default', nick or name or jid.node or jid.bare)
-            display = [ "  " , special_disp]
-            header = '(*) ' if special in self.alert_jid else ''
-            widget = sat_widgets.SelectableText(display,
-                                                selected = special==self.selected,
-                                                header=header)
-            widget.data = special
+        extra_shown = set()
+        for entity in specials:
+            # the special widgets
+            widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False)
             content.append(widget)
-            urwid.connect_signal(widget, 'change', self.__contactClicked)
 
-    def __buildList(self):
+            # resources which must be displayed (e.g. MUC private conversations)
+            extras = [extra for extra in self._special_extras if extra.bare == entity.bare]
+            extras.sort()
+            for extra in extras:
+                widget = self._buildEntityWidget(extra, ('resource',), markup_prepend = '  ')
+                content.append(widget)
+                extra_shown.add(extra)
+
+        # entities which must be visible but not resource of current special entities
+        for extra in self._special_extras.difference(extra_shown):
+            widget = self._buildEntityWidget(extra, ('resource',))
+            content.append(widget)
+
+    def _buildList(self):
         """Build the main contact list widget"""
         content = urwid.SimpleListWalker([])
 
-        self.__buildSpecials(content)
-        if self.specials:
+        self._buildSpecials(content)
+        if self._specials:
             content.append(urwid.Divider('='))
 
-        group_keys = self.groups.keys()
-        group_keys.sort(key=lambda x: x.lower() if x else x)
-        for key in group_keys:
-            unfolded = self.groups[key][0]
-            contacts = list(self.groups[key][1])
-            if key is not None and (self.nonEmptyGroup(contacts) or self.show_empty_groups):
-                header = '[-]' if unfolded else '[+]'
-                widget = sat_widgets.ClickableText(key, header=header + ' ')
+        groups = list(self._groups)
+        groups.sort(key=lambda x: x.lower() if x else x)
+        for group in groups:
+            data = self.getGroupData(group)
+            folded = data.get(C.GROUP_DATA_FOLDED, False)
+            jids = list(data['jids'])
+            if group is not None and (self.anyEntityToShow(jids) or self.show_empty_groups):
+                header = '[-]' if not folded else '[+]'
+                widget = sat_widgets.ClickableText(group, header=header + ' ')
                 content.append(widget)
-                urwid.connect_signal(widget, 'click', self.__groupClicked)
-            if unfolded:
-                self.__buildContact(content, contacts)
-        return urwid.ListBox(content)
-
-    def contactToShow(self, contact):
-        """Tell if the contact should be showed or hidden.
-
-        @param contact (str): JID userhost of the contact
-        @return: True if that contact should be showed in the list"""
-        show = self.getCache(JID(contact), 'show')
-        return (show is not None and show != "unavailable") or \
-            self.show_disconnected or contact in self.alert_jid or contact == self.selected
-
-    def nonEmptyGroup(self, contacts):
-        """Tell if a contact group contains some contacts to show.
-
-        @param contacts (list[str]): list of JID userhosts
-        @return: bool
-        """
-        for contact in contacts:
-            if self.contactToShow(contact):
-                return True
-        return False
-
-    def unselectAll(self):
-        """Unselect all contacts"""
-        self.selected = None
-        for widget in self.frame.body.body:
-            if widget.__class__ == sat_widgets.SelectableText:
-                widget.setState(False, invisible=True)
-
-    def getContact(self):
-        """Return contact currently selected"""
-        return self.selected
-
-    def clearContacts(self):
-        """clear all the contact list"""
-        QuickContactList.clearContacts(self)
-        self.groups={}
-        self.selected = None
-        self.unselectAll()
-        self.update()
-
-    def replace(self, jid, groups=None, attributes=None):
-        """Add a contact to the list if doesn't exist, else update it.
-
-        This method can be called with groups=None for the purpose of updating
-        the contact's attributes (e.g. nickname). In that case, the groups
-        attribute must not be set to the default group but ignored. If not,
-        you may move your contact from its actual group(s) to the default one.
-
-        None value for 'groups' has a different meaning than [None] which is for the default group.
+                urwid.connect_signal(widget, 'click', self._groupClicked)
+            if not folded:
+                self._buildEntities(content, jids)
+        not_in_roster = set(self._cache).difference(self._roster).difference(self._specials).difference((self.whoami.bare,))
+        if not_in_roster:
+            content.append(urwid.Divider('-'))
+            self._buildEntities(content, not_in_roster)
 
-        @param jid (JID)
-        @param groups (list): list of groups or None to ignore the groups membership.
-        @param attributes (dict)
-        """
-        QuickContactList.replace(self, jid, groups, attributes)  # eventually change the nickname
-        if jid.bare in self.specials:
-            return
-        if groups is None:
-            self.update()
-            return
-        assert isinstance(jid, JID)
-        assert isinstance(groups, list)
-        if groups == []:
-            groups = [None]  # [None] is the default group
-        for group in [group for group in self.groups if group not in groups]:
-            try:  # remove the contact from a previous group
-                self.groups[group][1].remove(jid.bare)
-            except KeyError:
-                pass
-        for group in groups:
-            if group not in self.groups:
-                self.groups[group] = [True, set()]  # [unfold, list_of_contacts]
-            self.groups[group][1].add(jid.bare)
-        self.update()
-
-    def remove(self, jid):
-        """remove a contact from the list"""
-        QuickContactList.remove(self, jid)
-        groups_to_remove = []
-        for group in self.groups:
-            contacts = self.groups[group][1]
-            if jid.bare in contacts:
-                contacts.remove(jid.bare)
-                if not len(contacts):
-                    groups_to_remove.append(group)
-        for group in groups_to_remove:
-            del self.groups[group]
-        self.update()
-
-    def add(self, jid, param_groups=None):
-        """add a contact to the list"""
-        self.replace(jid, param_groups if param_groups else [None])
-
-    def setSpecial(self, special_jid, special_type, show=False):
-        """Set entity as a special
-        @param special_jid: jid of the entity
-        @param special_type: special type (e.g.: "MUC")
-        @param show: True to display the dialog to chat with this entity
-        """
-        QuickContactList.setSpecial(self, special_jid, special_type, show)
-        if None in self.groups:
-            folded, group_jids = self.groups[None]
-            for group_jid in group_jids:
-                if JID(group_jid).bare == special_jid.bare:
-                    group_jids.remove(group_jid)
-                    break
-        self.update()
-        if show:
-            # also display the dialog for this room
-            self.setFocus(special_jid, True)
-            self.host.redraw()
-
-    def updatePresence(self, jid, show, priority, statuses):
-        #XXX: for the moment, we ignore presence updates for special entities
-        if jid.bare not in self.specials:
-            QuickContactList.updatePresence(self, jid, show, priority, statuses)
-
-    def showOfflineContacts(self, show):
-        show = C.bool(show)
-        if self.show_disconnected == show:
-            return
-        self.show_disconnected = show
-        self.update()
-
-    def showEmptyGroups(self, show):
-        show = C.bool(show)
-        if self.show_empty_groups == show:
-            return
-        self.show_empty_groups = show
-        self.update()
+        return urwid.ListBox(content)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/primitivus/game_tarot.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,348 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Primitivus: a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.i18n import _
+import urwid
+from urwid_satext import sat_widgets
+from sat_frontends.tools.games import TarotCard
+from sat_frontends.quick_frontend.quick_game_tarot import QuickTarotGame
+from sat_frontends.primitivus import xmlui
+from sat_frontends.primitivus.keys import action_key_map as a_key
+
+
+class CardDisplayer(urwid.Text):
+    """Show a card"""
+    signals = ['click']
+
+    def __init__(self, card):
+        self.__selected = False
+        self.card = card
+        urwid.Text.__init__(self, card.getAttrText())
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        if key == a_key['CARD_SELECT']:
+            self.select(not self.__selected)
+            self._emit('click')
+        return key
+
+    def mouse_event(self, size, event, button, x, y, focus):
+        if urwid.is_mouse_event(event) and button == 1:
+            self.select(not self.__selected)
+            self._emit('click')
+            return True
+
+        return False
+
+    def select(self, state=True):
+        self.__selected = state
+        attr, txt = self.card.getAttrText()
+        if self.__selected:
+            attr += '_selected'
+        self.set_text((attr, txt))
+        self._invalidate()
+
+    def isSelected(self):
+        return self.__selected
+
+    def getCard(self):
+        return self.card
+
+    def render(self, size, focus=False):
+        canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus))
+        if focus:
+            canvas.set_cursor((0, 0))
+        return canvas
+
+
+class Hand(urwid.WidgetWrap):
+    """Used to display several cards, and manage a hand"""
+    signals = ['click']
+
+    def __init__(self, hand=[], selectable=False, on_click=None, user_data=None):
+        """@param hand: list of Card"""
+        self.__selectable = selectable
+        self.columns = urwid.Columns([], dividechars=1)
+        if on_click:
+            urwid.connect_signal(self, 'click', on_click, user_data)
+        if hand:
+            self.update(hand)
+        urwid.WidgetWrap.__init__(self, self.columns)
+
+    def selectable(self):
+        return self.__selectable
+
+    def keypress(self, size, key):
+
+        if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]:
+            return self.columns.keypress(size, key)
+        else:
+            #No card displayed, we still have to manage the clicks
+            if key == a_key['CARD_SELECT']:
+                self._emit('click', None)
+            return key
+
+    def getSelected(self):
+        """Return a list of selected cards"""
+        _selected = []
+        for wid in self.columns.widget_list:
+            if isinstance(wid, CardDisplayer) and wid.isSelected():
+                _selected.append(wid.getCard())
+        return _selected
+
+    def update(self, hand):
+        """Update the hand displayed in this widget
+        @param hand: list of Card"""
+        try:
+            del self.columns.widget_list[:]
+            del self.columns.column_types[:]
+        except IndexError:
+            pass
+        self.columns.contents.append((urwid.Text(''), ('weight', 1, False)))
+        for card in hand:
+            widget = CardDisplayer(card)
+            self.columns.widget_list.append(widget)
+            self.columns.column_types.append(('fixed', 3))
+            urwid.connect_signal(widget, 'click', self.__onClick)
+        self.columns.contents.append((urwid.Text(''), ('weight', 1, False)))
+        self.columns.focus_position = 1
+
+    def __onClick(self, card_wid):
+        self._emit('click', card_wid)
+
+
+class Card(TarotCard):
+    """This class is used to represent a card, logically
+    and give a text representation with attributes"""
+    SIZE = 3  # size of a displayed card
+
+    def __init__(self, suit, value):
+        """@param file: path of the PNG file"""
+        TarotCard.__init__(self, (suit, value))
+
+    def getAttrText(self):
+        """return text representation of the card with attributes"""
+        try:
+            value = "%02i" % int(self.value)
+        except ValueError:
+            value = self.value[0].upper() + self.value[1]
+        if self.suit == "atout":
+            if self.value == "excuse":
+                suit = 'c'
+            else:
+                suit = 'A'
+            color = 'neutral'
+        elif self.suit == "pique":
+            suit = u'♠'
+            color = 'black'
+        elif self.suit == "trefle":
+            suit = u'♣'
+            color = 'black'
+        elif self.suit == "coeur":
+            suit = u'♥'
+            color = 'red'
+        elif self.suit == "carreau":
+            suit = u'♦'
+            color = 'red'
+        if self.bout:
+            color = 'special'
+        return ('card_%s' % color, u"%s%s" % (value, suit))
+
+    def getWidget(self):
+        """Return a widget representing the card"""
+        return CardDisplayer(self)
+
+
+class Table(urwid.FlowWidget):
+    """Represent the cards currently on the table"""
+
+    def __init__(self):
+        self.top = self.left = self.bottom = self.right = None
+
+    def putCard(self, location, card):
+        """Put a card on the table
+        @param location: where to put the card (top, left, bottom or right)
+        @param card: Card to play or None"""
+        assert location in ['top', 'left', 'bottom', 'right']
+        assert isinstance(card, Card) or card == None
+        if [getattr(self, place) for place in ['top', 'left', 'bottom', 'right']].count(None) == 0:
+            #If the table is full of card, we remove them
+            self.top = self.left = self.bottom = self.right = None
+        setattr(self, location, card)
+        self._invalidate()
+
+    def rows(self, size, focus=False):
+        return self.display_widget(size, focus).rows(size, focus)
+
+    def render(self, size, focus=False):
+        return self.display_widget(size, focus).render(size, focus)
+
+    def display_widget(self, size, focus):
+        cards = {}
+        max_col, = size
+        separator = " - "
+        margin = max((max_col - Card.SIZE) / 2, 0) * ' '
+        margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * ' '
+        for location in ['top', 'left', 'bottom', 'right']:
+            card = getattr(self, location)
+            cards[location] = card.getAttrText() if card else Card.SIZE * ' '
+        render_wid = [urwid.Text([margin, cards['top']]),
+                      urwid.Text([margin_center, cards['left'], separator, cards['right']]),
+                      urwid.Text([margin, cards['bottom']])]
+        return urwid.Pile(render_wid)
+
+
+class TarotGame(QuickTarotGame, urwid.WidgetWrap):
+    """Widget for card games"""
+
+    def __init__(self, parent, referee, players):
+        QuickTarotGame.__init__(self, parent, referee, players)
+        self.loadCards()
+        self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), 'center')])
+        #self.parent.host.debug()
+        self.table = Table()
+        self.center = urwid.Columns([('fixed', len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))),
+                                urwid.Filler(self.table),
+                                ('fixed', len(self.right_nick), urwid.Filler(urwid.Text(self.right_nick)))
+                               ])
+        """urwid.Pile([urwid.Padding(self.top_card_wid,'center'),
+                             urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)),
+                                            urwid.Padding(self.center_cards_wid,'center'),
+                                            ('fixed',len(self.right_nick),urwid.Text(self.right_nick))
+                                           ]),
+                             urwid.Padding(self.bottom_card_wid,'center')
+                             ])"""
+        self.hand_wid = Hand(selectable=True, on_click=self.onClick)
+        self.main_frame = urwid.Frame(self.center, header=self.top, footer=self.hand_wid, focus_part='footer')
+        urwid.WidgetWrap.__init__(self, self.main_frame)
+        self.parent.host.bridge.tarotGameReady(self.player_nick, referee, self.parent.profile)
+
+    def loadCards(self):
+        """Load all the cards in memory"""
+        QuickTarotGame.loadCards(self)
+        for value in map(str, range(1, 22)) + ['excuse']:
+            card = Card('atout', value)
+            self.cards[card.suit, card.value] = card
+            self.deck.append(card)
+        for suit in ["pique", "coeur", "carreau", "trefle"]:
+            for value in map(str, range(1, 11)) + ["valet", "cavalier", "dame", "roi"]:
+                card = Card(suit, value)
+                self.cards[card.suit, card.value] = card
+                self.deck.append(card)
+
+    def tarotGameNewHandler(self, hand):
+        """Start a new game, with given hand"""
+        if hand is []:  # reset the display after the scores have been showed
+            self.resetRound()
+            for location in ['top', 'left', 'bottom', 'right']:
+                self.table.putCard(location, None)
+            self.parent.host.redraw()
+            self.parent.host.bridge.tarotGameReady(self.player_nick, self.referee, self.parent.profile)
+            return
+        QuickTarotGame.tarotGameNewHandler(self, hand)
+        self.hand_wid.update(self.hand)
+        self.parent.host.redraw()
+
+    def tarotGameChooseContratHandler(self, xml_data):
+        """Called when the player has to select his contrat
+        @param xml_data: SàT xml representation of the form"""
+        form = xmlui.create(self.parent.host, xml_data, title=_('Please choose your contrat'), flags=['NO_CANCEL'], profile=self.parent.profile)
+        form.show(valign='top')
+
+    def tarotGameShowCardsHandler(self, game_stage, cards, data):
+        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
+        QuickTarotGame.tarotGameShowCardsHandler(self, game_stage, cards, data)
+        self.center.widget_list[1] = urwid.Filler(Hand(self.to_show))
+        self.parent.host.redraw()
+
+    def tarotGameYourTurnHandler(self):
+        QuickTarotGame.tarotGameYourTurnHandler(self)
+
+    def tarotGameScoreHandler(self, xml_data, winners, loosers):
+        """Called when the round is over, display the scores
+        @param xml_data: SàT xml representation of the form"""
+        if not winners and not loosers:
+            title = _("Draw game")
+        else:
+            title = _('You win \o/') if self.player_nick in winners else _('You loose :(')
+        form = xmlui.create(self.parent.host, xml_data, title=title, flags=['NO_CANCEL'], profile=self.parent.profile)
+        form.show()
+
+    def tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards):
+        """Invalid cards have been played
+        @param phase: phase of the game
+        @param played_cards: all the cards played
+        @param invalid_cards: cards which are invalid"""
+        QuickTarotGame.tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards)
+        self.hand_wid.update(self.hand)
+        if self._autoplay == None:  # No dialog if there is autoplay
+            self.parent.host.notify(_('Cards played are invalid !'))
+        self.parent.host.redraw()
+
+    def tarotGameCardsPlayedHandler(self, player, cards):
+        """A card has been played by player"""
+        QuickTarotGame.tarotGameCardsPlayedHandler(self, player, cards)
+        self.table.putCard(self.getPlayerLocation(player), self.played[player])
+        self._checkState()
+        self.parent.host.redraw()
+
+    def _checkState(self):
+        if isinstance(self.center.widget_list[1].original_widget, Hand):  # if we have a hand displayed
+            self.center.widget_list[1] = urwid.Filler(self.table)  # we show again the table
+            if self.state == "chien":
+                self.to_show = []
+                self.state = "wait"
+            elif self.state == "wait_for_ecart":
+                self.state = "ecart"
+                self.hand.extend(self.to_show)
+                self.hand.sort()
+                self.to_show = []
+                self.hand_wid.update(self.hand)
+
+    ##EVENTS##
+    def onClick(self, hand, card_wid):
+        """Called when user do an action on the hand"""
+        if not self.state in ['play', 'ecart', 'wait_for_ecart']:
+            #it's not our turn, we ignore the click
+            card_wid.select(False)
+            return
+        self._checkState()
+        if self.state == "ecart":
+            if len(self.hand_wid.getSelected()) == 6:
+                pop_up_widget = sat_widgets.ConfirmDialog(_("Do you put these cards in chien ?"), yes_cb=self.onEcartDone, no_cb=self.parent.host.removePopUp)
+                self.parent.host.showPopUp(pop_up_widget)
+        elif self.state == "play":
+            card = card_wid.getCard()
+            self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], self.parent.profile)
+            self.hand.remove(card)
+            self.hand_wid.update(self.hand)
+            self.state = "wait"
+
+    def onEcartDone(self, button):
+        """Called when player has finished his écart"""
+        ecart = []
+        for card in self.hand_wid.getSelected():
+            ecart.append((card.suit, card.value))
+            self.hand.remove(card)
+        self.hand_wid.update(self.hand)
+        self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, self.parent.profile)
+        self.state = "wait"
+        self.parent.host.removePopUp()
--- a/frontends/src/primitivus/keys.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/keys.py	Wed Mar 18 10:52:28 2015 +0100
@@ -42,15 +42,18 @@
         ("menu_global", "APP_QUIT"): 'ctrl x',
         ("menu_global", "ROOM_JOIN"): 'meta j',
 
+        # primitivus widgets
+        ("primitivus_widget", "DECORATION_HIDE"): "meta l",
+
         # contact list
         ("contact_list", "STATUS_HIDE"): "meta s",
         ("contact_list", "DISCONNECTED_HIDE"): "meta d",
+        ("contact_list", "RESOURCES_HIDE"): "meta r",
 
         # chat panel
         ("chat_panel", "OCCUPANTS_HIDE"): "meta p",
         ("chat_panel", "TIMESTAMP_HIDE"): "meta t",
         ("chat_panel", "SHORT_NICKNAME"): "meta n",
-        ("chat_panel", "DECORATION_HIDE"): "meta l",
         ("chat_panel", "SUBJECT_SWITCH"): "meta s",
 
         #card game
--- a/frontends/src/primitivus/primitivus	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/primitivus	Wed Mar 18 10:52:28 2015 +0100
@@ -27,9 +27,10 @@
 import urwid
 from urwid_satext import sat_widgets
 from urwid_satext.files_management import FileDialog
+from sat_frontends.bridge.DBus import DBusBridgeFrontend
 from sat_frontends.quick_frontend.quick_app import QuickApp
-from sat_frontends.quick_frontend.quick_chat_list import QuickChatList
-from sat_frontends.quick_frontend.quick_utils import getNewPath, unescapePrivate
+from sat_frontends.quick_frontend import quick_utils
+from sat_frontends.quick_frontend import quick_chat
 from sat_frontends.primitivus.profile_manager import ProfileManager
 from sat_frontends.primitivus.contact_list import ContactList
 from sat_frontends.primitivus.chat import Chat
@@ -39,35 +40,27 @@
 from sat_frontends.primitivus.keys import action_key_map as a_key
 from sat_frontends.primitivus import config
 from sat_frontends.tools.misc import InputHistory
-from sat_frontends.constants import Const as commonConst # FIXME
-from sat_frontends.tools.jid import JID
+from sat_frontends.tools import jid
 from os.path import join
 import signal
 
 
-class ChatList(QuickChatList):
-    """This class manage the list of chat windows"""
-
-    def createChat(self, target):
-        return Chat(target, self.host)
-
-
 class EditBar(sat_widgets.ModalEdit):
     """
     The modal edit bar where you would enter messages and commands.
     """
 
-    def __init__(self, app):
-        modes = {None: ('NORMAL', u''),
-                 a_key['MODE_INSERTION']: ('INSERTION', u'> '),
-                 a_key['MODE_COMMAND']: ('COMMAND', u':')} #XXX: captions *MUST* be unicode
+    def __init__(self, host):
+        modes = {None: (C.MODE_NORMAL, u''),
+                 a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '),
+                 a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode
         super(EditBar, self).__init__(modes)
-        self.app = app
+        self.host = host
         self.setCompletionMethod(self._text_completion)
         urwid.connect_signal(self, 'click', self.onTextEntered)
 
     def _text_completion(self, text, completion_data, mode):
-        if mode == 'INSERTION':
+        if mode == C.MODE_INSERTION:
             return self._nick_completion(text, completion_data)
         else:
             return text
@@ -75,41 +68,42 @@
     def _nick_completion(self, text, completion_data):
         """Completion method which complete pseudo in group chat
         for params, see AdvancedEdit"""
-        contact = self.app.contact_list.getContact() ###Based on the fact that there is currently only one contact selectable at once
-        if contact:
-            chat = self.app.chat_wins[contact]
-            if chat.type != "group":
-                return text
-            space = text.rfind(" ")
-            start = text[space+1:]
-            nicks = list(chat.occupants)
+        nicks = []
+        for profile, clist in self.host.contact_lists.iteritems():
+            for contact in clist.getContacts():
+                chat = self.host.widgets.getWidget(quick_chat.QuickChat, contact, profile)
+                if chat.type != C.CHAT_GROUP:
+                    continue
+                space = text.rfind(" ")
+                start = text[space + 1:]
+                nicks.extend(list(chat.occupants))
+        if nicks:
             nicks.sort()
             try:
-                start_idx=nicks.index(completion_data['last_nick'])+1
+                start_idx = nicks.index(completion_data['last_nick']) + 1
                 if start_idx == len(nicks):
                     start_idx = 0
-            except (KeyError,ValueError):
+            except (KeyError, ValueError):
                 start_idx = 0
-            for idx in range(start_idx,len(nicks)) + range(0,start_idx):
+            for idx in range(start_idx, len(nicks)) + range(0, start_idx):
                 if nicks[idx].lower().startswith(start.lower()):
                     completion_data['last_nick'] = nicks[idx]
-                    return text[:space+1] + nicks[idx] + (': ' if space < 0 else '')
+                    return text[:space + 1] + nicks[idx] + (': ' if space < 0 else '')
         return text
 
     def onTextEntered(self, editBar):
         """Called when text is entered in the main edit bar"""
-        if self.mode == 'INSERTION':
-            contact = self.app.contact_list.getContact() ###Based on the fact that there is currently only one contact selectableat once
-            if contact:
-                chat = self.app.chat_wins[contact]
-                self.app.sendMessage(contact,
+        if self.mode == C.MODE_INSERTION:
+            if isinstance(self.host.selected_widget, quick_chat.QuickChat):
+                chat_widget = self.host.selected_widget
+                self.host.sendMessage(chat_widget.target,
                                      editBar.get_edit_text(),
-                                     mess_type = "groupchat" if chat.type == 'group' else "chat",
-                                     errback=lambda failure: self.app.notify(_("Error while sending message (%s)") % failure),
-                                     profile_key=self.app.profile
+                                     mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
+                                     errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
+                                     profile_key=chat_widget.profile
                                     )
                 editBar.set_edit_text('')
-        elif self.mode == 'COMMAND':
+        elif self.mode == C.MODE_COMMAND:
             self.commandHandler()
 
     def commandHandler(self):
@@ -118,42 +112,44 @@
         tokens = self.get_edit_text().split(' ')
         command, args = tokens[0], tokens[1:]
         if command == 'quit':
-            self.app.onExit()
+            self.host.onExit()
             raise urwid.ExitMainLoop()
         elif command == 'messages':
             wid = sat_widgets.GenericList(logging.memoryGet())
-            self.app.addWindow(wid)
-        elif command == 'presence':
-            values = [value for value in commonConst.PRESENCE.keys()]
-            values = [value if value else 'online' for value in values]  # the empty value actually means 'online'
-            if args and args[0] in values:
-                presence = '' if args[0] == 'online' else args[0]
-                self.app.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
-            else:
-                self.app.status_bar.onPresenceClick()
-        elif command == 'status':
-            if args:
-                self.app.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
-            else:
-                self.app.status_bar.onStatusClick()
+            self.host.selectWidget(wid)
+        # elif command == 'presence':
+        #     values = [value for value in commonConst.PRESENCE.keys()]
+        #     values = [value if value else 'online' for value in values]  # the empty value actually means 'online'
+        #     if args and args[0] in values:
+        #         presence = '' if args[0] == 'online' else args[0]
+        #         self.host.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
+        #     else:
+        #         self.host.status_bar.onPresenceClick()
+        # elif command == 'status':
+        #     if args:
+        #         self.host.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
+        #     else:
+        #         self.host.status_bar.onStatusClick()
         elif command == 'history':
-            try:
-                limit = int(args[0])
-            except (IndexError, ValueError):
-                limit = 50
-            win = self.app.chat_wins[JID(self.app.contact_list.selected).bare]
-            win.clearHistory()
-            if limit > 0:
-                win.historyPrint(size=limit, profile=self.app.profile)
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat):
+                try:
+                    limit = int(args[0])
+                except (IndexError, ValueError):
+                    limit = 50
+                widget.clearHistory()
+                if limit > 0:
+                    widget.historyPrint(size=limit, profile=widget.profile)
         elif command == 'search':
-            pattern = " ".join(args)
-            if not pattern:
-                self.app.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
-            win = self.app.chat_wins[JID(self.app.contact_list.selected).bare]
-            win.clearHistory()
-            win.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, timestamp=0)
-            win.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=self.app.profile)
-            win.printInfo(D_("Type ':history <lines>' to reset the chat history"))
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat):
+                pattern = " ".join(args)
+                if not pattern:
+                    self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
+                widget.clearHistory()
+                widget.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, timestamp=0)
+                widget.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
+                widget.printInfo(D_("Type ':history <lines>' to reset the chat history"))
         else:
             return
         self.set_edit_text('')
@@ -167,22 +163,24 @@
         and move the index of the temporary history stack."""
         if key == a_key['MODAL_ESCAPE']:
             # first save the text to the current mode, then change to NORMAL
-            self.app._updateInputHistory(self.get_edit_text(), mode=self.mode)
-            self.app._updateInputHistory(mode='NORMAL')
-        if self._mode == 'NORMAL' and key in self._modes:
-            self.app._updateInputHistory(mode=self._modes[key][0])
+            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
+            self.host._updateInputHistory(mode=C.MODE_NORMAL)
+        if self._mode == C.MODE_NORMAL and key in self._modes:
+            self.host._updateInputHistory(mode=self._modes[key][0])
         if key == a_key['HISTORY_PREV']:
-            self.app._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
+            self.host._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
             return
         elif key == a_key['HISTORY_NEXT']:
-            self.app._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
+            self.host._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
             return
         elif key == a_key['EDIT_ENTER']:
-            self.app._updateInputHistory(self.get_edit_text(), mode=self.mode)
+            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
         else:
-            contact = self.app.contact_list.getContact()
-            if contact:
-                self.app.bridge.chatStateComposing(unescapePrivate(contact), self.app.profile)
+            if (self._mode == C.MODE_INSERTION
+                and isinstance(self.host.selected_widget, quick_chat.QuickChat)
+                and key not in sat_widgets.FOCUS_KEYS):
+                self.host.bridge.chatStateComposing(self.host.selected_widget.target, self.host.selected_widget.profile)
+
         return super(EditBar, self).keypress(size, key)
 
 
@@ -281,18 +279,19 @@
 class PrimitivusApp(QuickApp, InputHistory):
 
     def __init__(self):
-        QuickApp.__init__(self)
+        QuickApp.__init__(self, create_bridge=DBusBridgeFrontend, check_options=quick_utils.check_options)
 
         ## main loop setup ##
         self.main_widget = ProfileManager(self)
         self.loop = urwid.MainLoop(self.main_widget, C.PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
 
         ##misc setup##
-        self.chat_wins = ChatList(self)
+        self._visible_widgets = set()
         self.notif_bar = sat_widgets.NotificationBar()
         urwid.connect_signal(self.notif_bar, 'change', self.onNotification)
-        self.progress_wid = Progress(self)
-        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.addWindow(self.progress_wid))
+
+        self.progress_wid = self.widgets.getOrCreateWidget(Progress, None, on_new_widget=None)
+        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.selectWidget(self.progress_wid))
         self.__saved_overlay = None
 
         self.x_notify = Notify()
@@ -301,6 +300,10 @@
         signal.signal(signal.SIGINT, signal.SIG_IGN)
 
     @property
+    def visible_widgets(self):
+        return self._visible_widgets
+
+    @property
     def mode(self):
         return self.editBar.mode
 
@@ -351,7 +354,7 @@
                 self._early_popup = popup
             else:
                 self.showPopUp(popup)
-        super(PrimitivusApp, self).postInit()
+        super(PrimitivusApp, self).postInit(self.main_widget)
 
     def inputFilter(self, input_, raw):
         if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
@@ -387,14 +390,14 @@
 
         elif input_ == a_key['DEBUG'] and 'D' in self.bridge.getVersion(): #Debug only for dev versions
             self.debug()
-        elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact_list
+        elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
             try:
                 for wid, options in self.center_part.contents:
-                    if self.contact_list is wid:
+                    if self.contact_lists_pile is wid:
                         self.center_part.contents.remove((wid, options))
                         break
                 else:
-                    self.center_part.contents.insert(0, (self.contact_list, ('weight', 2, False)))
+                    self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
             except AttributeError:
                 #The main widget is not built (probably in Profile Manager)
                 pass
@@ -413,17 +416,18 @@
         except AttributeError:
             return input_
 
-    def addMenus(self, menu, type_, menu_data=None):
+    def addMenus(self, menu, type_filter, menu_data=None):
         """Add cached menus to instance
         @param menu: sat_widgets.Menu instance
-        @param type_: menu type like is sat.core.sat_main.importMenu
+        @param type_filter: menu type like is sat.core.sat_main.importMenu
         @param menu_data: data to send with these menus
 
         """
-        menus = self.profiles[self.profile]['menus'].get(type_,[])
         def add_menu_cb(callback_id):
-            self.launchAction(callback_id, menu_data, profile_key = self.profile)
-        for id_, path, path_i18n  in menus:
+            self.launchAction(callback_id, menu_data, profile=self.current_profile)
+        for id_, type_, path, path_i18n, extra  in self.bridge.getMenus("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
+            if type_ != type_filter:
+                continue
             if len(path) != 2:
                 raise NotImplementedError("Menu with a path != 2 are not implemented yet")
             menu.addMenu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
@@ -445,23 +449,27 @@
         #FIXME: do this in a more generic way (in quickapp)
         self.addMenus(menu, C.MENU_GLOBAL)
 
-        menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)])
+        menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
         return menu_roller
 
     def _buildMainWidget(self):
-        self.contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw())
-        #self.center_part = urwid.Columns([('weight',2,self.contact_list),('weight',8,Chat('',self))])
-        self.center_part = urwid.Columns([('weight', 2, self.contact_list), ('weight', 8, urwid.Filler(urwid.Text('')))])
+        self.contact_lists_pile = urwid.Pile([])
+        #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
+        self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
 
         self.editBar = EditBar(self)
         self.menu_roller = self._buildMenuRoller()
         self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
         return self.main_widget
 
-    def plug_profile_1(self, profile_key='@DEFAULT@'):
+    def addContactList(self, profile):
+        contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile)
+        self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
+        return contact_list
+
+    def plugging_profiles(self):
         self.loop.widget = self._buildMainWidget()
         self.redraw()
-        QuickApp.plug_profile_1(self, profile_key)
         try:
             # if a popup arrived before main widget is build, we need to show it now
             self.showPopUp(self._early_popup)
@@ -492,21 +500,38 @@
         self.notif_bar.addMessage(message)
         self.redraw()
 
-    def addWindow(self, widget):
-        """Display a window if possible,
+    def newWidget(self, widget):
+        self.selectWidget(widget)
+
+    def selectWidget(self, widget):
+        """Display a widget if possible,
+
         else add it in the notification bar queue
-        @param widget: BoxWidget"""
-        assert(len(self.center_part.widget_list)<=2)
+        @param widget: BoxWidget
+        """
+        assert len(self.center_part.widget_list)<=2
         wid_idx = len(self.center_part.widget_list)-1
         self.center_part.widget_list[wid_idx] = widget
-        self.menu_roller.removeMenu(_('Chat menu'))
-        self.contact_list.unselectAll()
+        try:
+            self.menu_roller.removeMenu(C.MENU_ID_WIDGET)
+        except KeyError:
+            log.debug("No menu to delete")
+        self.selected_widget = widget
+        self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
+        for contact_list in self.contact_lists.itervalues():
+            contact_list.unselectAll()
+
+        for wid in self.visible_widgets:
+            if isinstance(wid, Chat):
+                contact_list = self.contact_lists[wid.profile]
+                contact_list.select(wid.target)
+
         self.redraw()
 
     def removeWindow(self):
         """Remove window showed on the right column"""
-        #TODO: to a better Window management than this crappy hack
-        assert(len(self.center_part.widget_list)<=2)
+        #TODO: better Window management than this hack
+        assert len(self.center_part.widget_list) <= 2
         wid_idx = len(self.center_part.widget_list)-1
         self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(''))
         self.center_part.focus_position = 0
@@ -522,23 +547,14 @@
         """Set the progression shown in notification bar"""
         self.notif_bar.setProgress(percentage)
 
-    def contactSelected(self, contact_list):
-        contact = contact_list.getContact()
-        if contact:
-            assert(len(self.center_part.widget_list)==2)
-            self.center_part.widget_list[1] = self.chat_wins[contact]
-            self.menu_roller.addMenu(_('Chat menu'), self.chat_wins[contact].getMenu())
-
-    def newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile):
-        QuickApp.newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile)
-
-        if not from_jid in self.contact_list and from_jid.bare != self.profiles[profile]['whoami'].bare:
-            #XXX: needed to show entities which haven't sent any
-            #     presence information and which are not in roster
-            self.contact_list.replace(from_jid, [C.GROUP_NOT_IN_ROSTER])
-
-        if self.contact_list.selected is None or JID(self.contact_list.selected).bare != from_jid.bare:
-            self.contact_list.putAlert(from_jid)
+    def contactSelected(self, contact_list, entity):
+        if entity.resource:
+            # we have clicked on a private MUC conversation
+            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
+        else:
+            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
+        self.selectWidget(chat_widget)
+        self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
 
     def _dialogOkCb(self, widget, data):
         self.removePopUp()
@@ -552,7 +568,6 @@
         answer_data = [data[1]] if data[1] else []
         answer_cb(False, *answer_data)
 
-
     def showDialog(self, message, title="", type_="info", answer_cb = None, answer_data = None):
         if type_ == 'info':
             popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
@@ -578,11 +593,15 @@
         else:
             self.main_widget.show('notif_bar')
 
-    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
+    def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
         """ Launch a dynamic action
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
-        @param profile_key: %(doc_profile_key)s
+        @param callback: if not None and 'validated' key is present, it will be called with the following parameters:
+            - callback_id
+            - data
+            - profile
+        @param profile: %(doc_profile)s
 
         """
         if data is None:
@@ -593,31 +612,27 @@
                 # action was a one shot, nothing to do
                 pass
             elif "xmlui" in data:
-                ui = xmlui.create(self, xml_data=data['xmlui'])
+                ui = xmlui.create(self, xml_data=data['xmlui'], callback=callback, profile=profile)
                 ui.show()
-            elif "authenticated_profile" in data:
-                assert("caller" in data)
-                if data["caller"] == "profile_manager":
-                    assert(isinstance(self.main_widget, ProfileManager))
-                    self.main_widget.getXMPPParams(data['authenticated_profile'])
-                elif data["caller"] == "plug_profile":
-                    self.plug_profile_1(data['authenticated_profile'])
-                else:
-                    raise NotImplementedError
+            elif 'validated' in data:
+                pass # this key is managed below
             else:
                 self.showPopUp(sat_widgets.Alert(_("Error"), _(u"Unmanaged action result"), ok_cb=self.removePopUp))
 
+            if callback and 'validated' in data:
+                callback(callback_id, data, profile)
+
         def action_eb(failure):
             self.showPopUp(sat_widgets.Alert(failure.fullname, failure.message, ok_cb=self.removePopUp))
 
-        self.bridge.launchAction(callback_id, data, profile_key, callback=action_cb, errback=action_eb)
+        self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=action_eb)
 
     def askConfirmationHandler(self, confirmation_id, confirmation_type, data, profile):
         answer_data={}
 
         def dir_selected_cb(path):
             dest_path = join(path, data['filename'])
-            answer_data["dest_path"] = getNewPath(dest_path)
+            answer_data["dest_path"] = quick_utils.getNewPath(dest_path)
             self.addProgress(confirmation_id, dest_path)
             accept_cb(None)
 
@@ -660,7 +675,7 @@
                 title = _('Registration')
                 misc['target'] = data['target']
                 misc['action_back'] = self.bridge.gatewayRegister
-            ui = xmlui.create(self, title=title, xml_data = data['xml'], misc = misc)
+            ui = xmlui.create(self, title=title, xml_data=data['xml'], misc=misc, profile=profile)
             if data['type'] == 'registration':
                 ui.show('popup')
             else:
@@ -684,32 +699,39 @@
             log.error (_("FIXME FIXME FIXME: type [%s] not implemented") % type_)
             raise NotImplementedError
 
+
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
+        super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile)
+        self.contact_lists[profile].setFocus(jid.JID(room_jid_s), True)
+
+
+
     ##DIALOGS CALLBACKS##
     def onJoinRoom(self, button, edit):
         self.removePopUp()
-        room_jid = JID(edit.get_edit_text())
+        room_jid = jid.JID(edit.get_edit_text())
         if room_jid.is_valid():
-            self.bridge.joinMUC(room_jid, self.profiles[self.profile]['whoami'].node, {}, self.profile)
+            self.bridge.joinMUC(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile)
         else:
-            message = _("'%s' is an invalid JID !") % room_jid
+            message = _("'%s' is an invalid jid.JID !") % room_jid
             log.error (message)
             self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp))
 
     #MENU EVENTS#
     def onConnectRequest(self, menu):
-        QuickApp.asyncConnect(self, self.profile)
+        QuickApp.asyncConnect(self, self.current_profile)
 
     def onDisconnectRequest(self, menu):
-        self.bridge.disconnect(self.profile)
+        self.disconnect(self.current_profile)
 
     def onParam(self, menu):
         def success(params):
-            ui = xmlui.create(self, xml_data=params)
+            ui = xmlui.create(self, xml_data=params, profile=self.current_profile)
             ui.show()
 
         def failure(error):
             self.showPopUp(sat_widgets.Alert(_("Error"), _("Can't get parameters (%s)") % error, ok_cb=self.removePopUp))
-        self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.profile, callback=success, errback=failure)
+        self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
 
     def onExitRequest(self, menu):
         QuickApp.onExit(self)
@@ -725,12 +747,12 @@
 
     #MISC CALLBACKS#
 
-    def setStatusOnline(self, online=True, show="", statuses={}):
+    def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
         if not online or not statuses:
-            self.status_bar.setPresenceStatus(show if online else 'unavailable', '')
+            self.contact_lists[profile].status_bar.setPresenceStatus(show if online else 'unavailable', '')
             return
         try:
-            self.status_bar.setPresenceStatus(show, statuses['default'])
+            self.contact_lists[profile].status_bar.setPresenceStatus(show, statuses['default'])
         except (KeyError, TypeError):
             pass
 
--- a/frontends/src/primitivus/profile_manager.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/profile_manager.py	Wed Mar 18 10:52:28 2015 +0100
@@ -18,29 +18,28 @@
 # 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 sat_frontends.quick_frontend.quick_profile_manager import QuickProfileManager
 from sat_frontends.primitivus.constants import Const as C
+from sat_frontends.primitivus.keys import action_key_map as a_key
+from urwid_satext import sat_widgets
 import urwid
-from urwid_satext.sat_widgets import AdvancedEdit, Password, List, InputDialog, ConfirmDialog, Alert
-from sat_frontends.primitivus.keys import action_key_map as a_key
 
 
-class ProfileManager(urwid.WidgetWrap):
+class ProfileManager(QuickProfileManager, urwid.WidgetWrap):
 
-    def __init__(self, host):
-        self.host = host
-        #profiles list
-        profiles = self.host.bridge.getProfilesList()
-        profiles.sort()
+    def __init__(self, host, autoconnect=None):
+        QuickProfileManager.__init__(self, host, autoconnect)
 
         #login & password box must be created before list because of onProfileChange
-        self.login_wid = AdvancedEdit(_('Login:'), align='center')
-        self.pass_wid = Password(_('Password:'), align='center')
+        self.login_wid = sat_widgets.AdvancedEdit(_('Login:'), align='center')
+        self.pass_wid = sat_widgets.Password(_('Password:'), align='center')
 
-        self.selected_profile = None  # allow to reselect the previous selection until the profile is authenticated
-        style = ['single']
-        if self.host.options.profile:
-            style.append('no_first_select')
-        self.list_profile = List(profiles, style=style, align='center', on_change=self.onProfileChange)
+        style = ['no_first_select']
+        profiles = host.bridge.getProfilesList()
+        profiles.sort()
+        self.list_profile = sat_widgets.List(profiles, style=style, align='center', on_change=self.onProfileChange)
 
         #new & delete buttons
         buttons = [urwid.Button(_("New"), self.onNewProfile),
@@ -51,15 +50,18 @@
         divider = urwid.Divider('-')
 
         #connect button
-        connect_button = urwid.Button(_("Connect"), self.onConnectProfile)
+        connect_button = sat_widgets.CustomButton(_("Connect"), self.onConnectProfiles, align='center')
 
         #we now build the widget
-        list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button])
+        list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile, divider, self.login_wid, self.pass_wid, connect_button])
         frame_body = urwid.ListBox(list_walker)
         frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title'))
         self.main_widget = urwid.LineBox(frame)
         urwid.WidgetWrap.__init__(self, self.main_widget)
 
+        self.go(autoconnect)
+
+
     def keypress(self, size, key):
         if key == a_key['APP_QUIT']:
             self.host.onExit()
@@ -80,117 +82,96 @@
                     return
         return super(ProfileManager, self).keypress(size, key)
 
-    def __refillProfiles(self):
-        """Update the list of profiles"""
-        profiles = self.host.bridge.getProfilesList()
-        profiles.sort()
-        self.list_profile.changeValues(profiles)
-
     def cancelDialog(self, button):
         self.host.removePopUp()
 
     def newProfile(self, button, edit):
         """Create the profile"""
         name = edit.get_edit_text()
-        self.host.bridge.asyncCreateProfile(name, callback=lambda: self._newProfileCreated(name), errback=self._profileCreationFailure)
+        self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
 
-    def _newProfileCreated(self, name):
-        self.__refillProfiles()
-        #We select the profile created in the list
-        self.list_profile.selectValue(name)
+    def newProfileCreated(self, profile):
         self.host.removePopUp()
+        self.refillProfiles()
+        self.list_profile.selectValue(profile)
+        self.current.profile=profile
+        self.getConnectionParams(profile)
         self.host.redraw()
 
-    def _profileCreationFailure(self, reason):
+    def profileCreationFailure(self, reason):
         self.host.removePopUp()
-        if reason == "ConflictError":
-            message = _("A profile with this name already exists")
-        elif reason == "CancelError":
-            message = _("Profile creation cancelled by backend")
-        else:
-            message = _("Unknown reason (%s)") % reason
-        popup = Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
-        self.host.showPopUp(popup)
+        message = self._getErrorMessage(reason)
+        self.alert(_("Can't create profile"), message)
 
     def deleteProfile(self, button):
-        profile_name = self.list_profile.getSelectedValue()
-        if profile_name:
-            self.host.bridge.asyncDeleteProfile(profile_name, callback=self.__refillProfiles)
+        self._deleteProfile()
         self.host.removePopUp()
 
     def onNewProfile(self, e):
-        pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
+        pop_up_widget = sat_widgets.InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
         self.host.showPopUp(pop_up_widget)
 
     def onDeleteProfile(self, e):
-        pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
-        self.host.showPopUp(pop_up_widget)
+        if self.current.profile:
+            pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the profile {} ?").format(self.current.profile), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
+            self.host.showPopUp(pop_up_widget)
 
-    def getXMPPParams(self, profile):
-        """This is called from PrimitivusApp.launchAction when the profile has been authenticated.
+    def onConnectProfiles(self, button):
+        """Connect the profiles and start the main widget
 
-        @param profile: %(doc_profile)s
+        @param button: the connect button
         """
-        def setJID(jabberID):
-            self.login_wid.set_edit_text(jabberID)
-            self.host.redraw()
+        self._onConnectProfiles()
+
+    def resetFields(self):
+        """Set profile to None, and reset fields"""
+        super(ProfileManager, self).resetFields()
+        self.list_profile.unselectAll(invisible=True)
+
+    def setProfiles(self, profiles):
+        """Update the list of profiles"""
+        self.list_profile.changeValues(profiles)
+        self.host.redraw()
+
+    def getProfiles(self):
+        return self.list_profile.getSelectedValues()
 
-        def setPassword(password):
-            self.pass_wid.set_edit_text(password)
-            self.host.redraw()
+    def getJID(self):
+        return self.login_wid.get_edit_text()
+
+    def getPassword(self):
+        return self.pass_wid.get_edit_text()
 
-        self.list_profile.selectValue(profile, move_focus=False)
-        self.selected_profile = profile
-        self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError)
-        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError)
+    def setJID(self, jid_):
+        self.login_wid.set_edit_text(jid_)
+        self.current.login = jid_
+        self.host.redraw() # FIXME: redraw should be avoided
+
+    def setPassword(self, password):
+        self.pass_wid.set_edit_text(password)
+        self.current.password = password
+        self.host.redraw()
+
+    def alert(self, title, message):
+        popup = sat_widgets.Alert(title, message, ok_cb=self.host.removePopUp)
+        self.host.showPopUp(popup)
 
     def onProfileChange(self, list_wid):
         """This is called when a profile is selected in the profile list.
 
         @param list_wid: the List widget who sent the event
         """
-        profile_name = list_wid.getSelectedValue()
-        if not profile_name or profile_name == self.selected_profile:
-            return  # avoid infinite loop
-        if self.selected_profile:
-            list_wid.selectValue(self.selected_profile, move_focus=False)
-        else:
-            list_wid.unselectAll(invisible=True)
-        self.host.redraw()
-        self.host.profile = profile_name  # FIXME: EXTREMELY DIRTY, needed for sat_frontends.tools.xmlui.XMLUI._xmluiLaunchAction
-        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'profile_manager'}, profile_key=profile_name)
+        self.updateConnectionParams()
+        focused = list_wid.focus
+        selected = focused.getState()
+        if not selected: # profile was just unselected
+            return
+        focused.setState(False, invisible=True) # we don't want the widget to be selected until we are sure we can access it
+        def authenticate_cb(callback_id, data, profile):
+            if C.bool(data['validated']):
+                self.current.profile = profile
+                focused.setState(True, invisible=True)
+                self.getConnectionParams(profile)
+                self.host.redraw()
+        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text)
 
-    def onConnectProfile(self, button):
-        profile_name = self.list_profile.getSelectedValue()
-        assert(profile_name == self.selected_profile)  # if not, there's a bug somewhere...
-        if not profile_name:
-            pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog)
-            self.host.showPopUp(pop_up_widget)
-        elif profile_name[0] == '@':
-            pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog)
-            self.host.showPopUp(pop_up_widget)
-        else:
-            profile = self.host.bridge.getProfileName(profile_name)
-            assert(profile)
-            #TODO: move this to quick_app
-            self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
-                                            callback=lambda old_jid: self.__old_jidReceived(old_jid, profile), errback=self.getParamError)
-
-    def __old_jidReceived(self, old_jid, profile):
-        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile,
-                                        callback=lambda old_pass: self.__old_passReceived(old_jid, old_pass, profile), errback=self.getParamError)
-
-    def __old_passReceived(self, old_jid, old_pass, profile):
-        """Check if we have new jid/pass, save them if it is the case, and plug profile"""
-        new_jid = self.login_wid.get_edit_text()
-        new_pass = self.pass_wid.get_edit_text()
-
-        if old_jid != new_jid:
-            self.host.bridge.setParam("JabberID", new_jid, "Connection", profile_key=profile)
-        if old_pass != new_pass:
-            self.host.bridge.setParam("Password", new_pass, "Connection", profile_key=profile)
-        self.host.plug_profile(profile)
-
-    def getParamError(self, ignore):
-        popup = Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
-        self.host.showPopUp(popup)
--- a/frontends/src/primitivus/progress.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/progress.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,11 +20,15 @@
 from sat.core.i18n import _
 import urwid
 from urwid_satext import sat_widgets
+from sat_frontends.quick_frontend import quick_widgets
 
 
-class Progress(urwid.WidgetWrap):
+class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
+    PROFILES_ALLOW_NONE = True
 
-    def __init__(self, host):
+    def __init__(self, host, target, profiles):
+        assert target is None and profiles is None
+        quick_widgets.QuickWidget.__init__(self, host, target)
         self.host = host
         self.progress_list = urwid.SimpleListWalker([])
         self.progress_dict = {}
--- a/frontends/src/primitivus/status.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/status.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,7 +20,7 @@
 from sat.core.i18n import _
 import urwid
 from urwid_satext import sat_widgets
-from sat_frontends.constants import Const as commonConst
+from sat_frontends.quick_frontend.constants import Const as commonConst
 from sat_frontends.primitivus.constants import Const
 
 
@@ -39,7 +39,7 @@
         urwid.connect_signal(self.status, 'click', self.onStatusClick)
 
     def onPresenceClick(self, sender=None):
-        if not self.host.bridge.isConnected(self.host.profile):
+        if not self.host.bridge.isConnected(self.host.current_profile):  # FIXME: manage multi-profiles
             return
         options = [commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE]
         list_widget = sat_widgets.GenericList(options=options, option_type=sat_widgets.ClickableText, on_click=self.onChange)
@@ -47,7 +47,7 @@
         self.host.showPopUp(decorated)
 
     def onStatusClick(self, sender=None):
-        if not self.host.bridge.isConnected(self.host.profile):
+        if not self.host.bridge.isConnected(self.host.current_profile):  # FIXME: manage multi-profiles
             return
         pop_up_widget = sat_widgets.InputDialog(_('Set your status'), _('New status'), default_txt=self.status.get_text(),
                                                 cancel_cb=self.host.removePopUp, ok_cb=self.onChange)
@@ -61,7 +61,8 @@
         elif isinstance(user_data, sat_widgets.AdvancedEdit):
             new = (previous[0], new_value[0])
         if new != previous:
-            self.host.bridge.setPresence(show=new[0], statuses={'default': new[1]}, profile_key=self.host.profile)  #FIXME: manage multilingual statuses
+            for profile in self.host.profiles:  # FIXME: for now all the profiles share the same status
+                self.host.bridge.setPresence(show=new[0], statuses={'default': new[1]}, profile_key=profile)  # FIXME: manage multilingual statuses
             self.setPresenceStatus(new[0], new[1])
         self.host.removePopUp()
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/primitivus/widget.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Primitivus: a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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__)
+import urwid
+from urwid_satext import sat_widgets
+from sat_frontends.primitivus.keys import action_key_map as a_key
+
+
+class PrimitivusWidget(urwid.WidgetWrap):
+    """Base widget for Primitivus"""
+
+    def __init__(self, w, title=''):
+        self._title = title
+        self._title_dynamic = None
+        self._original_widget = w
+        urwid.WidgetWrap.__init__(self, self._getDecoration(w))
+
+    @property
+    def title(self):
+        """Text shown in title bar of the widget"""
+
+        # profiles currently managed by frontend
+        try:
+            all_profiles = self.host.profiles
+        except AttributeError:
+            all_profiles = []
+
+        # profiles managed by the widget
+        try:
+            profiles = self.profiles
+        except AttributeError:
+            try:
+                profiles = [self.profile]
+            except AttributeError:
+                profiles = []
+
+        title_elts = []
+        if self._title:
+            title_elts.append(self._title)
+        if self._title_dynamic:
+            title_elts.append(self._title_dynamic)
+        if len(all_profiles)>1 and profiles:
+            title_elts.append(u'[{}]'.format(u', '.join(profiles)))
+        return sat_widgets.SurroundedText(u' '.join(title_elts))
+
+    @title.setter
+    def title(self, value):
+        self._title = value
+        if self.decorationVisible:
+            self.showDecoration()
+
+    @property
+    def title_dynamic(self):
+        """Dynamic part of title"""
+        return self._title_dynamic
+
+    @title_dynamic.setter
+    def title_dynamic(self, value):
+        self._title_dynamic = value
+        if self.decorationVisible:
+            self.showDecoration()
+
+    @property
+    def decorationVisible(self):
+        """True if the decoration is visible"""
+        return isinstance(self._w, sat_widgets.LabelLine)
+
+
+    def keypress(self, size, key):
+        if key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
+            show = not self.decorationVisible
+            self.showDecoration(show)
+        else:
+            return super(PrimitivusWidget, self).keypress(size, key)
+
+    def _getDecoration(self, widget):
+        return sat_widgets.LabelLine(widget, self.title)
+
+    def showDecoration(self, show=True):
+        """Show/Hide the decoration around the window"""
+        self._w = self._getDecoration(self._original_widget) if show else self._original_widget
+
+    def getMenu(self):
+        raise NotImplementedError
--- a/frontends/src/primitivus/xmlui.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/xmlui.py	Wed Mar 18 10:52:28 2015 +0100
@@ -26,6 +26,7 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat_frontends.primitivus.constants import Const as C
+from sat_frontends.primitivus.widget import PrimitivusWidget
 from sat_frontends.tools import xmlui
 
 
@@ -355,14 +356,14 @@
             return cls
 
 
-class XMLUIPanel(xmlui.XMLUIPanel, urwid.WidgetWrap):
+class XMLUIPanel(xmlui.XMLUIPanel, PrimitivusWidget):
     widget_factory = WidgetFactory()
 
-    def __init__(self, host, parsed_xml, title = None, flags = None):
+    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
         self.widget_factory._xmlui_main = self
         self._dest = None
-        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags)
-        urwid.WidgetWrap.__init__(self, self.main_cont)
+        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags, callback, profile)
+        PrimitivusWidget.__init__(self, self.main_cont, self.xmlui_title)
 
     def constructUI(self, parsed_dom):
         def postTreat():
@@ -412,11 +413,10 @@
             raise ValueError('Invalid show_type [%s]' % show_type)
 
         self._dest = show_type
-        decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or ''))
         if show_type == 'popup':
-            self.host.showPopUp(decorated, valign=valign)
+            self.host.showPopUp(self, valign=valign)
         elif show_type == 'window':
-            self.host.addWindow(decorated)
+            self.host.newWidget(self)
         else:
             assert(False)
         self.host.redraw()
--- a/frontends/src/quick_frontend/constants.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/constants.py	Wed Mar 18 10:52:28 2015 +0100
@@ -17,9 +17,58 @@
 # 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 import constants
+from sat.core import constants
+from sat.core.i18n import _
+from collections import OrderedDict  # only available from python 2.7
 
 
 class Const(constants.Const):
 
-    PRIVATE_PREFIX = "@private@"
+    PRESENCE = OrderedDict([("", _("Online")),
+                            ("chat", _("Free for chat")),
+                            ("away", _("Away from keyboard")),
+                            ("dnd", _("Do not disturb")),
+                            ("xa", _("Extended away"))])
+
+    # from plugin_misc_text_syntaxes
+    SYNTAX_XHTML = "XHTML"
+    SYNTAX_CURRENT = "@CURRENT@"
+    SYNTAX_TEXT = "text"
+
+    # XMLUI
+    SAT_FORM_PREFIX = "SAT_FORM_"
+    SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_"  # used to have unique elements names
+    XMLUI_STATUS_VALIDATED = "validated"
+    XMLUI_STATUS_CANCELLED = constants.Const.XMLUI_DATA_CANCELLED
+
+    # MUC
+    ALL_OCCUPANTS = 1
+    MUC_USER_STATES = {
+        "active": u'✔',
+        "inactive": u'☄',
+        "gone": u'✈',
+        "composing": u'✎',
+        "paused": u"⦷"
+    }
+
+    # Roster
+    CONTACT_GROUPS = 'groups'
+    CONTACT_RESOURCES = 'resources'
+    CONTACT_MAIN_RESOURCE = 'main_resource'
+    CONTACT_SPECIAL = 'special'
+    CONTACT_SPECIAL_GROUP = 'group'  # group chat special entity
+    CONTACT_SPECIAL_ALLOWED = (CONTACT_SPECIAL_GROUP,)  # set of allowed values for special flag
+    CONTACT_DATA_FORBIDDEN = {CONTACT_GROUPS, CONTACT_RESOURCES, CONTACT_MAIN_RESOURCE}  # set of forbidden names for contact data
+
+    # Chats
+    CHAT_ONE2ONE = 'one2one'
+    CHAT_GROUP = 'group'
+
+    # Widgets management
+    # FIXME: should be in quick_frontend.constant, but Libervia doesn't inherit from it
+    WIDGET_NEW = 'NEW'
+    WIDGET_KEEP = 'KEEP'
+    WIDGET_RAISE = 'RAISE'
+    WIDGET_RECREATE = 'RECREATE'
+
+    LISTENERS = {'avatar', 'nick', 'presence', 'profilePlugged', 'disconnect', 'gotMenus', 'menu'}
--- a/frontends/src/quick_frontend/quick_app.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/quick_app.py	Wed Mar 18 10:52:28 2015 +0100
@@ -17,40 +17,228 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from sat.core.i18n import _
-import sys
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat_frontends.tools.jid import JID
-from sat_frontends.bridge.DBus import DBusBridgeFrontend
+
+from sat.core.i18n import _
 from sat.core import exceptions
-from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
-from optparse import OptionParser
+from sat.tools.misc import TriggerManager
+
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_menus
+from sat_frontends.quick_frontend import quick_chat, quick_games
+from sat_frontends.quick_frontend.constants import Const as C
+
+import sys
+from collections import OrderedDict
+
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('')  # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError):  # Error raised is not the same depending on pyjsbuild options
+    unicode = str
+
+
+class ProfileManager(object):
+    """Class managing all data relative to one profile, and plugging in mechanism"""
+    host = None
+    bridge = None
+    cache_keys_to_get = ['avatar']
+
+    def __init__(self, profile):
+        self.profile = profile
+        self.whoami = None
+        self.data = {}
+
+    def __getitem__(self, key):
+        return self.data[key]
+
+    def __setitem__(self, key, value):
+        self.data[key] = value
+
+    def plug(self):
+        """Plug the profile to the host"""
+        # we get the essential params
+        self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile,
+                                   callback=self._plug_profile_jid, errback=self._getParamError)
+
+    def _plug_profile_jid(self, _jid):
+        self.whoami = jid.JID(_jid)
+        self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile,
+                                   callback=self._plug_profile_autoconnect, errback=self._getParamError)
+
+    def _plug_profile_autoconnect(self, value_str):
+        autoconnect = C.bool(value_str)
+        if autoconnect and not self.bridge.isConnected(self.profile):
+            self.host.asyncConnect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect())
+        else:
+            self._plug_profile_afterconnect()
+
+    def _plug_profile_afterconnect(self):
+        # Profile can be connected or not
+        # we get cached data
+        self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues)
+
+    def _plug_profile_failedCachedValues(self, failure):
+        log.error("Couldn't get cached values: {}".format(failure))
+        self._plug_profile_gotCachedValues({})
+
+    def _plug_profile_gotCachedValues(self, cached_values):
+        # TODO: watched plugin
+
+        # add the contact list and its listener
+        contact_list = self.host.addContactList(self.profile)
+        self.host.contact_lists[self.profile] = contact_list
+
+        for entity, data in cached_values.iteritems():
+            for key, value in data.iteritems():
+                contact_list.setCache(jid.JID(entity), key, value)
+
+        if not self.bridge.isConnected(self.profile):
+            self.host.setStatusOnline(False, profile=self.profile)
+        else:
+            self.host.setStatusOnline(True, profile=self.profile)
+
+            contact_list.fill()
+
+            #The waiting subscription requests
+            self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub)
 
-from sat_frontends.quick_frontend.constants import Const as C
+    def _plug_profile_gotWaitingSub(self, waiting_sub):
+        for sub in waiting_sub:
+            self.host.subscribeHandler(waiting_sub[sub], sub, self.profile)
+
+        self.bridge.getRoomsJoined(self.profile, callback=self._plug_profile_gotRoomsJoined)
+
+    def _plug_profile_gotRoomsJoined(self, rooms_args):
+        #Now we open the MUC window where we already are:
+        for room_args in rooms_args:
+            self.host.roomJoinedHandler(*room_args, profile=self.profile)
+
+        self.bridge.getRoomsSubjects(self.profile, callback=self._plug_profile_gotRoomsSubjects)
+
+    def _plug_profile_gotRoomsSubjects(self, subjects_args):
+        for subject_args in subjects_args:
+            self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
+
+        #Presence must be requested after rooms are filled
+        self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences)
+
+
+    def _plug_profile_gotPresences(self, presences):
+        def gotEntityData(data, contact):
+            for key in ('avatar', 'nick'):
+                if key in data:
+                    self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
+
+        for contact in presences:
+            for res in presences[contact]:
+                jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
+                show = presences[contact][res][0]
+                priority = presences[contact][res][1]
+                statuses = presences[contact][res][2]
+                self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
+            self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile, callback=lambda data, contact=contact: gotEntityData(data, contact), errback=lambda failure, contact=contact: log.debug("No cache data for {}".format(contact)))
+
+        #Finaly, we get the waiting confirmation requests
+        self.bridge.getWaitingConf(self.profile, callback=self._plug_profile_gotWaitingConf)
+
+    def _plug_profile_gotWaitingConf(self, waiting_confs):
+        for confirm_id, confirm_type, data in waiting_confs:
+            self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
+
+        # At this point, profile should be fully plugged
+        # and we launch frontend specific method
+        self.host.profilePlugged(self.profile)
+
+    def _getParamError(self, ignore):
+        log.error(_("Can't get profile parameter"))
+
+
+class ProfilesManager(object):
+    """Class managing collection of profiles"""
+
+    def __init__(self):
+        self._profiles = {}
+
+    def __contains__(self, profile):
+        return profile in self._profiles
+
+    def __iter__(self):
+        return self._profiles.iterkeys()
+
+    def __getitem__(self, profile):
+        return self._profiles[profile]
+
+    def __len__(self):
+        return len(self._profiles)
+
+    def plug(self, profile):
+        if profile in self._profiles:
+            raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile))
+        self._profiles[profile] = ProfileManager(profile)
+        self._profiles[profile].plug()
+
+    def unplug(self, profile):
+        if profile not in self._profiles:
+            raise ValueError('The profile [{}] is not plugged'.format(profile))
+
+        # remove the contact list and its listener
+        host = self._profiles[profile].host
+        host.contact_lists[profile].onDelete()
+        del host.contact_lists[profile]
+
+        del self._profiles[profile]
+
+    def chooseOneProfile(self):
+        return self._profiles.keys()[0]
 
 
 class QuickApp(object):
     """This class contain the main methods needed for the frontend"""
 
-    def __init__(self, single_profile=True):
-        self.profiles = {}
-        self.single_profile = single_profile
-        self.check_options()
+    def __init__(self, create_bridge, check_options=None):
+        """Create a frontend application
+
+        @param create_bridge: method to use to create the Bridge
+        @param check_options: method to call to check options (usually command line arguments)
+        """
+        self.menus = quick_menus.QuickMenusManager(self)
+        ProfileManager.host = self
+        self.profiles = ProfilesManager()
+        self.ready_profiles = set() # profiles which are connected and ready
+        self.signals_cache = {} # used to keep signal received between start of plug_profile and when the profile is actualy ready
+        self.contact_lists = {}
+        self.widgets = quick_widgets.QuickWidgetsManager(self)
+        if check_options is not None:
+            self.options = check_options()
+        else:
+            self.options = None
+
+        # widgets
+        self.selected_widget = None # widget currently selected (must be filled by frontend)
+
+        # listeners
+        self._listeners = {} # key: listener type ("avatar", "selected", etc), value: list of callbacks
+
+        # triggers
+        self.trigger = TriggerManager()  # trigger are used to change the default behaviour
 
         ## bridge ##
         try:
-            self.bridge = DBusBridgeFrontend()
+            self.bridge = create_bridge()
         except exceptions.BridgeExceptionNoService:
             print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
             sys.exit(1)
         except exceptions.BridgeInitError:
             print(_(u"Can't init bridge"))
             sys.exit(1)
+        ProfileManager.bridge = self.bridge
         self.registerSignal("connected")
         self.registerSignal("disconnected")
         self.registerSignal("newContact")
-        self.registerSignal("newMessage", self._newMessage)
+        self.registerSignal("newMessage")
         self.registerSignal("newAlert")
         self.registerSignal("presenceUpdate")
         self.registerSignal("subscribe")
@@ -66,40 +254,43 @@
         self.registerSignal("roomUserLeft", iface="plugin")
         self.registerSignal("roomUserChangedNick", iface="plugin")
         self.registerSignal("roomNewSubject", iface="plugin")
-        self.registerSignal("tarotGameStarted", iface="plugin")
-        self.registerSignal("tarotGameNew", iface="plugin")
-        self.registerSignal("tarotGameChooseContrat", iface="plugin")
-        self.registerSignal("tarotGameShowCards", iface="plugin")
-        self.registerSignal("tarotGameYourTurn", iface="plugin")
-        self.registerSignal("tarotGameScore", iface="plugin")
-        self.registerSignal("tarotGameCardsPlayed", iface="plugin")
-        self.registerSignal("tarotGameInvalidCards", iface="plugin")
-        self.registerSignal("quizGameStarted", iface="plugin")
-        self.registerSignal("quizGameNew", iface="plugin")
-        self.registerSignal("quizGameQuestion", iface="plugin")
-        self.registerSignal("quizGamePlayerBuzzed", iface="plugin")
-        self.registerSignal("quizGamePlayerSays", iface="plugin")
-        self.registerSignal("quizGameAnswerResult", iface="plugin")
-        self.registerSignal("quizGameTimerExpired", iface="plugin")
-        self.registerSignal("quizGameTimerRestarted", iface="plugin")
         self.registerSignal("chatStateReceived", iface="plugin")
+        self.registerSignal("personalEvent", iface="plugin")
 
-        self.current_action_ids = set()
-        self.current_action_ids_cb = {}
+        # FIXME: do it dynamically
+        quick_games.Tarot.registerSignals(self)
+        quick_games.Quiz.registerSignals(self)
+        quick_games.Radiocol.registerSignals(self)
+
+        self.current_action_ids = set() # FIXME: to be removed
+        self.current_action_ids_cb = {} # FIXME: to be removed
         self.media_dir = self.bridge.getConfig('', 'media_dir')
 
-    def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
+    @property
+    def current_profile(self):
+        """Profile that a user would expect to use"""
+        try:
+            return self.selected_widget.profile
+        except (TypeError, AttributeError):
+            return self.profiles.chooseOneProfile()
+
+    @property
+    def visible_widgets(self):
+        """widgets currently visible (must be implemented by frontend)"""
+        raise NotImplementedError
+
+    def registerSignal(self, function_name, handler=None, iface="core", with_profile=True):
         """Register a handler for a signal
 
-        @param functionName (str): name of the signal to handle
-        @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (functionName + 'Handler')
+        @param function_name (str): name of the signal to handle
+        @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (function_name + 'Handler')
         @param iface (str): interface of the bridge to use ('core' or 'plugin')
         @param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller
         """
         if handler is None:
-            handler = getattr(self, "%s%s" % (functionName, 'Handler'))
+            handler = getattr(self, "{}{}".format(function_name, 'Handler'))
         if not with_profile:
-            self.bridge.register(functionName, handler, iface)
+            self.bridge.register(function_name, handler, iface)
             return
 
         def signalReceived(*args, **kwargs):
@@ -108,78 +299,97 @@
                 if not args:
                     raise exceptions.ProfileNotSetError
                 profile = args[-1]
-            if profile is not None and not self.check_profile(profile):
-                return  # we ignore signal for profiles we don't manage
+            if profile is not None:
+                if not self.check_profile(profile):
+                    if profile in self.profiles:
+                        # profile is not ready but is in self.profiles, that's mean that it's being connecting and we need to cache the signal
+                        self.signals_cache.setdefault(profile, []).append((function_name, handler, args, kwargs))
+                    return  # we ignore signal for profiles we don't manage
             handler(*args, **kwargs)
-        self.bridge.register(functionName, signalReceived, iface)
+        self.bridge.register(function_name, signalReceived, iface)
+
+    def addListener(self, type_, callback, profiles_filter=None):
+        """Add a listener for an event
+
+        /!\ don't forget to remove listener when not used anymore (e.g. if you delete a widget)
+        @param type_: type of event, can be:
+            - avatar: called when avatar data is updated
+                args: (entity, avatar file, profile)
+            - nick: called when nick data is updated
+                args: (entity, new_nick, profile)
+            - presence: called when a presence is received
+                args: (entity, show, priority, statuses, profile)
+            - menu: called when a menu item is added or removed
+                args: (type_, path, path_i18n, item) were values are:
+                    type_: same as in [sat.core.sat_main.SAT.importMenu]
+                    path: same as in [sat.core.sat_main.SAT.importMenu]
+                    path_i18n: translated path (or None if the item is removed)
+                    item: instance of quick_menus.MenuItemBase or None if the item is removed
+            - gotMenus: called only once when menu are available (no arg)
+        @param callback: method to call on event
+        @param profiles_filter (set[unicode]): if set and not empty, the
+            listener will be callable only by one of the given profiles.
+        """
+        assert type_ in C.LISTENERS
+        self._listeners.setdefault(type_, OrderedDict())[callback] = profiles_filter
+
+    def removeListener(self, type_, callback):
+        """Remove a callback from listeners
+
+        @param type_: same as for [addListener]
+        @param callback: callback to remove
+        """
+        assert type_ in C.LISTENERS
+        self._listeners[type_].pop(callback)
+
+    def callListeners(self, type_, *args, **kwargs):
+        """Call the methods which listen type_ event. If a profiles filter has
+        been register with a listener and profile argument is not None, the
+        listener will be called only if profile is in the profiles filter list.
+
+        @param type_: same as for [addListener]
+        @param *args: arguments sent to callback
+        @param **kwargs: keywords argument, mainly used to pass "profile" when needed
+        """
+        assert type_ in C.LISTENERS
+        try:
+            listeners = self._listeners[type_]
+        except KeyError:
+            pass
+        else:
+            profile = kwargs.get("profile")
+            for listener, profiles_filter in listeners.iteritems():
+                if profile is None or not profiles_filter or profile in profiles_filter:
+                    listener(*args, **kwargs)
 
     def check_profile(self, profile):
-        """Tell if the profile is currently followed by the application"""
-        return profile in self.profiles.keys()
+        """Tell if the profile is currently followed by the application, and ready"""
+        return profile in self.ready_profiles
 
-    def postInit(self):
-        """Must be called after initialization is done, do all automatic task (auto plug profile)"""
-        if self.options.profile:
-            if not self.bridge.getProfileName(self.options.profile):
-                log.error(_("Trying to plug an unknown profile (%s)" % self.options.profile))
-            else:
-                self.plug_profile(self.options.profile)
+    def postInit(self, profile_manager):
+        """Must be called after initialization is done, do all automatic task (auto plug profile)
 
-    def check_options(self):
-        """Check command line options"""
-        usage = _("""
-        %prog [options]
-
-        %prog --help for options list
-        """)
-        parser = OptionParser(usage=usage)
+        @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
+        """
+        if self.options and self.options.profile:
+            profile_manager.autoconnect([self.options.profile])
 
-        parser.add_option("-p", "--profile", help=_("Select the profile to use"))
-
-        (self.options, args) = parser.parse_args()
-        if self.options.profile:
-            self.options.profile = self.options.profile.decode('utf-8')
-        return args
-
-    def _getParamError(self, ignore):
-        log.error(_("Can't get profile parameter"))
+    def profilePlugged(self, profile):
+        """Method called when the profile is fully plugged, to launch frontend specific workflow
 
-    def plug_profile(self, profile_key='@DEFAULT@'):
-        """Tell application which profile must be used"""
-        if self.single_profile and self.profiles:
-            log.error(_('There is already one profile plugged (we are in single profile mode) !'))
-            return
-        profile = self.bridge.getProfileName(profile_key)
-        if not profile:
-            log.error(_("The profile asked doesn't exist"))
-            return
-        if profile in self.profiles:
-            log.warning(_("The profile is already plugged"))
-            return
-        self.profiles[profile] = {}
-        if self.single_profile:
-            self.profile = profile # FIXME: must be refactored (multi profiles are not managed correclty)
-        raw_menus = self.bridge.getMenus("", C.NO_SECURITY_LIMIT )
-        menus = self.profiles[profile]['menus'] = {}
-        for raw_menu in raw_menus:
-            id_, type_, path, path_i18n  = raw_menu
-            menus_data = menus.setdefault(type_, [])
-            menus_data.append((id_, path, path_i18n))
-        self.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'plug_profile'}, profile_key=profile)
+        /!\ if you override the method and don't call the parent, be sure to add the profile to ready_profiles !
+            if you don't, all signals will stay in cache
+
+        @param profile(unicode): %(doc_profile)s
+        """
+        self.ready_profiles.add(profile)
 
-    def plug_profile_1(self, profile):
-        ###now we get the essential params###
-        self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
-                                   callback=lambda _jid: self.plug_profile_2(_jid, profile), errback=self._getParamError)
+        # profile is ready, we can call send signals that where is cache
+        cached_signals = self.signals_cache.pop(profile, [])
+        for function_name, handler, args, kwargs in cached_signals:
+            log.debug(u"Calling cached signal [%s] with args %s and kwargs %s" % (function_name, args, kwargs))
 
-    def plug_profile_2(self, _jid, profile):
-        self.profiles[profile]['whoami'] = JID(_jid)
-        self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=profile,
-                                   callback=lambda value: self.plug_profile_3(value == "true", profile), errback=self._getParamError)
-
-    def plug_profile_3(self, autoconnect, profile):
-        self.bridge.asyncGetParamA("Watched", "Misc", profile_key=profile,
-                                   callback=lambda watched: self.plug_profile_4(watched, autoconnect, profile), errback=self._getParamError)
+        self.callListeners('profilePlugged', profile=profile)
 
     def asyncConnect(self, profile, callback=None, errback=None):
         if not callback:
@@ -188,351 +398,215 @@
             def errback(failure):
                 log.error(_(u"Can't connect profile [%s]") % failure)
                 if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
-                    self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile_key=profile)
+                    self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
                 else:
                     self.showDialog(failure.message, failure.fullname, 'error')
         self.bridge.asyncConnect(profile, callback=callback, errback=errback)
 
-    def plug_profile_4(self, watched, autoconnect, profile):
-        if autoconnect and not self.bridge.isConnected(profile):
-            #Does the user want autoconnection ?
-            self.asyncConnect(profile, callback=lambda dummy: self.plug_profile_5(watched, autoconnect, profile))
-        else:
-            self.plug_profile_5(watched, autoconnect, profile)
-
-    def plug_profile_5(self, watched, autoconnect, profile):
-        self.profiles[profile]['watched'] = watched.split()  # TODO: put this in a plugin
-
-        ## misc ##
-        self.profiles[profile]['onlineContact'] = set()  # FIXME: temporary
-
-        #TODO: manage multi-profiles here
-        if not self.bridge.isConnected(profile):
-            self.setStatusOnline(False)
-        else:
-            self.setStatusOnline(True)
-
-            ### now we fill the contact list ###
-            for contact in self.bridge.getContacts(profile):
-                self.newContactHandler(*contact, profile=profile)
+    def plug_profiles(self, profiles):
+        """Tell application which profiles must be used
 
-            presences = self.bridge.getPresenceStatuses(profile)
-            for contact in presences:
-                for res in presences[contact]:
-                    jabber_id = ('%s/%s' % (JID(contact).bare, res)) if res else contact
-                    show = presences[contact][res][0]
-                    priority = presences[contact][res][1]
-                    statuses = presences[contact][res][2]
-                    self.presenceUpdateHandler(jabber_id, show, priority, statuses, profile)
-                data = self.bridge.getEntityData(contact, ['avatar', 'nick'], profile)
-                for key in ('avatar', 'nick'):
-                    if key in data:
-                        self.entityDataUpdatedHandler(contact, key, data[key], profile)
+        @param profiles: list of valid profile names
+        """
+        self.plugging_profiles()
+        for profile in profiles:
+            self.profiles.plug(profile)
 
-            #The waiting subscription requests
-            waitingSub = self.bridge.getWaitingSub(profile)
-            for sub in waitingSub:
-                self.subscribeHandler(waitingSub[sub], sub, profile)
+    def plugging_profiles(self):
+        """Method to subclass to manage frontend specific things to do
 
-            #Now we open the MUC window where we already are:
-            for room_args in self.bridge.getRoomsJoined(profile):
-                self.roomJoinedHandler(*room_args, profile=profile)
-
-            for subject_args in self.bridge.getRoomsSubjects(profile):
-                self.roomNewSubjectHandler(*subject_args, profile=profile)
-
-            #Finaly, we get the waiting confirmation requests
-            for confirm_id, confirm_type, data in self.bridge.getWaitingConf(profile):
-                self.askConfirmationHandler(confirm_id, confirm_type, data, profile)
+        will be called when profiles are choosen and are to be plugged soon
+        """
+        pass
 
     def unplug_profile(self, profile):
         """Tell the application to not follow anymore the profile"""
         if not profile in self.profiles:
-            log.warning(_("This profile is not plugged"))
-            return
-        self.profiles.remove(profile)
+            raise ValueError("The profile [{}] is not plugged".format(profile))
+        self.profiles.unplug(profile)
 
     def clear_profile(self):
         self.profiles.clear()
 
+    def addContactList(self, profile):
+        """Method to subclass to add a contact list widget
+
+        will be called on each profile session build
+        @return: a ContactList widget
+        """
+        return NotImplementedError
+
+    def newWidget(self, widget):
+        raise NotImplementedError
+
     def connectedHandler(self, profile):
         """called when the connection is made"""
         log.debug(_("Connected"))
-        self.setStatusOnline(True)
+        self.setStatusOnline(True, profile=profile)
 
     def disconnectedHandler(self, profile):
         """called when the connection is closed"""
         log.debug(_("Disconnected"))
-        self.contact_list.clearContacts()
-        self.setStatusOnline(False)
+        self.contact_lists[profile].clearContacts()
+        self.setStatusOnline(False, profile=profile)
 
     def newContactHandler(self, JabberId, attributes, groups, profile):
-        entity = JID(JabberId)
+        entity = jid.JID(JabberId)
         _groups = list(groups)
-        self.contact_list.replace(entity, _groups, attributes)
+        self.contact_lists[profile].setContact(entity, _groups, attributes, in_roster=True)
 
-    def _newMessage(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
-        """newMessage premanagement: a dirty hack to manage private messages
+    def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
+        from_jid = jid.JID(from_jid_s)
+        to_jid = jid.JID(to_jid_s)
 
-        if a private MUC message is detected, from_jid or to_jid is prefixed and resource is escaped
-        """
-        # FIXME: must be refactored for 0.6
-        from_jid = JID(from_jid_s)
-        to_jid = JID(to_jid_s)
+        if not self.trigger.point("newMessageTrigger", from_jid, msg, type_, to_jid, extra, profile=profile):
+            return
 
-        from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
-        win = to_jid if from_me else from_jid
+        from_me = from_jid.bare == self.profiles[profile].whoami.bare
+        target = to_jid if from_me else from_jid
+
+        chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
+        contact_list = self.contact_lists[profile]
+
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, on_new_widget=None, profile=profile)
 
-        if ((type_ != "groupchat" and self.contact_list.getSpecial(win) == "MUC") and
-            (type_ != C.MESS_TYPE_INFO or (type_ == C.MESS_TYPE_INFO and win.resource))):
-            #we have a private message in a MUC room
-            #XXX: normaly we use bare jid as key, here we need the full jid
-            #     so we cheat by replacing the "/" before the resource by
-            #     a "@", so the jid is invalid,
-            new_jid = escapePrivate(win)
-            if from_me:
-                to_jid = new_jid
-            else:
-                from_jid = new_jid
-            if new_jid not in self.contact_list:
-                self.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
+        self.current_action_ids = set() # FIXME: to be removed
+        self.current_action_ids_cb = {} # FIXME: to be removed
 
-        self.newMessageHandler(from_jid, to_jid, msg, type_, extra, profile)
+        if not from_jid in contact_list and from_jid.bare != self.profiles[profile].whoami.bare:
+            #XXX: needed to show entities which haven't sent any
+            #     presence information and which are not in roster
+            contact_list.setContact(from_jid)
+
+        # we display the message in the widget
+        chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
 
-    def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
-        from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
-        win = to_jid if from_me else from_jid
-
-        self.current_action_ids = set()
-        self.current_action_ids_cb = {}
+        # ContactList alert
+        visible = False
+        for widget in self.visible_widgets:
+            if isinstance(widget, quick_chat.QuickChat) and widget.manageMessage(from_jid, type_):
+                visible = True
+                break
+        if not visible:
+            contact_list.setAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
 
-        timestamp = extra.get('archive')
-        if type_ == C.MESS_TYPE_INFO:
-            self.chat_wins[win.bare].printInfo(msg, timestamp=float(timestamp) if timestamp else None)
-        else:
-            self.chat_wins[win.bare].printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
-
-    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key="@NONE@"):
-        if to_jid.startswith(C.PRIVATE_PREFIX):
-            to_jid = unescapePrivate(to_jid)
-            mess_type = "chat"
+    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
         if callback is None:
-            callback = lambda: None
+            callback = lambda dummy=None: None # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy
         if errback is None:
             errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error")
-        self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
+
+        if not self.trigger.point("sendMessageTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key):
+            return
+
+        self.bridge.sendMessage(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
 
     def newAlertHandler(self, msg, title, alert_type, profile):
         assert alert_type in ['INFO', 'ERROR']
         self.showDialog(unicode(msg), unicode(title), alert_type.lower())
 
-    def setStatusOnline(self, online=True, show="", statuses={}):
+    def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
         raise NotImplementedError
 
-    def presenceUpdateHandler(self, jabber_id, show, priority, statuses, profile):
+    def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
 
-        log.debug(_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
-              % {'jid': jabber_id, 'show': show, 'priority': priority, 'statuses': statuses, 'profile': profile})
-        from_jid = JID(jabber_id)
+        log.debug(_("presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
+              % {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile})
+        entity = jid.JID(entity_s)
 
-        if from_jid == self.profiles[profile]['whoami']:
+        if entity == self.profiles[profile].whoami:
             if show == "unavailable":
-                self.setStatusOnline(False)
+                self.setStatusOnline(False, profile=profile)
             else:
-                self.setStatusOnline(True, show, statuses)
+                self.setStatusOnline(True, show, statuses, profile=profile)
             return
 
-        presences = self.profiles[profile].setdefault('presences', {})
-
-        if show != 'unavailable':
-
-            #FIXME: must be moved in a plugin
-            if from_jid.bare in self.profiles[profile].get('watched',[]) and not from_jid.bare in self.profiles[profile]['onlineContact']:
-                self.showAlert(_("Watched jid [%s] is connected !") % from_jid.bare)
+        # #FIXME: must be moved in a plugin
+        # if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']:
+        #     self.showAlert(_("Watched jid [%s] is connected !") % entity.bare)
 
-            presences[jabber_id] = {'show': show, 'priority': priority, 'statuses': statuses}
-            self.profiles[profile].setdefault('onlineContact',set()).add(from_jid)  # FIXME onlineContact is useless with CM, must be removed
-
-            #TODO: vcard data (avatar)
-
-        if show == "unavailable" and from_jid in self.profiles[profile].get('onlineContact',set()):
-            try:
-                del presences[jabber_id]
-            except KeyError:
-                pass
-            self.profiles[profile]['onlineContact'].remove(from_jid)
+        self.callListeners('presence', entity, show, priority, statuses, profile=profile)
 
-        # check if the contact is connected with another resource, use the one with highest priority
-        jids = [jid for jid in presences if JID(jid).bare == from_jid.bare]
-        if jids:
-            max_jid = max(jids, key=lambda jid: presences[jid]['priority'])
-            data = presences[max_jid]
-            max_priority = data['priority']
-            if show == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
-                from_jid = JID(max_jid)
-                show, priority, statuses = data['show'], data['priority'], data['statuses']
-        if not jids or priority >= max_priority:
-            # case 1: not jids means all resources are disconnected, send the 'unavailable' presence
-            # case 2: update (or confirm) with the values of the resource which takes precedence
-            self.contact_list.updatePresence(from_jid, show, priority, statuses)
-
-    def roomJoinedHandler(self, room_jid, room_nicks, user_nick, profile):
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
         """Called when a MUC room is joined"""
-        log.debug(_("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s") % {'room_jid': room_jid, 'profile': profile, 'users': room_nicks})
-        self.chat_wins[room_jid].setUserNick(user_nick)
-        self.chat_wins[room_jid].setType("group")
-        self.chat_wins[room_jid].id = room_jid
-        self.chat_wins[room_jid].setPresents(list(set([user_nick] + room_nicks)))
-        self.contact_list.setSpecial(JID(room_jid), "MUC", show=True)
+        log.debug("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.setUserNick(user_nick)
+        chat_widget.id = room_jid  # FIXME: to be removed
+        room_nicks = [unicode(nick) for nick in room_nicks]  # FIXME: should be done in DBus bridge / is that still needed?!
+        nicks = list(set([user_nick] + room_nicks))
+        nicks.sort()
+        chat_widget.setPresents(nicks)
+        self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
 
     def roomLeftHandler(self, room_jid_s, profile):
         """Called when a MUC room is left"""
-        log.debug(_("Room [%(room_jid)s] left by %(profile)s") % {'room_jid': room_jid_s, 'profile': profile})
-        del self.chat_wins[room_jid_s]
-        self.contact_list.remove(JID(room_jid_s))
-
-    def roomUserJoinedHandler(self, room_jid, user_nick, user_data, profile):
-        """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].replaceUser(user_nick)
-            log.debug(_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
-
-    def roomUserLeftHandler(self, room_jid, user_nick, user_data, profile):
-        """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].removeUser(user_nick)
-            log.debug(_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
-
-    def roomUserChangedNickHandler(self, room_jid, old_nick, new_nick, profile):
-        """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].changeUserNick(old_nick, new_nick)
-            log.debug(_("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]") % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
+        log.debug("Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getWidget(quick_chat.QuickChat, room_jid, profile)
+        if chat_widget:
+            self.widgets.deleteWidget(chat_widget)
+        self.contact_lists[profile].remove(room_jid)
 
-    def roomNewSubjectHandler(self, room_jid, subject, profile):
-        """Called when subject of MUC room change"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].setSubject(subject)
-            log.debug(_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid': room_jid, "subject": subject})
-
-    def tarotGameStartedHandler(self, room_jid, referee, players, profile):
-        log.debug(_("Tarot Game Started \o/"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].startGame("Tarot", referee, players)
-            log.debug(_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
-
-    def tarotGameNewHandler(self, room_jid, hand, profile):
-        log.debug(_("New Tarot Game"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").newGame(hand)
-
-    def tarotGameChooseContratHandler(self, room_jid, xml_data, profile):
-        """Called when the player has to select his contrat"""
-        log.debug(_("Tarot: need to select a contrat"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").chooseContrat(xml_data)
-
-    def tarotGameShowCardsHandler(self, room_jid, game_stage, cards, data, profile):
-        log.debug(_("Show cards"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").showCards(game_stage, cards, data)
-
-    def tarotGameYourTurnHandler(self, room_jid, profile):
-        log.debug(_("My turn to play"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").myTurn()
+    def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
+        """Called when an user joined a MUC room"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.replaceUser(user_nick)
+        log.debug("user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
 
-    def tarotGameScoreHandler(self, room_jid, xml_data, winners, loosers, profile):
-        """Called when the game is finished and the score are updated"""
-        log.debug(_("Tarot: score received"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").showScores(xml_data, winners, loosers)
-
-    def tarotGameCardsPlayedHandler(self, room_jid, player, cards, profile):
-        log.debug(_("Card(s) played (%(player)s): %(cards)s") % {"player": player, "cards": cards})
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").cardsPlayed(player, cards)
-
-    def tarotGameInvalidCardsHandler(self, room_jid, phase, played_cards, invalid_cards, profile):
-        log.debug(_("Cards played are not valid: %s") % invalid_cards)
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").invalidCards(phase, played_cards, invalid_cards)
-
-    def quizGameStartedHandler(self, room_jid, referee, players, profile):
-        log.debug(_("Quiz Game Started \o/"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].startGame("Quiz", referee, players)
-            log.debug(_("new Quiz game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
-
-    def quizGameNewHandler(self, room_jid, data, profile):
-        log.debug(_("New Quiz Game"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameNewHandler(data)
+    def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
+        """Called when an user joined a MUC room"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.removeUser(user_nick)
+        log.debug("user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
 
-    def quizGameQuestionHandler(self, room_jid, question_id, question, timer, profile):
-        """Called when a new question is asked"""
-        log.debug(_(u"Quiz: new question: %s") % question)
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameQuestionHandler(question_id, question, timer)
-
-    def quizGamePlayerBuzzedHandler(self, room_jid, player, pause, profile):
-        """Called when a player pushed the buzzer"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGamePlayerBuzzedHandler(player, pause)
+    def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
+        """Called when an user joined a MUC room"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.changeUserNick(old_nick, new_nick)
+        log.debug("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
 
-    def quizGamePlayerSaysHandler(self, room_jid, player, text, delay, profile):
-        """Called when a player say something"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGamePlayerSaysHandler(player, text, delay)
-
-    def quizGameAnswerResultHandler(self, room_jid, player, good_answer, score, profile):
-        """Called when a player say something"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameAnswerResultHandler(player, good_answer, score)
-
-    def quizGameTimerExpiredHandler(self, room_jid, profile):
-        """Called when nobody answered the question in time"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameTimerExpiredHandler()
-
-    def quizGameTimerRestartedHandler(self, room_jid, time_left, profile):
-        """Called when the question is not answered, and we still have time"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestartedHandler(time_left)
+    def roomNewSubjectHandler(self, room_jid_s, subject, profile):
+        """Called when subject of MUC room change"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.setSubject(subject)
+        log.debug("new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject})
 
     def chatStateReceivedHandler(self, from_jid_s, state, profile):
-        """Callback when a new chat state is received.
+        """Called when a new chat state is received.
+
         @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
         @param state: new state (string)
         @profile: current profile
         """
+        from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
+        for widget in self.widgets.getWidgets(quick_chat.QuickChat):
+            if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
+                widget.updateChatState(from_jid, state)
 
-        if from_jid_s == '@ALL@':
-            target = '@ALL@'
-            nick = C.ALL_OCCUPANTS
-        else:
-            from_jid = JID(from_jid_s)
-            target = from_jid.bare
-            nick = from_jid.resource
+    def personalEventHandler(self, sender, event_type, data):
+        """Called when a PEP event is received.
 
-        for bare in self.chat_wins.keys():
-            if target == '@ALL' or target == bare:
-                chat_win = self.chat_wins[bare]
-                if chat_win.type == 'one2one':
-                    chat_win.updateChatState(state)
-                elif chat_win.type == 'group':
-                    chat_win.updateChatState(state, nick=nick)
+        @param sender (jid.JID): event sender
+        @param event_type (unicode): event type, e.g. 'MICROBLOG' or 'MICROBLOG_DELETE'
+        @param data (dict): event data
+        """
+        # FIXME move some code from Libervia to here and put the magic strings to constants
+        pass
 
     def _subscribe_cb(self, answer, data):
         entity, profile = data
-        if answer:
-            self.bridge.subscription("subscribed", entity.bare, profile_key=profile)
-        else:
-            self.bridge.subscription("unsubscribed", entity.bare, profile_key=profile)
+        type_ = "subscribed" if answer else "unsubscribed"
+        self.bridge.subscription(type_, unicode(entity.bare), profile_key=profile)
 
     def subscribeHandler(self, type, raw_jid, profile):
         """Called when a subsciption management signal is received"""
-        entity = JID(raw_jid)
+        entity = jid.JID(raw_jid)
         if type == "subscribed":
             # this is a subscription confirmation, we just have to inform user
             self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation'))
@@ -543,7 +617,7 @@
             # this is a subscriptionn request, we have to ask for user confirmation
             self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.bare, _('Subscription confirmation'), 'yes/no', answer_cb=self._subscribe_cb, answer_data=(entity, profile))
 
-    def showDialog(self, message, title, type="info", answer_cb=None):
+    def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None):
         raise NotImplementedError
 
     def showAlert(self, message):
@@ -553,33 +627,30 @@
         log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
         if (namespace, name) == ("Connection", "JabberID"):
             log.debug(_("Changing JID to %s") % value)
-            self.profiles[profile]['whoami'] = JID(value)
+            self.profiles[profile].whoami = jid.JID(value)
         elif (namespace, name) == ("Misc", "Watched"):
             self.profiles[profile]['watched'] = value.split()
         elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS):
-            self.contact_list.showOfflineContacts(C.bool(value))
+            self.contact_lists[profile].showOfflineContacts(C.bool(value))
         elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS):
-            self.contact_list.showEmptyGroups(C.bool(value))
+            self.contact_lists[profile].showEmptyGroups(C.bool(value))
 
-    def contactDeletedHandler(self, jid, profile):
-        target = JID(jid)
-        self.contact_list.remove(target)
-        try:
-            self.profiles[profile]['onlineContact'].remove(target.bare)
-        except KeyError:
-            pass
+    def contactDeletedHandler(self, jid_s, profile):
+        target = jid.JID(jid_s)
+        self.contact_lists[profile].remove(target)
 
-    def entityDataUpdatedHandler(self, jid_str, key, value, profile):
-        jid = JID(jid_str)
+    def entityDataUpdatedHandler(self, entity_s, key, value, profile):
+        entity = jid.JID(entity_s)
         if key == "nick":
-            if jid in self.contact_list:
-                self.contact_list.setCache(jid, 'nick', value)
-                self.contact_list.replace(jid)
+            if entity in self.contact_lists[profile]:
+                self.contact_lists[profile].setCache(entity, 'nick', value)
+                self.callListeners('nick', entity, value, profile=profile)
         elif key == "avatar":
-            if jid in self.contact_list:
-                filename = self.bridge.getAvatarFile(value)
-                self.contact_list.setCache(jid, 'avatar', filename)
-                self.contact_list.replace(jid)
+            if entity in self.contact_lists[profile]:
+                def gotFilename(filename):
+                    self.contact_lists[profile].setCache(entity, 'avatar', filename)
+                    self.callListeners('avatar', entity, filename, profile=profile)
+                self.bridge.getAvatarFile(value, callback=gotFilename)
 
     def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
         raise NotImplementedError
@@ -587,22 +658,32 @@
     def actionResultHandler(self, type, id, data, profile):
         raise NotImplementedError
 
-    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
-        """ Launch a dynamic action
+    def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
+        """Launch a dynamic action
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
-        @param profile_key: %(doc_profile_key)s
+        @param callback: if not None and 'validated' key is present, it will be called with the following parameters:
+            - callback_id
+            - data
+            - profile_key
+        @param profile_key: %(doc_profile)s
 
         """
         raise NotImplementedError
 
+    def disconnect(self, profile):
+        log.info("disconnecting")
+        self.callListeners('disconnect', profile=profile)
+        self.bridge.disconnect(profile)
+
     def onExit(self):
         """Must be called when the frontend is terminating"""
-        #TODO: mange multi-profile here
-        try:
-            if self.bridge.isConnected(self.profile):
-                if self.bridge.getParamA("autodisconnect", "Connection", profile_key=self.profile) == "true":
+        to_unplug = []
+        for profile in self.profiles:
+            if self.bridge.isConnected(profile):
+                if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)):
                     #The user wants autodisconnection
-                    self.bridge.disconnect(self.profile)
-        except:
-            pass
+                    self.disconnect(profile)
+            to_unplug.append(profile)
+        for profile in to_unplug:
+            self.unplug_profile(profile)
--- a/frontends/src/quick_frontend/quick_card_game.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,164 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# helper class for making a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.jid  import JID
-
-
-
-class QuickCardGame(object):
-
-    def __init__(self, parent, referee, players, player_nick):
-        self._autoplay = None #XXX: use 0 to activate fake play, None else
-        self.parent = parent
-        self.referee = referee
-        self.players = players
-        self.played = {}
-        for player in players:
-            self.played[player] = None
-        self.player_nick = player_nick
-        self.bottom_nick = unicode(self.player_nick)
-        idx = self.players.index(self.player_nick)
-        idx = (idx + 1) % len(self.players)
-        self.right_nick = unicode(self.players[idx])
-        idx = (idx + 1) % len(self.players)
-        self.top_nick = unicode(self.players[idx])
-        idx = (idx + 1) % len(self.players)
-        self.left_nick = unicode(self.players[idx])
-        self.bottom_nick = unicode(player_nick)
-        self.selected = [] #Card choosed by the player (e.g. during ecart)
-        self.hand_size = 13 #number of cards in a hand
-        self.hand = []
-        self.to_show = []
-        self.state = None
-
-    def resetRound(self):
-        """Reset the game's variables to be reatty to start the next round"""
-        del self.selected[:]
-        del self.hand[:]
-        del self.to_show[:]
-        self.state = None
-        for pl in self.played:
-            self.played[pl] = None
-
-    def getPlayerLocation(self, nick):
-        """return player location (top,bottom,left or right)"""
-        for location in ['top','left','bottom','right']:
-            if getattr(self,'%s_nick' % location) == nick:
-                return location
-        assert(False)
-
-    def loadCards(self):
-        """Load all the cards in memory
-        @param dir: directory where the PNG files are"""
-        self.cards={}
-        self.deck=[]
-        self.cards["atout"]={} #As Tarot is a french game, it's more handy & logical to keep french names
-        self.cards["pique"]={} #spade
-        self.cards["coeur"]={} #heart
-        self.cards["carreau"]={} #diamond
-        self.cards["trefle"]={} #club
-
-    def newGame(self, hand):
-        """Start a new game, with given hand"""
-        assert (len(self.hand) == 0)
-        for suit, value in hand:
-            self.hand.append(self.cards[suit, value])
-        self.hand.sort()
-        self.state = "init"
-
-    def contratSelected(self, contrat):
-        """Called when the contrat has been choosed
-        @param data: form result"""
-        self.parent.host.bridge.tarotGameContratChoosed(self.player_nick, self.referee, contrat or 'Passe', self.parent.host.profile)
-
-    def chooseContrat(self, xml_data):
-        """Called when the player as to select his contrat
-        @param xml_data: SàT xml representation of the form"""
-        raise NotImplementedError
-
-    def showCards(self, game_stage, cards, data):
-        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
-        self.to_show = []
-        for suit, value in cards:
-            self.to_show.append(self.cards[suit, value])
-        if game_stage == "chien" and data['attaquant'] == self.player_nick:
-            self.state = "wait_for_ecart"
-        else:
-            self.state = "chien"
-
-    def myTurn(self):
-        """Called when we have to play :)"""
-        if self.state == "chien":
-            self.to_show = []
-        self.state = "play"
-        self.__fakePlay()
-
-    def __fakePlay(self):
-        """Convenience method for stupid autoplay
-        /!\ don't forgot to comment any interactive dialog for invalid card"""
-        if self._autoplay == None:
-            return
-        if self._autoplay >= len(self.hand):
-            self._autoplay = 0
-        card = self.hand[self._autoplay]
-        self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], self.parent.host.profile)
-        del self.hand[self._autoplay]
-        self.state = "wait"
-        self._autoplay+=1
-
-    def showScores(self, xml_data, winners, loosers):
-        """Called at the end of a game
-        @param xml_data: SàT xml representation of the scores
-        @param winners: list of winners' nicks
-        @param loosers: list of loosers' nicks"""
-        raise NotImplementedError
-
-    def cardsPlayed(self, player, cards):
-        """A card has been played by player"""
-        if self.to_show:
-            self.to_show = []
-        pl_cards = []
-        if self.played[player] != None: #FIXME
-            for pl in self.played:
-                self.played[pl] = None
-        for suit, value in cards:
-            pl_cards.append(self.cards[suit, value])
-        self.played[player] = pl_cards[0]
-
-    def invalidCards(self, phase, played_cards, invalid_cards):
-        """Invalid cards have been played
-        @param phase: phase of the game
-        @param played_cards: all the cards played
-        @param invalid_cards: cards which are invalid"""
-
-        if phase == "play":
-            self.state = "play"
-        elif phase == "ecart":
-            self.state = "ecart"
-        else:
-            log.error ('INTERNAL ERROR: unmanaged game phase')
-
-        for suit, value in played_cards:
-            self.hand.append(self.cards[suit, value])
-
-        self.hand.sort()
-        self.__fakePlay()
-
--- a/frontends/src/quick_frontend/quick_chat.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/quick_chat.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,41 +20,92 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat_frontends.tools.jid  import JID
-from sat_frontends.quick_frontend.quick_utils import unescapePrivate
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.quick_frontend.constants import Const as C
 
 
-class QuickChat(object):
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('') # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
+    unicode = lambda x: str(x)
+
+
+class QuickChat(quick_widgets.QuickWidget):
 
-    def __init__(self, target, host, type_='one2one'):
-        self.target = target
-        self.host = host
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+        """
+        @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
+        """
+        quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
+        assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
+        if type_ == C.CHAT_GROUP and target.resource:
+            raise ValueError("A group chat entity can't have a resource")
+        self.current_target = target
         self.type = type_
-        self.id = ""
+        self.id = "" # FIXME: to be removed
         self.nick = None
         self.occupants = set()
+        self.games = {}
 
-    def setType(self, type_):
-        """Set the type of the chat
-        @param type: can be 'one2one' for single conversation or 'group' for chat à la IRC
+    def __str__(self):
+        return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
+
+    @staticmethod
+    def getWidgetHash(target, profile):
+        return (unicode(profile), target.bare)
+
+    @staticmethod
+    def getPrivateHash(target, profile):
+        """Get unique hash for private conversations
+
+        This method should be used with force_hash to get unique widget for private MUC conversations
         """
-        self.type = type_
+        return (unicode(profile), target)
+
+
+    def addTarget(self, target):
+        super(QuickChat, self).addTarget(target)
+        if target.resource:
+            self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead
+
+    @property
+    def target(self):
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    def manageMessage(self, entity, mess_type):
+        """Tell if this chat widget manage an entity and message type couple
+
+        @param entity (jid.JID): (full) jid of the sending entity
+        @param mess_type (str): message type as given by newMessage
+        @return (bool): True if this Chat Widget manage this couple
+        """
+        if self.type == C.CHAT_GROUP:
+            if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
+                return True
+        else:
+            if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
+                return True
+        return False
 
     def setPresents(self, nicks):
-        """Set the users presents in the contact list for a group chat
-        @param nicks: list of nicknames
+        """Set the occupants of a group chat.
+
+        @param nicks (list[unicode]): sorted list of nicknames
         """
-        log.debug (_("Adding users %s to room") % nicks)
-        if self.type != "group":
-            log.error (_("[INTERNAL] trying to set presents nicks for a non group chat window"))
-            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
+        log.debug(_("Adding users %s to room") % nicks)
+        if self.type != C.CHAT_GROUP:
+            log.error(_("[INTERNAL] trying to set presents nicks for a non group chat window"))
+            raise Exception("INTERNAL ERROR")  # TODO: raise proper Exception here
         self.occupants.update(nicks)
 
     def replaceUser(self, nick, show_info=True):
         """Add user if it is not in the group list"""
         log.debug (_("Replacing user %s") % nick)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to replace user for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         len_before = len(self.occupants)
@@ -65,7 +116,7 @@
     def removeUser(self, nick, show_info=True):
         """Remove a user from the group list"""
         log.debug(_("Removing user %s") % nick)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to remove user for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         self.occupants.remove(nick)
@@ -82,7 +133,7 @@
     def changeUserNick(self, old_nick, new_nick):
         """Change nick of a user in group list"""
         log.debug(_("Changing nick of user %(old_nick)s to %(new_nick)s") % {"old_nick": old_nick, "new_nick": new_nick})
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to change user nick for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         self.removeUser(old_nick, show_info=False)
@@ -92,7 +143,7 @@
     def setSubject(self, subject):
         """Set title for a group chat"""
         log.debug(_("Setting subject to %s") % subject)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to set subject for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
 
@@ -106,71 +157,97 @@
         @param search (str): pattern to filter the history results
         @param profile (str): %(doc_profile)s
         """
-        log.debug(_("now we print the history (%d messages)") % size)
+        log_msg = _(u"now we print the history")
+        if size != C.HISTORY_LIMIT_DEFAULT:
+            log_msg += _(u" (%d messages)" % size)
+        log.debug(log_msg)
 
         def onHistory(history):
             for line in history:
-                timestamp, from_jid, to_jid, message, _type, extra = line
-                if ((self.type == 'group' and _type != 'groupchat') or
-                   (self.type == 'one2one' and _type == 'groupchat')):
+                timestamp, from_jid, to_jid, message, type_, extra = line # FIXME: extra is unused !
+                if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
+                   (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
                     continue
-                self.printMessage(JID(from_jid), message, profile, timestamp)
+                self.printMessage(jid.JID(from_jid), message, {'timestamp':timestamp}, profile)
             self.afterHistoryPrint()
 
         def onHistoryError(err):
             log.error(_("Can't get history"))
 
-        if self.target.startswith(C.PRIVATE_PREFIX):
-            target = unescapePrivate(self.target)
-        else:
-            target = self.target.bare
+        target = self.target.bare
+
+        self.host.bridge.getHistory(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=onHistory, errback=onHistoryError)
 
-        return self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)
+    def _get_nick(self, entity):
+        """Return nick of this entity when possible"""
+        if self.type == C.CHAT_GROUP:
+            return entity.resource
+        contact_list = self.host.contact_lists[self.profile]
+        if entity.bare in contact_list:
+            return contact_list.getCache(entity,'nick') or contact_list.getCache(entity,'name') or entity.node or entity
+        return entity.node or entity
+
+    def onPrivateCreated(self, widget):
+        """Method called when a new widget for private conversation (MUC) is created"""
+        raise NotImplementedError
+
+    def getOrCreatePrivateWidget(self, entity):
+        """Create a widget for private conversation, or get it if it already exists
 
-    def _get_nick(self, jid):
-        """Return nick of this jid when possible"""
-        if self.target.startswith(C.PRIVATE_PREFIX):
-            unescaped = unescapePrivate(self.target)
-            if jid.startswith(C.PRIVATE_PREFIX) or unescaped.bare == jid.bare:
-                return unescaped.resource
-        return jid.resource if self.type == "group" else (self.host.contact_list.getCache(jid,'nick') or self.host.contact_list.getCache(jid,'name') or jid.node)
+        @param entity: full jid of the target
+        """
+        return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again
 
-    def printMessage(self, from_jid, msg, profile, timestamp=None):
+    def newMessage(self, from_jid, target, msg, type_, extra, profile):
+        if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
+            # we have a private message, we forward it to a private conversation widget
+            chat_widget = self.getOrCreatePrivateWidget(target)
+            chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
+        else:
+            if type_ == C.MESS_TYPE_INFO:
+                self.printInfo(msg, extra=extra)
+            else:
+                self.printMessage(from_jid, msg, extra, profile)
+
+    def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
         """Print message in chat window. Must be implemented by child class"""
-        jid = JID(from_jid)
-        nick = self._get_nick(jid)
-        mymess = (jid.resource == self.nick) if self.type == "group" else (jid.bare == self.host.profiles[profile]['whoami'].bare) #mymess = True if message comes from local user
+        nick = self._get_nick(from_jid)
+        mymess = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user
         if msg.startswith('/me '):
-            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', timestamp=timestamp)
+            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', extra=extra)
             return
-        return jid, nick, mymess
+        return nick, mymess
 
-    def printInfo(self, msg, type_='normal', timestamp=None):
+    def printInfo(self, msg, type_='normal', extra=None):
         """Print general info
         @param msg: message to print
         @type_: one of:
             normal: general info like "toto has joined the room"
             me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
-        @param timestamp (float): number of seconds since epoch
+        @param extra (dict): message data
+        """
+        raise NotImplementedError
+
+    def updateChatState(self, from_jid, state):
+        """Set the chat state (XEP-0085) of the contact.
+
+        @param state: the new chat state
         """
         raise NotImplementedError
 
-    def startGame(self, game_type, referee, players):
-        """Configure the chat window to start a game"""
-        #No need to raise an error as game are not mandatory
-        log.warning(_('startGame is not implemented in this frontend'))
+    def addGamePanel(self, widget):
+        """Insert a game panel to this Chat dialog.
 
-    def getGame(self, game_type):
-        """Return class managing the game type"""
-        #No need to raise an error as game are not mandatory
-        log.warning(_('getGame is not implemented in this frontend'))
-
-    def updateChatState(self, state, nick=None):
-        """Set the chat state (XEP-0085) of the contact. Leave nick to None
-        to set the state for a one2one conversation, or give a nickname or
-        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
-        @param state: the new chat state
-        @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS
+        @param widget (Widget): the game panel
         """
         raise NotImplementedError
 
+    def removeGamePanel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        raise NotImplementedError
+
+
+quick_widgets.register(QuickChat)
--- a/frontends/src/quick_frontend/quick_chat_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# helper class for making a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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.tools.jid  import JID
-
-
-class QuickChatList(dict):
-    """This class is used to manage the list of chat windows.
-    It act as a dict, but create a chat window when the name is found for the first time."""
-
-    def __init__(self, host):
-        dict.__init__(self)
-        self.host = host
-
-    def __getitem__(self, to_jid):
-        target = JID(to_jid)
-        if not target.bare in self:
-            #we have to create the chat win
-            self[target.bare] = self.createChat(target)
-        return dict.__getitem__(self, target.bare)
-
-    def createChat(self, target):
-        raise NotImplementedError
--- a/frontends/src/quick_frontend/quick_contact_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/quick_contact_list.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,41 +20,237 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
+from sat_frontends.quick_frontend.quick_widgets import QuickWidget
+from sat_frontends.quick_frontend.constants import Const as C
+from sat_frontends.tools import jid
 
 
-class QuickContactList(object):
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('') # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
+    # XXX: pyjamas' max doesn't support key argument, so we implement it ourself
+    pyjamas_max = max
+    def max(iterable, key):
+        iter_cpy = list(iterable)
+        iter_cpy.sort(key=key)
+        return pyjamas_max(iter_cpy)
+
+
+class QuickContactList(QuickWidget):
     """This class manage the visual representation of contacts"""
 
-    def __init__(self):
+    def __init__(self, host, profile):
         log.debug(_("Contact List init"))
+        super(QuickContactList, self).__init__(host, profile, profile)
+        # bare jids as keys, resources are used in data
         self._cache = {}
-        self.specials={}
+
+        # special entities (groupchat, gateways, etc), bare jids
+        self._specials = set()
+        # extras are specials with full jids (e.g.: private MUC conversation)
+        self._special_extras = set()
+
+        # group data contain jids in groups and misc frontend data
+        self._groups = {} # groups to group data map
+
+        # contacts in roster (bare jids)
+        self._roster = set()
+
+        # entities with an alert (usually a waiting message), full jid
+        self._alerts = set()
+
+        # selected entities, full jid
+        self._selected = set()
+
+        # we keep our own jid
+        self.whoami = host.profiles[profile].whoami
+
+        # options
+        self.show_disconnected = False
+        self.show_empty_groups = True
+        self.show_resources = False
+        self.show_status = False
+        # TODO: this may lead to two successive UI refresh and needs an optimization
+        self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups)
+        self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts)
+
+        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+        self.presenceListener = self.onPresenceUpdate
+        self.host.addListener('presence', self.presenceListener, [profile])
+        self.nickListener = self.onNickUpdate
+        self.host.addListener('nick', self.nickListener, [profile])
+
+    def __contains__(self, entity):
+        """Check if entity is in contact list
+
+        @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
+        """
+        if entity.resource:
+            try:
+                return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
+            except KeyError:
+                return False
+        return entity in self._cache
 
-    def update_jid(self, jid):
-        """Update the jid in the list when something changed"""
+    @property
+    def roster_entities(self):
+        """Return all the bare JIDs of the roster entities.
+
+        @return: set(jid.JID)
+        """
+        return self._roster
+
+    @property
+    def roster_entities_connected(self):
+        """Return all the bare JIDs of the roster entities that are connected.
+
+        @return: set(jid.JID)
+        """
+        return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None])
+
+    @property
+    def roster_entities_by_group(self):
+        """Return a dictionary binding the roster groups to their entities bare
+        JIDs. This also includes the empty group (None key).
+
+        @return: dict{unicode: set(jid.JID)}
+        """
+        return {group: self._groups[group]['jids'] for group in self._groups}
+
+    @property
+    def roster_groups_by_entity(self):
+        """Return a dictionary binding the entities bare JIDs to their roster
+        groups. The empty group is filtered out.
+
+        @return: dict{jid.JID: set(unicode)}
+        """
+        result = {}
+        for group, data in self._groups.iteritems():
+            if group is None:
+                continue
+            for entity in data['jids']:
+                result.setdefault(entity, set()).add(group)
+        return result
+
+    def fill(self):
+        """Get all contacts from backend, and fill the widget"""
+        def gotContacts(contacts):
+            for contact in contacts:
+                self.host.newContactHandler(*contact, profile=self.profile)
+
+        self.host.bridge.getContacts(self.profile, callback=gotContacts)
+
+    def update(self):
+        """Update the display when something changed"""
         raise NotImplementedError
 
-    def getCache(self, jid, name):
+    def getCache(self, entity, name=None):
+        """Return a cache value for a contact
+
+        @param entity(entity.entity): entity of the contact from who we want data (resource is used if given)
+            if a resource specific information is requested:
+                - if no resource is given (bare jid), the main resource is used, according to priority
+                - if resource is given, it is used
+        @param name(unicode): name the data to get, or None to get everything
+        @return: full cache if no name is given, or value of "name", or None
+        """
         try:
-            jid_cache = self._cache[jid.bare]
-            if name == 'status': #XXX: we get the first status for 'status' key
-                return jid_cache['statuses'].get('default','')
-            return jid_cache[name]
-        except (KeyError, IndexError):
+            cache = self._cache[entity.bare]
+        except KeyError:
+            self.setContact(entity)
+            cache = self._cache[entity.bare]
+
+        if name is None:
+            return cache
+        try:
+            if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
+                # these data are related to the resource
+                if not entity.resource:
+                    main_resource = cache[C.CONTACT_MAIN_RESOURCE]
+                    cache = cache[C.CONTACT_RESOURCES][main_resource]
+                else:
+                    cache = cache[C.CONTACT_RESOURCES][entity.resource]
+
+                if name == 'status': #XXX: we get the first status for 'status' key
+                    # TODO: manage main language for statuses
+                    return cache[C.PRESENCE_STATUSES].get('default','')
+
+            return cache[name]
+        except KeyError:
             return None
 
-    def setCache(self, jid, name, value):
-        jid_cache = self._cache.setdefault(jid.bare, {})
-        jid_cache[name] = value
+    def setCache(self, entity, name, value):
+        """Set or update value for one data in cache
+
+        @param entity(JID): entity to update
+        @param name(unicode): value to set or update
+        """
+        self.setContact(entity, None, {name: value})
+
+    def getFullJid(self, entity):
+        """Get full jid from a bare jid
+
+        @param entity(jid.JID): must be a bare jid
+        @return (jid.JID): bare jid + main resource
+        @raise ValueError: the entity is not bare
+        """
+        if entity.resource:
+            raise ValueError("getFullJid must be used with a bare jid")
+        main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE)
+        return jid.JID(u"{}/{}".format(entity, main_resource))
+
+
+    def setGroupData(self, group, name, value):
+        """Register a data for a group
+
+        @param group: a valid (existing) group name
+        @param name: name of the data (can't be "jids")
+        @param value: value to set
+        """
+        assert name is not 'jids'
+        self._groups[group][name] = value
 
-    def __contains__(self, jid):
-        raise NotImplementedError
+    def getGroupData(self, group, name=None):
+        """Return value associated to group data
+
+        @param group: a valid (existing) group name
+        @param name: name of the data or None to get the whole dict
+        @return: registered value
+        """
+        if name is None:
+            return self._groups[group]
+        return self._groups[group][name]
+
+    def setSpecial(self, entity, special_type):
+        """Set special flag on an entity
+
+        @param entity(jid.JID): jid of the special entity
+        @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag
+        """
+        assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
+        self.setCache(entity, C.CONTACT_SPECIAL, special_type)
+
+    def getSpecials(self, special_type=None):
+        """Return all the bare JIDs of the special roster entities of the type
+        specified by special_type. If special_type is None, return all specials.
+
+        @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all specials.
+        @return: set(jid.JID)
+        """
+        if special_type is None:
+            return self._specials
+        return set([entity for entity in self._specials if self.getCache(entity, C.CONTACT_SPECIAL) == special_type])
 
     def clearContacts(self):
         """Clear all the contact list"""
-        self.specials.clear()
+        self.unselectAll()
+        self._cache.clear()
+        self._groups.clear()
+        self._specials.clear()
+        self.update()
 
-    def replace(self, jid, groups=None, attributes=None):
+    def setContact(self, entity, groups=None, attributes=None, in_roster=False):
         """Add a contact to the list if doesn't exist, else update it.
 
         This method can be called with groups=None for the purpose of updating
@@ -64,49 +260,212 @@
 
         None value for 'groups' has a different meaning than [None] which is for the default group.
 
-        @param jid (JID)
+        @param entity (jid.JID): entity to add or replace
         @param groups (list): list of groups or None to ignore the groups membership.
-        @param attributes (dict)
+        @param attributes (dict): attibutes of the added jid or to update
+        @param in_roster (bool): True if contact is from roster
         """
-        if attributes and 'name' in attributes:
-            self.setCache(jid, 'name', attributes['name'])
+        if attributes is None:
+            attributes = {}
+
+        entity_bare = entity.bare
+
+        if in_roster:
+            self._roster.add(entity_bare)
+
+        cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}})
+
+        assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes
 
-    def remove(self, jid):
-        """remove a contact from the list"""
-        try:
-            del self.specials[jid.bare]
-        except KeyError:
-            pass
+        # we set groups and fill self._groups accordingly
+        if groups is not None:
+            if not groups:
+                groups = [None]  # [None] is the default group
+            if C.CONTACT_GROUPS in cache:
+                # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS]
+                for group in [group for group in cache[C.CONTACT_GROUPS] if group not in groups]:
+                    self._groups[group]['jids'].remove(entity_bare)
+            cache[C.CONTACT_GROUPS] = groups
+            for group in groups:
+                self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare)
+
+        # special entities management
+        if C.CONTACT_SPECIAL in attributes:
+            if attributes[C.CONTACT_SPECIAL] is None:
+                del attributes[C.CONTACT_SPECIAL]
+                self._specials.remove(entity_bare)
+            else:
+                self._specials.add(entity_bare)
+
+        # now the attribute we keep in cache
+        for attribute, value in attributes.iteritems():
+            cache[attribute] = value
+
+        # we can update the display
+        self.update()
+
+    def getContacts(self):
+        """Return contacts currently selected
+
+        @return (set): set of selected entities"""
+        return self._selected
+
+    def entityToShow(self, entity, check_resource=False):
+        """Tell if the contact should be showed or hidden.
 
-    def add(self, jid, param_groups=None):
-        """add a contact to the list"""
-        raise NotImplementedError
+        @param entity (jid.JID): jid of the contact
+        @param check_resource (bool): True if resource must be significant
+        @return (bool): True if that contact should be showed in the list
+        """
+        show = self.getCache(entity, C.PRESENCE_SHOW)
 
-    def getSpecial(self, jid):
-        """Return special type of jid, or None if it's not special"""
-        return self.specials.get(jid.bare)
+        if check_resource:
+            alerts = self._alerts
+            selected = self._selected
+        else:
+            alerts = {alert.bare for alert in self._alerts}
+            selected = {selected.bare for selected in self._selected}
+        return ((show is not None and show != "unavailable")
+                or self.show_disconnected
+                or entity in alerts
+                or entity in selected)
+
+    def anyEntityToShow(self, entities, check_resources=False):
+        """Tell if in a list of entities, at least one should be shown
+
+        @param entities (list[jid.JID]): list of jids
+        @param check_resources (bool): True if resources must be significant
+        @return: bool
+        """
+        for entity in entities:
+            if self.entityToShow(entity, check_resources):
+                return True
+        return False
+
+    def isEntityInGroup(self, entity, group):
+        """Tell if an entity is in a roster group
 
-    def setSpecial(self, jid, _type, show=False):
-        """Set entity as a special
-        @param jid: jid of the entity
-        @param _type: special type (e.g.: "MUC")
-        @param show: True to display the dialog to chat with this entity
+        @param entity(jid.JID): jid of the entity
+        @param group(unicode): group to check
+        @return (bool): True if the entity is in the group
+        """
+        return entity in self.getGroupData(group, "jids")
+
+    def remove(self, entity):
+        """remove a contact from the list
+
+        @param entity(jid.JID): jid of the entity to remove (bare jid is used)
         """
-        self.specials[jid.bare] = _type
+        entity_bare = entity.bare
+        try:
+            groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
+        except KeyError:
+            log.warning(_("Trying to delete an unknow entity [{}]").format(entity))
+        del self._cache[entity_bare]
+        for group in groups:
+            self._groups[group]['jids'].remove(entity_bare)
+        for set_ in (self._selected, self._alerts, self._specials, self._special_extras):
+            to_remove = set()
+            for set_entity in set_:
+                if set_entity.bare == entity.bare:
+                    to_remove.add(set_entity)
+            set_.difference_update(to_remove)
+        self.update()
 
-    def updatePresence(self, jid, show, priority, statuses):
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
         """Update entity's presence status
-        @param jid: entity to update's jid
+
+        @param entity(jid.JID): entity updated
         @param show: availability
         @parap priority: resource's priority
-        @param statuses: dict of statuses"""
-        self.setCache(jid, 'show', show)
-        self.setCache(jid, 'prority', priority)
-        self.setCache(jid, 'statuses', statuses)
-        self.update_jid(jid)
+        @param statuses: dict of statuses
+        @param profile: %(doc_profile)s
+        """
+        cache = self.getCache(entity)
+        if show == C.PRESENCE_UNAVAILABLE:
+            if not entity.resource:
+                cache[C.CONTACT_RESOURCES].clear()
+                cache[C.CONTACT_MAIN_RESOURCE]= None
+            else:
+                try:
+                    del cache[C.CONTACT_RESOURCES][entity.resource]
+                except KeyError:
+                    log.error("Presence unavailable received for an unknown resource [{}]".format(entity))
+                if not cache[C.CONTACT_RESOURCES]:
+                    cache[C.CONTACT_MAIN_RESOURCE] = None
+        else:
+            assert entity.resource
+            resources_data = cache[C.CONTACT_RESOURCES]
+            resource_data = resources_data.setdefault(entity.resource, {})
+            resource_data[C.PRESENCE_SHOW] = show
+            resource_data[C.PRESENCE_PRIORITY] = int(priority)
+            resource_data[C.PRESENCE_STATUSES] = statuses
+
+            priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
+            cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
+
+    def onNickUpdate(self, entity, new_nick, profile):
+        """Update entity's nick
+
+        @param entity(jid.JID): entity updated
+        @param new_nick(unicode): new nick of the entity
+        @param profile: %(doc_profile)s
+        """
+        raise NotImplementedError # Must be implemented by frontends
+
+    def unselectAll(self):
+        """Unselect all contacts"""
+        self._selected.clear()
+        self.update()
+
+    def select(self, entity):
+        """Select an entity
+
+        @param entity(jid.JID): entity to select (resource is significant)
+        """
+        log.debug("select %s" % entity)
+        self._selected.add(entity)
+        self.update()
+
+    def setAlert(self, entity):
+        """Set an alert on the entity (usually for a waiting message)
+
+        @param entity(jid.JID): entity which must displayed in alert mode (resource is significant)
+        """
+        self._alerts.add(entity)
+        self.update()
+
+    def _showOfflineContacts(self, show_str):
+        self.showOfflineContacts(C.bool(show_str))
 
     def showOfflineContacts(self, show):
-        pass
+        """Tell if offline contacts should shown
+
+        @param show(bool): True if offline contacts should be shown
+        """
+        assert isinstance(show, bool)
+        if self.show_disconnected == show:
+            return
+        self.show_disconnected = show
+        self.update()
+
+    def _showEmptyGroups(self, show_str):
+        self.showEmptyGroups(C.bool(show_str))
 
     def showEmptyGroups(self, show):
-        pass
+        assert isinstance(show, bool)
+        if self.show_empty_groups == show:
+            return
+        self.show_empty_groups = show
+        self.update()
+
+    def showResources(self, show):
+        show = C.bool(show)
+        if self.show_resources == show:
+            return
+        self.show_resources = show
+        self.update()
+
+    def onDelete(self):
+        QuickWidget.onDelete(self)
+        self.host.removeListener('presence', self.presenceListener)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/quick_frontend/quick_game_tarot.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# helper class for making a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools.jid import JID
+
+
+class QuickTarotGame(object):
+
+    def __init__(self, parent, referee, players):
+        self._autoplay = None  #XXX: use 0 to activate fake play, None else
+        self.parent = parent
+        self.referee = referee
+        self.players = players
+        self.played = {}
+        for player in players:
+            self.played[player] = None
+        self.player_nick = parent.nick
+        self.bottom_nick = unicode(self.player_nick)
+        idx = self.players.index(self.player_nick)
+        idx = (idx + 1) % len(self.players)
+        self.right_nick = unicode(self.players[idx])
+        idx = (idx + 1) % len(self.players)
+        self.top_nick = unicode(self.players[idx])
+        idx = (idx + 1) % len(self.players)
+        self.left_nick = unicode(self.players[idx])
+        self.bottom_nick = unicode(self.player_nick)
+        self.selected = []  #Card choosed by the player (e.g. during ecart)
+        self.hand_size = 13  #number of cards in a hand
+        self.hand = []
+        self.to_show = []
+        self.state = None
+
+    def resetRound(self):
+        """Reset the game's variables to be reatty to start the next round"""
+        del self.selected[:]
+        del self.hand[:]
+        del self.to_show[:]
+        self.state = None
+        for pl in self.played:
+            self.played[pl] = None
+
+    def getPlayerLocation(self, nick):
+        """return player location (top,bottom,left or right)"""
+        for location in ['top','left','bottom','right']:
+            if getattr(self,'%s_nick' % location) == nick:
+                return location
+        assert(False)
+
+    def loadCards(self):
+        """Load all the cards in memory
+        @param dir: directory where the PNG files are"""
+        self.cards={}
+        self.deck=[]
+        self.cards["atout"]={} #As Tarot is a french game, it's more handy & logical to keep french names
+        self.cards["pique"]={} #spade
+        self.cards["coeur"]={} #heart
+        self.cards["carreau"]={} #diamond
+        self.cards["trefle"]={} #club
+
+    def tarotGameNewHandler(self, hand):
+        """Start a new game, with given hand"""
+        assert (len(self.hand) == 0)
+        for suit, value in hand:
+            self.hand.append(self.cards[suit, value])
+        self.hand.sort()
+        self.state = "init"
+
+    def tarotGameChooseContratHandler(self, xml_data):
+        """Called when the player as to select his contrat
+        @param xml_data: SàT xml representation of the form"""
+        raise NotImplementedError
+
+    def tarotGameShowCardsHandler(self, game_stage, cards, data):
+        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
+        self.to_show = []
+        for suit, value in cards:
+            self.to_show.append(self.cards[suit, value])
+        if game_stage == "chien" and data['attaquant'] == self.player_nick:
+            self.state = "wait_for_ecart"
+        else:
+            self.state = "chien"
+
+    def tarotGameYourTurnHandler(self):
+        """Called when we have to play :)"""
+        if self.state == "chien":
+            self.to_show = []
+        self.state = "play"
+        self.__fakePlay()
+
+    def __fakePlay(self):
+        """Convenience method for stupid autoplay
+        /!\ don't forgot to comment any interactive dialog for invalid card"""
+        if self._autoplay == None:
+            return
+        if self._autoplay >= len(self.hand):
+            self._autoplay = 0
+        card = self.hand[self._autoplay]
+        self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], self.parent.profile)
+        del self.hand[self._autoplay]
+        self.state = "wait"
+        self._autoplay+=1
+
+    def tarotGameScoreHandler(self, xml_data, winners, loosers):
+        """Called at the end of a game
+        @param xml_data: SàT xml representation of the scores
+        @param winners: list of winners' nicks
+        @param loosers: list of loosers' nicks"""
+        raise NotImplementedError
+
+    def tarotGameCardsPlayedHandler(self, player, cards):
+        """A card has been played by player"""
+        if self.to_show:
+            self.to_show = []
+        pl_cards = []
+        if self.played[player] != None: #FIXME
+            for pl in self.played:
+                self.played[pl] = None
+        for suit, value in cards:
+            pl_cards.append(self.cards[suit, value])
+        self.played[player] = pl_cards[0]
+
+    def tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards):
+        """Invalid cards have been played
+        @param phase: phase of the game
+        @param played_cards: all the cards played
+        @param invalid_cards: cards which are invalid"""
+
+        if phase == "play":
+            self.state = "play"
+        elif phase == "ecart":
+            self.state = "ecart"
+        else:
+            log.error ('INTERNAL ERROR: unmanaged game phase')
+
+        for suit, value in played_cards:
+            self.hand.append(self.cards[suit, value])
+
+        self.hand.sort()
+        self.__fakePlay()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/quick_frontend/quick_games.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,106 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# helper class for making a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _
+
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend.constants import Const as C
+
+import quick_chat
+
+
+class RoomGame(object):
+    _game_name = None
+    _signal_prefix = None
+    _signal_suffixes = None
+
+    @classmethod
+    def registerSignals(cls, host):
+
+        def make_handler(suffix, signal):
+            def handler(*args):
+                if suffix in ("Started", "Players"):
+                    return cls.startedHandler(host, suffix, *args)
+                return cls.genericHandler(host, signal, *args)
+            return handler
+
+        for suffix in cls._signal_suffixes:
+            signal = cls._signal_prefix + suffix
+            host.registerSignal(signal, handler=make_handler(suffix, signal), iface="plugin")
+
+    @classmethod
+    def startedHandler(cls, host, suffix, *args):
+        room_jid, args, profile = jid.JID(args[0]), args[1:-1], args[-1]
+        referee, players, args = args[0], args[1], args[2:]
+        chat_widget = host.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+
+        # self.occupants_panel.updateSpecials(players, SYMBOLS[self._game_name.lower()]) # FIXME
+        if suffix == "Players" or chat_widget.nick not in players:
+            return  # waiting for other players to join, or not playing
+        if cls._game_name in chat_widget.games:
+            return  # game panel is already there
+        real_class = host.widgets.getRealClass(cls)
+        if real_class == cls:
+            host.showDialog(_(u"A {game} activity between {players} has been started, but you couldn't take part because your client doesn't support it.").format(game=cls._game_name, players=', '.join(players)),
+                            _(u"{game} Game").format(game=cls._game_name))
+            return
+        panel = real_class(chat_widget, referee, players, *args)
+        chat_widget.games[cls._game_name] = panel
+        chat_widget.addGamePanel(panel)
+
+    @classmethod
+    def genericHandler(cls, host, signal, *args):
+        room_jid, args, profile = jid.JID(args[0]), args[1:-1], args[-1]
+        chat_widget = host.widgets.getWidget(quick_chat.QuickChat, room_jid, profile)
+        if chat_widget:
+            try:
+                game_panel = chat_widget.games[cls._game_name]
+            except KeyError:
+                log.error("TODO: better game synchronisation - received signal %s but no panel is found" % signal)
+                return
+            else:
+                getattr(game_panel, "%sHandler" % signal)(*args)
+
+
+class Tarot(RoomGame):
+    _game_name = "Tarot"
+    _signal_prefix = "tarotGame"
+    _signal_suffixes = ("Started", "Players", "New", "ChooseContrat",
+                        "ShowCards", "YourTurn", "Score", "CardsPlayed",
+                        "InvalidCards",
+                        )
+
+
+class Quiz(RoomGame):
+    _game_name = "Quiz"
+    _signal_prefix = "quizGame"
+    _signal_suffixes = ("Started", "New", "Question", "PlayerBuzzed",
+                        "PlayerSays", "AnswerResult", "TimerExpired",
+                        "TimerRestarted",
+                        )
+
+
+class Radiocol(RoomGame):
+    _game_name = "Radiocol"
+    _signal_prefix = "radiocol"
+    _signal_suffixes = ("Started", "Players", "SongRejected", "Preload",
+                        "Play", "NoUpload", "UploadOk")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/quick_frontend/quick_menus.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,443 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# helper class for making a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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/>.
+
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('')  # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError):  # Error raised is not the same depending on pyjsbuild options
+    unicode = str
+
+from sat.core.log import getLogger
+from sat.core.i18n import _, languageSwitch
+log = getLogger(__name__)
+from sat_frontends.quick_frontend.constants import Const as C
+from collections import OrderedDict
+
+
+## items ##
+
+class MenuBase(object):
+    ACTIVE=True
+
+    def __init__(self, name, extra=None):
+        """
+        @param name(unicode): canonical name of the item
+        @param extra(dict[unicode, unicode], None): same as in [addMenus]
+        """
+        self._name = name
+        self.setExtra(extra)
+
+    @property
+    def canonical(self):
+        """Return the canonical name of the container, used to identify it"""
+        return self._name
+
+    @property
+    def name(self):
+        """Return the name of the container, can be translated"""
+        return self._name
+
+    def setExtra(self, extra):
+        if extra is None:
+            extra = {}
+        self.icon = extra.get("icon")
+
+
+class MenuItem(MenuBase):
+    """A callable item in the menu"""
+    CALLABLE=False
+
+    def __init__(self, name, name_i18n, extra=None, type_=None):
+        """
+        @param name(unicode): canonical name of the item
+        @param name_i18n(unicode): translated name of the item
+        @param extra(dict[unicode, unicode], None): same as in [addMenus]
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        """
+        MenuBase.__init__(self, name, extra)
+        self._name_i18n = name_i18n if name_i18n else name
+        self.type = type_
+
+    @property
+    def name(self):
+        return self._name_i18n
+
+    def collectData(self, caller):
+        """Get data according to data_collector
+
+        @param caller: Menu caller
+        """
+        assert self.type is not None # if data collector are used, type must be set
+        data_collector = QuickMenusManager.getDataCollector(self.type)
+
+        if data_collector is None:
+            return {}
+
+        elif callable(data_collector):
+            return data_collector(caller, self.name)
+
+        else:
+            if caller is None:
+                log.error(u"Caller can't be None with a dictionary as data_collector")
+                return {}
+            data = {}
+            for data_key, caller_attr in data_collector.iteritems():
+                data[data_key] = unicode(getattr(caller, caller_attr))
+            return data
+
+
+    def call(self, caller, profile=C.PROF_KEY_NONE):
+        """Execute the menu item
+
+        @param caller: instance linked to the menu
+        @param profile: %(doc_profile)s
+        """
+        raise NotImplementedError
+
+
+class MenuItemDistant(MenuItem):
+    """A MenuItem with a distant callback"""
+    CALLABLE=True
+
+    def __init__(self, host, type_, name, name_i18n, id_, extra=None):
+        """
+        @param host: %(doc_host)s
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param name(unicode): canonical name of the item
+        @param name_i18n(unicode): translated name of the item
+        @param id_(unicode): id of the distant callback
+        @param extra(dict[unicode, unicode], None): same as in [addMenus]
+        """
+        MenuItem.__init__(self, name, name_i18n, extra, type_)
+        self.host = host
+        self.id = id_
+
+    def call(self, caller, profile=C.PROF_KEY_NONE):
+        data =  self.collectData(caller)
+        log.debug("data collected: %s" % data)
+        self.host.launchAction(self.id, data, profile=profile)
+
+
+class MenuItemLocal(MenuItem):
+    """A MenuItem with a distant callback"""
+    CALLABLE=True
+
+    def __init__(self, type_, name, name_i18n, callback, extra=None):
+        """
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param name(unicode): canonical name of the item
+        @param name_i18n(unicode): translated name of the item
+        @param callback(callable): local callback. Will be called with the caller as only argument if data_collector is None, else with the caller + requested data + profile.
+        @param extra(dict[unicode, unicode], None): same as in [addMenus]
+        """
+        MenuItem.__init__(self, name, name_i18n, extra, type_)
+        self.callback = callback
+
+    def call(self, caller, profile=C.PROF_KEY_NONE):
+        data_collector = QuickMenusManager.getDataCollector(self.type)
+        if data_collector is None:
+            self.callback()
+        else:
+            self.callback(caller, self.collectData(caller), profile)
+
+
+class MenuHook(MenuItemLocal):
+    """A MenuItem which replace an expected item from backend"""
+    pass
+
+
+class MenuPlaceHolder(MenuItem):
+    """A non existant menu which is used to keep a position"""
+    ACTIVE=False
+
+    def __init__(self, name):
+        MenuItem.__init__(self, name, name)
+
+
+class MenuSeparator(MenuItem):
+    """A separation between items/categories"""
+    SEP_IDX=0
+
+    def __init__(self):
+        MenuSeparator.SEP_IDX +=1
+        name = u"___separator_{}".format(MenuSeparator.SEP_IDX)
+        MenuItem.__init__(self, name, name)
+
+
+## containers ##
+
+
+class MenuContainer(MenuBase):
+
+    def __init__(self, name, extra=None):
+        MenuBase.__init__(self, name, extra)
+        self._items = OrderedDict()
+
+    def __len__(self):
+        return len(self._items)
+
+    def __contains__(self, item):
+        return item.canonical in self._items
+
+    def __iter__(self):
+        return self._items.itervalues()
+
+    def __getitem__(self, item):
+        try:
+            return self._items[item.canonical]
+        except KeyError:
+            raise KeyError(item)
+
+    def getOrCreate(self, item):
+        log.debug("MenuContainer getOrCreate: item=%s name=%s\nlist=%s" % (item, item.canonical, self._items.keys()))
+        try:
+            return self[item]
+        except KeyError:
+            self.append(item)
+            return item
+
+    def getActiveMenus(self):
+        """Return an iterator on active children"""
+        for child in self._items.itervalues():
+            if child.ACTIVE:
+                yield child
+
+    def append(self, item):
+        """add an item at the end of current ones
+
+        @param item: instance of MenuBase (must be unique in container)
+        """
+        assert isinstance(item, MenuItem) or isinstance(item, MenuContainer)
+        assert item.canonical not in self._items
+        self._items[item.canonical] = item
+
+    def replace(self, item):
+        """add an item at the end of current ones or replace an existing one"""
+        self._items[item.canonical] = item
+
+
+class MenuCategory(MenuContainer):
+    """A category which can hold other menus or categories"""
+
+    def __init__(self, name, name_i18n=None, extra=None):
+        """
+        @param name(unicode): canonical name
+        @param name_i18n(unicode, None): translated name
+        @param icon(unicode, None): same as in MenuBase.__init__
+        """
+        log.debug("creating menuCategory %s with extra %s" % (name, extra))
+        MenuContainer.__init__(self, name, extra)
+        self._name_i18n = name_i18n or name
+
+    @property
+    def name(self):
+        return self._name_i18n
+
+
+class MenuType(MenuContainer):
+    """A type which can hold other menus or categories"""
+    pass
+
+
+## manager ##
+
+
+class QuickMenusManager(object):
+    """Manage all the menus"""
+    _data_collectors={C.MENU_GLOBAL: None} # No data is associated with C.MENU_GLOBAL items
+
+    def __init__(self, host, menus=None, language=None):
+        """
+        @param host: %(doc_host)s
+        @param menus(iterable): menus as in [addMenus]
+        @param language: same as in [i18n.languageSwitch]
+        """
+        self.host = host
+        MenuBase.host = host
+        self.language = language
+        self.menus = {}
+        if menus is not None:
+            self.addMenus(menus)
+
+    def _getPathI18n(self, path):
+        """Return translated version of path"""
+        languageSwitch(self.language)
+        path_i18n = [_(elt) for elt in path]
+        languageSwitch()
+        return path_i18n
+
+    def _createCategories(self, type_, path, path_i18n=None, top_extra=None):
+        """Create catogories of the path
+
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param path(list[unicode]):  same as in [sat.core.sat_main.SAT.importMenu]
+        @param path_i18n(list[unicode], None):  translated menu path (same lenght as path) or None to get deferred translation of path
+        @param top_extra: extra data to use on the first element of path only. If the first element already exists and is reused, top_extra will be ignored (you'll have to manually change it if you really want to).
+        @return (MenuContainer): last category created, or MenuType if path is empty
+        """
+        if path_i18n is None:
+            path_i18n = self._getPathI18n(path)
+        assert len(path) == len(path_i18n)
+        menu_container = self.menus.setdefault(type_, MenuType(type_))
+
+        for idx, category in enumerate(path):
+            menu_category = MenuCategory(category, path_i18n[idx], extra=top_extra)
+            menu_container = menu_container.getOrCreate(menu_category)
+            top_extra = None
+
+        return menu_container
+
+    @staticmethod
+    def addDataCollector(type_, data_collector):
+        """Associate a data collector to a menu type
+
+        A data collector is a method or a map which allow to collect context data to construct the dictionnary which will be sent to the bridge method managing the menu item.
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param data_collector(dict[unicode,unicode], callable, None): can be:
+            - a dict which map data name to local name. The attribute named after the dict values will be getted from caller, and put in data. e.g.: if data_collector={'room_jid':'target'}, then the "room_jid" data will be the value of the "target" attribute of the caller.
+            - a callable which must return the data dictionnary. callable will have caller and item name as argument
+            - None: an empty dict will be used
+        """
+        QuickMenusManager._data_collectors[type_] = data_collector
+
+    @staticmethod
+    def getDataCollector(type_):
+        """Get data_collector associated to type_
+
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @return (callable, dict, None): data_collector
+        """
+        try:
+            return QuickMenusManager._data_collectors[type_]
+        except KeyError:
+            log.error(u"No data collector registered for {}".format(type_))
+            return None
+
+    def addMenuItem(self, type_, path, item, path_i18n=None, top_extra=None):
+        """Add a MenuItemBase instance
+
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param path(list[unicode]):  same as in [sat.core.sat_main.SAT.importMenu], stop at the last parent category
+        @param item(MenuItem): a instancied item
+        @param path_i18n(list[unicode],None):  translated menu path (same lenght as path) or  None to use deferred translation of path
+        @param top_extra: same as in [_createCategories]
+        """
+        if path_i18n is None:
+            path_i18n = self._getPathI18n(path)
+        assert path and len(path) == len(path_i18n)
+
+        menu_container = self._createCategories(type_, path, path_i18n, top_extra)
+
+        if item in menu_container:
+            if isinstance(item, MenuHook):
+                menu_container.replace(item)
+            else:
+                container_item = menu_container[item]
+                if isinstance(container_item, MenuPlaceHolder):
+                    menu_container.replace(item)
+                elif isinstance(container_item, MenuHook):
+                    # MenuHook must not be replaced
+                    log.debug(u"ignoring menu at path [{}] because a hook is already in place".format(path))
+                else:
+                    log.error(u"Conflicting menus at path [{}]".format(path))
+        else:
+            log.debug(u"Adding menu [{type_}] {path}".format(type_=type_, path=path))
+            menu_container.append(item)
+            self.host.callListeners('menu', type_, path, path_i18n, item)
+
+    def addMenu(self, type_, path, path_i18n=None, extra=None, top_extra=None, id_=None, callback=None):
+        """Add a menu item
+
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param path(list[unicode]):  same as in [sat.core.sat_main.SAT.importMenu]
+        @param path_i18n(list[unicode], None):  translated menu path (same lenght as path), or None to get deferred translation
+        @param extra(dict[unicode, unicode], None): same as in [addMenus]
+        @param top_extra: same as in [_createCategories]
+        @param id_(unicode): callback id (mutually exclusive with callback)
+        @param callback(callable): local callback (mutually exclusive with id_)
+        """
+        if path_i18n is None:
+            path_i18n = self._getPathI18n(path)
+        assert bool(id_) ^ bool(callback) # we must have id_ xor callback defined
+        if id_:
+            menu_item = MenuItemDistant(self.host, type_, path[-1], path_i18n[-1], id_=id_, extra=extra)
+        else:
+            menu_item = MenuItemLocal(type_, path[-1], path_i18n[-1], callback=callback, extra=extra)
+        self.addMenuItem(type_, path[:-1], menu_item, path_i18n[:-1], top_extra)
+
+    def addMenus(self, menus, top_extra=None):
+        """Add several menus at once
+
+        @param menus(iterable): iterable with:
+            id_(unicode,callable): id of distant callback or local callback
+            type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+            path(iterable[unicode]):  same as in [sat.core.sat_main.SAT.importMenu]
+            path_i18n(iterable[unicode]):  translated menu path (same lenght as path)
+            extra(dict[unicode,unicode]): dictionary of extra data (used on the leaf menu), can be:
+                - "icon": icon name
+        @param top_extra: same as in [_createCategories]
+        """
+        # TODO: manage icons
+        for id_, type_, path, path_i18n, extra in menus:
+            if callable(id_):
+                self.addMenu(type_, path, path_i18n, callback=id_, extra=extra, top_extra=top_extra)
+            else:
+                self.addMenu(type_, path, path_i18n, id_=id_, extra=extra, top_extra=top_extra)
+
+    def addMenuHook(self, type_, path, path_i18n=None, extra=None, top_extra=None, callback=None):
+        """Helper method to add a menu hook
+
+        Menu hooks are local menus which override menu given by backend
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param path(list[unicode]):  same as in [sat.core.sat_main.SAT.importMenu]
+        @param path_i18n(list[unicode], None):  translated menu path (same lenght as path), or None to get deferred translation
+        @param extra(dict[unicode, unicode], None): same as in [addMenus]
+        @param top_extra: same as in [_createCategories]
+        @param callback(callable): local callback (mutually exclusive with id_)
+        """
+        if path_i18n is None:
+            path_i18n = self._getPathI18n(path)
+        menu_item = MenuHook(type_, path[-1], path_i18n[-1], callback=callback, extra=extra)
+        self.addMenuItem(type_, path[:-1], menu_item, path_i18n[:-1], top_extra)
+        log.info(u"Menu hook set on {path} ({type_})".format(path=path, type_=type_))
+
+    def addCategory(self, type_, path, path_i18n=None, extra=None, top_extra=None):
+        """Create a category with all parents, and set extra on the last one
+
+        @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu]
+        @param path(list[unicode]):  same as in [sat.core.sat_main.SAT.importMenu]
+        @param path_i18n(list[unicode], None):  translated menu path (same lenght as path), or None to get deferred translation of path
+        @param extra(dict[unicode, unicode], None): same as in [addMenus] (added on the leaf category only)
+        @param top_extra: same as in [_createCategories]
+        @return (MenuCategory): last category add
+        """
+        if path_i18n is None:
+            path_i18n = self._getPathI18n(path)
+        last_container = self._createCategories(type_, path, path_i18n, top_extra=top_extra)
+        last_container.setExtra(extra)
+        return last_container
+
+    def getMainContainer(self, type_):
+        """Get a main MenuType container
+
+        @param type_: a C.MENU_* constant
+        @return(MenuContainer): the main container
+        """
+        menu_container = self.menus.setdefault(type_, MenuType(type_))
+        return menu_container
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/quick_frontend/quick_profile_manager.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,259 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# helper class for making a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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 sat_frontends.primitivus.constants import Const as C
+
+
+class ProfileRecord(object):
+    """Class which manage data for one profile"""
+
+    def __init__(self, profile=None, login=None, password=None):
+        self._profile = profile
+        self._login = login
+        self._password = password
+
+    @property
+    def profile(self):
+        return self._profile
+
+    @profile.setter
+    def profile(self, value):
+        self._profile = value
+        # if we change the profile,
+        # we must have no login/password until backend give them
+        self._login = self._password = None
+
+    @property
+    def login(self):
+        return self._login
+
+    @login.setter
+    def login(self, value):
+        self._login = value
+
+    @property
+    def password(self):
+        return self._password
+
+    @password.setter
+    def password(self, value):
+        self._password = value
+
+
+class QuickProfileManager(object):
+    """Class with manage profiles creation/deletion/connection"""
+
+    def __init__(self, host, autoconnect=None):
+        """Create the manager
+
+        @param host: %(doc_host)s
+        @param autoconnect(iterable): list of profiles to connect automatically
+        """
+        self.host = host
+        self._autoconnect = bool(autoconnect)
+        self.current = ProfileRecord()
+
+    def go(self, autoconnect):
+        if self._autoconnect:
+            self.autoconnect(autoconnect)
+
+    def autoconnect(self, profile_keys):
+        """Automatically connect profiles
+
+        @param profile_keys(iterable): list of profile keys to connect
+        """
+        if not profile_keys:
+            log.warning("No profile given to autoconnect")
+            return
+        self._autoconnect = True
+        self._autoconnect_profiles=[]
+        self._do_autoconnect(profile_keys)
+
+
+    def _do_autoconnect(self, profile_keys):
+        """Connect automatically given profiles
+
+        @param profile_kes(iterable): profiles to connect
+        """
+        assert self._autoconnect
+
+        def authenticate_cb(callback_id, data, profile):
+
+            if C.bool(data['validated']):
+                self._autoconnect_profiles.append(profile)
+                if len(self._autoconnect_profiles) == len(profile_keys):
+                    # all the profiles have been validated
+                    self.host.plug_profiles(self._autoconnect_profiles)
+            else:
+                # a profile is not validated, we go to manual mode
+                self._autoconnect=False
+
+        for profile_key in profile_keys:
+            profile = self.host.bridge.getProfileName(profile_key)
+            if not profile:
+                self._autoconnect = False # manual mode
+                msg = _("Trying to plug an unknown profile key ({})".format(profile_key))
+                log.warning(msg)
+                self.alert(_("Profile plugging in error"), msg)
+                break
+            self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=profile)
+
+
+    def getParamError(self, dummy):
+        self.alert(_(u"Error"), _("Can't get profile parameter"))
+
+    ## Helping methods ##
+
+    def _getErrorMessage(self, reason):
+        """Return an error message corresponding to profile creation error
+
+        @param reason (str): reason as returned by asyncCreateProfile
+        @return (unicode): human readable error message
+        """
+        if reason == "ConflictError":
+            message = _("A profile with this name already exists")
+        elif reason == "CancelError":
+            message = _("Profile creation cancelled by backend")
+        elif reason == "ValueError":
+            message = _("You profile name is not valid") # TODO: print a more informative message (empty name, name starting with '@')
+        else:
+            message = _("Can't create profile ({})").format(reason)
+        return message
+
+    def _deleteProfile(self):
+        """Delete the currently selected profile"""
+        if self.current.profile:
+            self.host.bridge.asyncDeleteProfile(self.current.profile, callback=self.refillProfiles)
+            self.resetFields()
+
+    ## workflow methods (events occuring during the profiles selection) ##
+
+    # These methods must be called by the frontend at some point
+
+    def _onConnectProfiles(self):
+        """Connect the profiles and start the main widget"""
+        if self._autoconnect:
+            self.alert(_('Internal error'), _('You can connect manually and automatically at the same time'))
+            return
+        self.updateConnectionParams()
+        profiles = self.getProfiles()
+        if not profiles:
+            self.alert(_('No profile selected'), _('You need to create and select at least one profile before connecting'))
+        else:
+            # All profiles in the list are already validated, so we can plug them directly
+            self.host.plug_profiles(profiles)
+
+    def getConnectionParams(self, profile):
+        """Get login and password and display them
+
+        @param profile: %(doc_profile)s
+        """
+        self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=self.setJID, errback=self.getParamError)
+        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=self.setPassword, errback=self.getParamError)
+
+    def updateConnectionParams(self):
+        """Check if connection parameters have changed, and update them if so"""
+        if self.current.profile:
+            login = self.getJID()
+            password = self.getPassword()
+            if login != self.current.login and self.current.login is not None:
+                self.current.login = login
+                self.host.bridge.setParam("JabberID", login, "Connection", profile_key=self.current.profile)
+                log.info("login updated for profile [{}]".format(self.current.profile))
+            if password != self.current.password and self.current.password is not None:
+                self.current.password = password
+                self.host.bridge.setParam("Password", password, "Connection", profile_key=self.current.profile)
+                log.info("password updated for profile [{}]".format(self.current.profile))
+
+    ## graphic updates (should probably be overriden in frontends) ##
+
+    def resetFields(self):
+        """Set profile to None, and reset fields"""
+        self.current.profile=None
+        self.setJID("")
+        self.setPassword("")
+
+    def refillProfiles(self):
+        """Rebuild the list of profiles"""
+        profiles = self.host.bridge.getProfilesList()
+        profiles.sort()
+        self.setProfiles(profiles)
+
+    ## Method which must be implemented by frontends ##
+
+    # get/set data
+
+    def getProfiles(self):
+        """Return list of selected profiles
+
+        Must be implemented by frontends
+        @return (list): list of profiles
+        """
+        raise NotImplementedError
+
+    def setProfiles(self, profiles):
+        """Update the list of profiles"""
+        raise NotImplementedError
+
+
+    def getJID(self):
+        """Get current jid
+
+        Must be implemented by frontends
+        @return (unicode): current jabber id
+        """
+        raise NotImplementedError
+
+    def getPassword(self):
+        """Get current password
+
+        Must be implemented by frontends
+        @return (unicode): current password
+        """
+        raise NotImplementedError
+
+    def setJID(self, jid_):
+        """Set current jid
+
+        Must be implemented by frontends
+        @param jid_(unicode): jabber id to set
+        """
+        raise NotImplementedError
+
+    def setPassword(self, password):
+        """Set current password
+
+        Must be implemented by frontends
+        """
+        raise NotImplementedError
+
+    # dialogs
+
+    def alert(self, title, message):
+        """Show an alert message, must be implemented by frontends
+
+        @param title: title of the dialog/popup
+        @param message: error message
+        """
+        raise NotImplementedError
+
+    # Note: a method which check profiles change must be implemented too
--- a/frontends/src/quick_frontend/quick_utils.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/quick_utils.py	Wed Mar 18 10:52:28 2015 +0100
@@ -17,20 +17,9 @@
 # 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.tools.jid  import JID
+from sat.core.i18n import _
 from os.path import exists, splitext
-from sat_frontends.quick_frontend.constants import Const
-
-def escapePrivate(ori_jid):
-    """Escape a private jid"""
-    return JID(Const.PRIVATE_PREFIX + ori_jid.bare + '@' + ori_jid.resource)
-
-def unescapePrivate(escaped_jid):
-    if not escaped_jid.startswith(Const.PRIVATE_PREFIX):
-        return escaped_jid
-    escaped_split = tuple(escaped_jid[len(Const.PRIVATE_PREFIX):].split('@'))
-    assert(len(escaped_split) == 3)
-    return JID("%s@%s/%s" % escaped_split)
+from optparse import OptionParser
 
 def getNewPath(path):
     """ Check if path exists, and find a non existant path if needed """
@@ -44,3 +33,19 @@
             return new_path
         idx+=1
 
+def check_options():
+    """Check command line options"""
+    usage = _("""
+    %prog [options]
+
+    %prog --help for options list
+    """)
+    parser = OptionParser(usage=usage) # TODO: use argparse
+
+    parser.add_option("-p", "--profile", help=_("Select the profile to use"))
+
+    (options, args) = parser.parse_args()
+    if options.profile:
+        options.profile = options.profile.decode('utf-8')
+    return options
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/quick_frontend/quick_widgets.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,310 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# helper class for making a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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.log import getLogger
+log = getLogger(__name__)
+from sat.core import exceptions
+
+from sat_frontends.quick_frontend.constants import Const as C
+
+classes_map = {}
+
+
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('') # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
+    unicode = str
+
+
+def register(base_cls, child_cls=None):
+    """Register a child class to use by default when a base class is needed
+
+    @param base_cls: "Quick..." base class (like QuickChat or QuickContact), must inherit from QuickWidget
+    @param child_cls: inherited class to use when Quick... class is requested, must inherit from base_cls.
+        Can be None if it's the base_cls itself which register
+    """
+    # FIXME: we use base_cls.__name__ instead of base_cls directly because pyjamas because
+    #        in the second case
+    classes_map[base_cls.__name__] = child_cls
+
+
+class WidgetAlreadyExistsError(Exception):
+    pass
+
+
+class QuickWidgetsManager(object):
+    """This class is used to manage all the widgets of a frontend
+    A widget can be a window, a graphical thing, or someting else depending of the frontend"""
+
+    def __init__(self, host):
+        self.host = host
+        self._widgets = {}
+
+    def __iter__(self):
+        """Iterate throught all widgets"""
+        for widget_map in self._widgets.itervalues():
+            for widget in widget_map.itervalues():
+                yield widget
+
+    def getRealClass(self, class_):
+        """Return class registered for given class_
+
+        @param class_: subclass of QuickWidget
+        @return: class actually used to create widget
+        """
+        try:
+            # FIXME: we use base_cls.__name__ instead of base_cls directly because pyjamas bugs
+            #        in the second case
+            cls = classes_map[class_.__name__]
+        except KeyError:
+            cls = class_
+        if cls is None:
+            raise exceptions.InternalError("There is not class registered for {}".format(class_))
+        return cls
+
+    def getWidgets(self, class_):
+        """Get all subclassed widgets
+
+        @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget]
+        @return: iterator on widgets
+        """
+        class_ = self.getRealClass(class_)
+        try:
+            widgets_map = self._widgets[class_.__name__]
+        except KeyError:
+            return iter([])
+        else:
+            return widgets_map.itervalues()
+
+    def getWidget(self, class_, target, profile):
+        """Get a widget without creating it if it doesn't exist.
+
+        @param class_(class): class of the widget to create
+        @param target: target depending of the widget, usually a JID instance
+        @param profile (unicode): %(doc_profile)s
+        @return: a class_ instance or None if the widget doesn't exist
+        """
+        class_ = self.getRealClass(class_)
+        hash_ = class_.getWidgetHash(target, profile)
+        try:
+            return self._widgets[class_.__name__][hash_]
+        except KeyError:
+            return None
+
+    def getOrCreateWidget(self, class_, target, *args, **kwargs):
+        """Get an existing widget or create a new one when necessary
+
+        If the widget is new, self.host.newWidget will be called with it.
+        @param class_(class): class of the widget to create
+        @param target: target depending of the widget, usually a JID instance
+        @param args(list): optional args to create a new instance of class_
+        @param kwargs(dict): optional kwargs to create a new instance of class_
+            if 'profile' key is present, it will be popped and put in 'profiles'
+            if there is neither 'profile' nor 'profiles', None will be used for 'profiles'
+            if 'on_new_widget' is present it can have the following values:
+                C.WIDGET_NEW [default]: self.host.newWidget will be called on widget creation
+                [callable]: this method will be called instead of self.host.newWidget
+                None: do nothing
+            if 'on_existing_widget' is present it can have the following values:
+                C.WIDGET_KEEP  [default]: return the existing widget
+                C.WIDGET_RAISE: raise WidgetAlreadyExistsError
+                C.WIDGET_RECREATE: create a new widget *WITH A NEW HASH*
+                [callable]: this method will be called with existing widget as argument
+            if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash
+            other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass,
+                it will be used to create a new QuickChat instance).
+        @return: a class_ instance, either new or already existing
+        """
+        cls = self.getRealClass(class_)
+
+        ## arguments management ##
+        _args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args
+        _kwargs = kwargs or {}
+        if 'profiles' in _kwargs and 'profile' in _kwargs:
+            raise ValueError("You can't have 'profile' and 'profiles' keys at the same time")
+        try:
+            _kwargs['profiles'] = _kwargs.pop('profile')
+        except KeyError:
+            if not 'profiles' in _kwargs:
+                _kwargs['profiles'] = None
+
+        #on_new_widget tell what to do for the new widget creation
+        try:
+            on_new_widget = _kwargs.pop('on_new_widget')
+        except KeyError:
+            on_new_widget = C.WIDGET_NEW
+
+        #on_existing_widget tell what to do when the widget already exists
+        try:
+            on_existing_widget = _kwargs.pop('on_existing_widget')
+        except KeyError:
+            on_existing_widget = C.WIDGET_KEEP
+
+        ## we get the hash ##
+        try:
+            hash_ = _kwargs.pop('force_hash')
+        except KeyError:
+            hash_ = cls.getWidgetHash(target, _kwargs['profiles'])
+
+        ## widget creation or retrieval ##
+
+        widgets_map = self._widgets.setdefault(cls.__name__, {}) # we sorts widgets by classes
+        if not cls.SINGLE:
+            widget = None # if the class is not SINGLE, we always create a new widget
+        else:
+            try:
+                widget = widgets_map[hash_]
+                widget.addTarget(target)
+            except KeyError:
+                widget = None
+
+        if widget is None:
+            # we need to create a new widget
+            log.debug(u"Creating new widget for target {} {}".format(target, cls))
+            widget = cls(*_args, **_kwargs)
+            widgets_map[hash_] = widget
+
+            if on_new_widget == C.WIDGET_NEW:
+                self.host.newWidget(widget)
+            elif callable(on_new_widget):
+                on_new_widget(widget)
+            else:
+                assert on_new_widget is None
+        else:
+            # the widget already exists
+            if on_existing_widget == C.WIDGET_KEEP:
+                pass
+            elif on_existing_widget == C.WIDGET_RAISE:
+                raise WidgetAlreadyExistsError(hash_)
+            elif on_existing_widget == C.WIDGET_RECREATE:
+                # we use getOrCreateWidget to recreate the new widget
+                # /!\ we use args and kwargs and not _args and _kwargs because we need the original args
+                #     we need to get rid of kwargs special options
+                new_kwargs = kwargs.copy()
+                try:
+                    new_kwargs.pop('force_hash')  # FIXME: we use pop instead of del here because pyjamas doesn't raise error on del
+                except KeyError:
+                    pass
+                else:
+                    raise ValueError("force_hash option can't be used with on_existing_widget=RECREATE")
+
+                # XXX: keep up-to-date if new special kwargs are added (i.e.: delete these keys here)
+                new_kwargs['on_existing_widget'] = C.WIDGET_RAISE
+                hash_idx = 1
+                while True:
+                    new_kwargs['force_hash'] = "{}_new_instance_{}".format(hash_, hash_idx)
+                    try:
+                        widget = self.getOrCreateWidget(class_, target, *args, **new_kwargs)
+                    except WidgetAlreadyExistsError:
+                        hash_idx += 1
+                    else:
+                        log.debug(u"Widget already exists, a new one has been recreated with hash {}".format(new_kwargs['force_hash']))
+                        break
+            elif callable(on_existing_widget):
+                on_existing_widget(widget)
+            else:
+                raise exceptions.InternalError("Unexpected on_existing_widget value ({})".format(on_existing_widget))
+
+        return widget
+
+    def deleteWidget(self, widget_to_delete):
+        """Delete a widget
+
+        widget's onDelete method will be called before deletion
+        """
+        widget_to_delete.onDelete()
+
+        for widget_map in self._widgets.itervalues():
+            for hash_, widget in widget_map.iteritems():
+                if widget_to_delete is widget:
+                    del widget_map[hash_]
+
+
+class QuickWidget(object):
+    """generic widget base"""
+    SINGLE=True # if True, there can be only one widget per target(s)
+    PROFILES_MULTIPLE=False
+    PROFILES_ALLOW_NONE=False
+
+    def __init__(self, host, target, profiles=None):
+        """
+        @param host: %(doc_host)s
+        @param target: target specific for this widget class
+        @param profiles: can be either:
+            - (unicode): used when widget class manage a unique profile
+            - (iterable): some widget class can manage several profiles, several at once can be specified here
+            - None: no profile is managed by this widget class (rare)
+        @raise: ValueError when (iterable) or None is given to profiles for a widget class which manage one unique profile.
+        """
+        self.host = host
+        self.targets = set()
+        self.addTarget(target)
+        self.profiles = set()
+        if isinstance(profiles, basestring):
+            self.addProfile(profiles)
+        elif profiles is None:
+            if not self.PROFILES_ALLOW_NONE:
+                raise ValueError("profiles can't have a value of None")
+        else:
+            if not self.PROFILES_MULTIPLE:
+                raise ValueError("multiple profiles are not allowed")
+            for profile in profiles:
+                self.addProfile(profile)
+
+    @property
+    def profile(self):
+        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
+        return list(self.profiles)[0]
+
+    def addTarget(self, target):
+        """Add a target if it doesn't already exists
+
+        @param target: target to add
+        """
+        self.targets.add(target)
+
+    def addProfile(self, profile):
+        """Add a profile is if doesn't already exists
+
+        @param profile: profile to add
+        """
+        if self.profiles and not self.PROFILES_MULTIPLE:
+            raise ValueError("multiple profiles are not allowed")
+        self.profiles.add(profile)
+
+    @staticmethod
+    def getWidgetHash(target, profiles):
+        """Return the hash associated with this target for this widget class
+
+        some widget classes can manage several target on the same instance
+        (e.g.: a chat widget with multiple resources on the same bare jid),
+        this method allow to return a hash associated to one or several targets
+        to retrieve the good instance. For example, a widget managing JID targets,
+        and all resource of the same bare jid would return the bare jid as hash.
+
+        @param target: target to check
+        @param profiles: profile(s) associated to target, see __init__ docstring
+        @return: a hash (can correspond to one or many targets or profiles, depending of widget class)
+        """
+        return unicode(target) # by defaut, there is one hash for one target
+
+    def onDelete(self):
+        """Called when a widget is deleted"""
+        log.debug(u"deleting widget {}".format(self)) # Must be implemented by frontends
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/tools/host_listener.py	Wed Mar 18 10:52:28 2015 +0100
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# SAT: a jabber client
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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 is only used launch callbacks when host is ready, used for early initialisation stuffs"""
+
+
+listeners = []
+
+def addListener(cb):
+    """Add a listener which will be called when host is ready
+
+    @param cb: callback which will be called when host is ready with host as only argument
+    """
+    listeners.append(cb)
+
+def callListeners(host):
+    """Must be called by frontend when host is ready.
+
+    The call will launch all the callbacks, then remove the listeners list.
+    @param host(QuickApp): the instancied QuickApp subclass
+    """
+    global listeners
+    while True:
+        try:
+            cb = listeners.pop(0)
+            cb(host)
+        except IndexError:
+            break
+    del listeners
--- a/frontends/src/tools/jid.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/tools/jid.py	Wed Mar 18 10:52:28 2015 +0100
@@ -18,38 +18,94 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-class JID(unicode):
+# hack to use this module with pyjamas
+try:
+    unicode('') # XXX: unicode doesn't exist in pyjamas
+
+    # normal version
+    class BaseJID(unicode):
+        def __new__(cls, jid_str):
+            self = unicode.__new__(cls, cls._normalize(jid_str))
+            return self
+
+        def __init__(self, jid_str):
+            pass
+
+        def _parse(self):
+            """Find node domain and resource"""
+            node_end = self.find('@')
+            if node_end < 0:
+                node_end = 0
+            domain_end = self.find('/')
+            if domain_end == 0:
+               raise ValueError("a jid can't start with '/'")
+            if domain_end == -1:
+                domain_end = len(self)
+            self.node = self[:node_end] or None
+            self.domain = self[(node_end + 1) if node_end else 0:domain_end]
+            self.resource = self[domain_end + 1:] or None
+
+except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
+    # pyjamas version
+    class BaseJID(object):
+        def __init__(self, jid_str):
+            self.__internal_str = JID._normalize(jid_str)
+
+        def __str__(self):
+            return self.__internal_str
+
+        def __getattr__(self, name):
+            return getattr(self.__internal_str, name)
+
+        def __eq__(self, other):
+            if not isinstance(other, JID):
+                return False
+            return (self.node == other.node
+                    and self.domain == other.domain
+                    and self.resource == other.resource)
+
+        def __hash__(self):
+            return hash('JID<{}>'.format(self.__internal_str))
+
+        def find(self, *args):
+            return self.__internal_str.find(*args)
+
+        def _parse(self):
+            """Find node domain and resource"""
+            node_end = self.__internal_str.find('@')
+            if node_end < 0:
+                node_end = 0
+            domain_end = self.__internal_str.find('/')
+            if domain_end == 0:
+                raise ValueError("a jid can't start with '/'")
+            if domain_end == -1:
+                domain_end = len(self.__internal_str)
+            self.node = self.__internal_str[:node_end] or None
+            self.domain = self.__internal_str[(node_end + 1) if node_end else 0:domain_end]
+            self.resource = self.__internal_str[domain_end + 1:] or None
+
+
+class JID(BaseJID):
     """This class help manage JID (Node@Domaine/Resource)"""
 
-    def __new__(cls, jid):
-        self = unicode.__new__(cls, cls.__normalize(jid))
-        self.__parse()
-        return self
+    def __init__(self, jid_str):
+        super(JID, self).__init__(jid_str)
+        self._parse()
 
-    @classmethod
-    def __normalize(cls, jid):
+    @staticmethod
+    def _normalize(jid_str):
         """Naive normalization before instantiating and parsing the JID"""
-        if not jid:
-            return jid
-        tokens = jid.split('/')
+        if not jid_str:
+            return jid_str
+        tokens = jid_str.split('/')
         tokens[0] = tokens[0].lower()  # force node and domain to lower-case
         return '/'.join(tokens)
 
-    def __parse(self):
-        """Find node domain and resource"""
-        node_end = self.find('@')
-        if node_end < 0:
-            node_end = 0
-        domain_end = self.find('/')
-        if domain_end < 1:
-            domain_end = len(self)
-        self.node = self[:node_end]
-        self.domain = self[(node_end + 1) if node_end else 0:domain_end]
-        self.resource = self[domain_end + 1:]
-        if not node_end:
-            self.bare = self
-        else:
-            self.bare = self.node + '@' + self.domain
+    @property
+    def bare(self):
+        if not self.node:
+            return JID(self.domain)
+        return JID(u"{}@{}".format(self.node, self.domain))
 
     def is_valid(self):
         """
--- a/frontends/src/tools/xmlui.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/tools/xmlui.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,7 +20,7 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat_frontends.constants import Const as C
+from sat_frontends.quick_frontend.constants import Const as C
 from sat.core.exceptions import DataError
 
 
@@ -202,7 +202,7 @@
     This class must not be instancied directly
     """
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
         """Initialise the XMLUI instance
 
         @param host: %(doc_host)s
@@ -210,15 +210,18 @@
         @param title: force the title, or use XMLUI one if None
         @param flags: list of string which can be:
             - NO_CANCEL: the UI can't be cancelled
+        @param callback: if present, will be used with launchAction
         """
         self.host = host
         top=parsed_dom.documentElement
         self.session_id = top.getAttribute("session_id") or None
         self.submit_id = top.getAttribute("submit") or None
-        self.title = title or top.getAttribute("title") or u""
+        self.xmlui_title = title or top.getAttribute("title") or u""
         if flags is None:
             flags = []
         self.flags = flags
+        self.callback = callback
+        self.profile = profile
 
     def _isAttrSet(self, name, node):
         """Returnw widget boolean attribute status
@@ -253,7 +256,7 @@
         self._xmluiLaunchAction(self.submit_id, data)
 
     def _xmluiLaunchAction(self, action_id, data):
-        self.host.launchAction(action_id, data, profile_key = self.host.profile)
+        self.host.launchAction(action_id, data, callback=self.callback, profile=self.profile)
 
 
 class XMLUIPanel(XMLUIBase):
@@ -265,8 +268,8 @@
     """
     widget_factory = None
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
-        super(XMLUIPanel, self).__init__(host, parsed_dom, title = None, flags = None)
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
+        super(XMLUIPanel, self).__init__(host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile)
         self.ctrl_list = {}  # usefull to access ctrl
         self._main_cont = None
         self.constructUI(parsed_dom)
@@ -286,7 +289,7 @@
         self._main_cont = value
 
     def _parseChilds(self, _xmlui_parent, current_node, wanted = ('container',), data = None):
-        """Recursively parse childNodes of an elemen
+        """Recursively parse childNodes of an element
 
         @param _xmlui_parent: widget container with '_xmluiAppend' method
         @param current_node: element from which childs will be parsed
@@ -462,7 +465,7 @@
         raise NotImplementedError
 
     def _xmluiSetParam(self, name, value, category):
-        self.host.bridge.setParam(name, value, category, profile_key=self.host.profile)
+        self.host.bridge.setParam(name, value, category, profile_key=self.profile)
 
     ##EVENTS##
 
@@ -639,8 +642,8 @@
 class XMLUIDialog(XMLUIBase):
     dialog_factory = None
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
-        super(XMLUIDialog, self).__init__(host, parsed_dom, title = None, flags = None)
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
+        super(XMLUIDialog, self).__init__(host, parsed_dom, title=None, flags=None, callback=callback, profile=C.PROF_KEY_NONE)
         top=parsed_dom.documentElement
         dlg_elt =  self._getChildNode(top, "dialog")
         if dlg_elt is None:
@@ -654,23 +657,23 @@
         level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO
 
         if dlg_type == C.XMLUI_DIALOG_MESSAGE:
-            self.dlg = self.dialog_factory.createMessageDialog(self, self.title, message, level)
+            self.dlg = self.dialog_factory.createMessageDialog(self, self.xmlui_title, message, level)
         elif dlg_type == C.XMLUI_DIALOG_NOTE:
-            self.dlg = self.dialog_factory.createNoteDialog(self, self.title, message, level)
+            self.dlg = self.dialog_factory.createNoteDialog(self, self.xmlui_title, message, level)
         elif dlg_type == C.XMLUI_DIALOG_CONFIRM:
             try:
                 buttons_elt = self._getChildNode(dlg_elt, "buttons")
                 buttons_set = buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT
             except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
                 buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT
-            self.dlg = self.dialog_factory.createConfirmDialog(self, self.title, message, level, buttons_set)
+            self.dlg = self.dialog_factory.createConfirmDialog(self, self.xmlui_title, message, level, buttons_set)
         elif dlg_type == C.XMLUI_DIALOG_FILE:
             try:
                 file_elt = self._getChildNode(dlg_elt, "file")
                 filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT
             except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
                 filetype = C.XMLUI_DATA_FILETYPE_DEFAULT
-            self.dlg = self.dialog_factory.createFileDialog(self, self.title, message, level, filetype)
+            self.dlg = self.dialog_factory.createFileDialog(self, self.xmlui_title, message, level, filetype)
         else:
             raise ValueError("Unknown dialog type [%s]" % dlg_type)
 
@@ -690,9 +693,9 @@
     class_map[type_] = class_
 
 
-def create(host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None):
+def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, callback=None, profile=C.PROF_KEY_NONE):
     """
-        @param dom_parse: methode equivalent to minidom.parseString (but which  must manage unicode), or None to use default one
+        @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one
         @param dom_free: method used to free the parsed DOM
     """
     if dom_parse is None:
@@ -713,6 +716,6 @@
     except KeyError:
         raise ClassNotRegistedError(_("You must register classes with registerClass before creating a XMLUI"))
 
-    xmlui = cls(host, parsed_dom, title, flags)
+    xmlui = cls(host, parsed_dom, title, flags, callback, profile)
     dom_free(parsed_dom)
     return xmlui
--- a/frontends/src/wix/card_game.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,261 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core.i18n import _
-import wx
-import os.path, glob
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.games import TarotCard
-from sat_frontends.quick_frontend.quick_card_game import QuickCardGame
-from sat_frontends.wix import xmlui
-
-CARD_WIDTH = 74
-CARD_HEIGHT = 136
-MIN_WIDTH = 950 #Minimum size of the panel
-MIN_HEIGHT = 500
-
-
-class WxCard(TarotCard):
-    """This class is used to represent a card, graphically and logically"""
-
-    def __init__(self, file):
-        """@param file: path of the PNG file"""
-        self.bitmap = wx.Image(file).ConvertToBitmap()
-        root_name = os.path.splitext(os.path.basename(file))[0]
-        suit,value = root_name.split('_')
-        TarotCard.__init__(self, (suit, value))
-        log.debug("Card: %s %s" % (suit, value)) #, self.bout
-
-    def draw(self, dc, x, y):
-        """Draw the card on the device context
-        @param dc: device context
-        @param x: abscissa
-        @param y: ordinate"""
-        dc.DrawBitmap(self.bitmap, x, y, True)
-
-
-class CardPanel(QuickCardGame, wx.Panel):
-    """This class is used to display the cards"""
-
-    def __init__(self, parent, referee, players, player_nick):
-        QuickCardGame.__init__(self, parent, referee, players, player_nick)
-        wx.Panel.__init__(self, parent)
-        self.SetMinSize(wx.Size(MIN_WIDTH, MIN_HEIGHT))
-        self.loadCards(os.path.join(self.parent.host.media_dir, 'games/cards/tarot'))
-        self.mouse_over_card = None #contain the card to highlight
-        self.visible_size = CARD_WIDTH/2 #number of pixels visible for cards
-        self.hand = []
-        self.to_show = []
-        self.state = None
-        self.SetBackgroundColour(wx.GREEN)
-        self.Bind(wx.EVT_SIZE, self.onResize)
-        self.Bind(wx.EVT_PAINT, self.onPaint)
-        self.Bind(wx.EVT_MOTION, self.onMouseMove)
-        self.Bind(wx.EVT_LEFT_UP, self.onMouseClick)
-
-        self.parent.host.bridge.tarotGameReady(player_nick, referee, self.parent.host.profile)
-
-    def loadCards(self, dir):
-        """Load all the cards in memory
-        @param dir: directory where the PNG files are"""
-        QuickCardGame.loadCards(self)
-        for file in glob.glob(dir+'/*_*.png'):
-            card = WxCard(file)
-            self.cards[card.suit, card.value]=card
-            self.deck.append(card)
-
-    def newGame(self, hand):
-        """Start a new game, with given hand"""
-        if hand is []:  # reset the display after the scores have been showed
-            self.resetRound()
-            self.Refresh()
-            self.parent.host.bridge.tarotGameReady(self.player_nick, self.referee, self.parent.host.profile)
-            return
-        QuickCardGame.newGame(self, hand)
-        self._recalc_ori()
-        self.Refresh()
-
-    def contratSelected(self, data):
-        """Called when the contrat has been choosed
-        @param data: form result"""
-        log.debug (_("Contrat choosed"))
-        contrat = data[0][1]
-        QuickCardGame.contratSelected(self, contrat)
-
-    def chooseContrat(self, xml_data):
-        """Called when the player has to select his contrat
-        @param xml_data: SàT xml representation of the form"""
-        xmlui.create(self.parent.host, xml_data, title=_('Please choose your contrat'), flags=['NO_CANCEL'])
-
-    def showScores(self, xml_data, winners, loosers):
-        """Called when the round is over, display the scores
-        @param xml_data: SàT xml representation of the form"""
-        if not winners and not loosers:
-            title = _("Draw game")
-        else:
-            title = _('You win \o/') if self.player_nick in winners else _('You loose :(')
-        xmlui.create(self.parent.host, xml_data, title=title, flags=['NO_CANCEL'])
-
-    def cardsPlayed(self, player, cards):
-        """A card has been played by player"""
-        QuickCardGame.cardsPlayed(self, player, cards)
-        self.Refresh()
-
-    def invalidCards(self, phase, played_cards, invalid_cards):
-        """Invalid cards have been played
-        @param phase: phase of the game
-        @param played_cards: all the cards played
-        @param invalid_cards: cards which are invalid"""
-        QuickCardGame.invalidCards(self, phase, played_cards, invalid_cards)
-
-        self._recalc_ori()
-        self.Refresh()
-        if self._autoplay==None: #No dialog if there is autoplay
-            wx.MessageDialog(self, _("Cards played are invalid !"), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal()
-
-    def _is_on_hand(self, pos_x, pos_y):
-        """Return True if the coordinate are on the hand cards"""
-        if pos_x > self.orig_x and pos_y > self.orig_y \
-           and pos_x < self.orig_x + (len(self.hand)+1) * self.visible_size \
-           and pos_y < self.end_y:
-           return True
-        return False
-
-    def onResize(self, event):
-        self._recalc_ori()
-
-    def _recalc_ori(self):
-        """Recalculate origins of hand, must be call when hand size change"""
-        self.orig_x = (self.GetSizeTuple()[0]-(len(self.hand)+1)*self.visible_size)/2 #where we start to draw cards
-        self.orig_y = self.GetSizeTuple()[1] - CARD_HEIGHT - 20
-        self.end_y = self.orig_y + CARD_HEIGHT
-
-    def onPaint(self, event):
-        dc = wx.PaintDC(self)
-
-        #We print the names to know who play where TODO: print avatars when available
-        max_x, max_y = self.GetSize()
-        border = 10 #border between nick and end of panel
-        right_y = left_y = 200
-        right_width, right_height = dc.GetTextExtent(self.right_nick)
-        right_x = max_x - right_width - border
-        left_x = border
-        top_width, top_height = dc.GetTextExtent(self.top_nick)
-        top_x = (max_x - top_width) / 2
-        top_y = border
-        dc.DrawText(self.right_nick, right_x, right_y)
-        dc.DrawText(self.top_nick, top_x, top_y)
-        dc.DrawText(self.left_nick, left_x, left_y)
-
-        #We draw the played cards:
-        center_y = 200 #ordinate used as center point
-        left_x = (max_x - CARD_WIDTH)/2 - CARD_WIDTH - 5
-        right_x = (max_x/2) + (CARD_WIDTH/2) + 5
-        left_y = right_y = center_y - CARD_HEIGHT/2
-        top_x = bottom_x = (max_x - CARD_WIDTH)/2
-        top_y = center_y - CARD_HEIGHT - 5
-        bottom_y = center_y + 5
-        for side in ['left', 'top', 'right', 'bottom']:
-            card = self.played[getattr(self, side+'_nick')]
-            if card != None:
-                card.draw(dc,locals()[side+'_x'], locals()[side+'_y'])
-
-        x=self.orig_x
-        for card in self.hand:
-            if (self.state == "play" or self.state == "ecart") and card == self.mouse_over_card \
-                or self.state == "ecart" and card in self.selected:
-                y = self.orig_y - 30
-            else:
-                y = self.orig_y
-
-            card.draw(dc,x,y)
-            x+=self.visible_size
-
-        if self.to_show:
-            """There are cards to display in the middle"""
-            size = len(self.to_show)*(CARD_WIDTH+10)-10
-            x = (max_x - size)/2
-            for card in self.to_show:
-                card.draw(dc, x, 150)
-                x+=CARD_WIDTH+10
-
-    def onMouseMove(self, event):
-        pos_x,pos_y = event.GetPosition()
-        if self._is_on_hand(pos_x, pos_y):
-           try:
-               self.mouse_over_card = self.hand[(pos_x-self.orig_x)/self.visible_size]
-           except IndexError:
-               self.mouse_over_card = self.hand[-1]
-           self.Refresh()
-        else:
-            self.mouse_over_card = None
-            self.Refresh()
-
-    def onMouseClick(self, event):
-        log.debug("mouse click: %s" % event.GetPosition())
-        pos_x,pos_y = event.GetPosition()
-
-        if self.state == "chien":
-            self.to_show = []
-            self.state = "wait"
-            return
-        elif self.state == "wait_for_ecart":
-            self.state = "ecart"
-            self.hand.extend(self.to_show)
-            self.hand.sort()
-            self.to_show = []
-            self._recalc_ori()
-            self.Refresh()
-            return
-
-        if self._is_on_hand(pos_x, pos_y):
-           idx = (pos_x-self.orig_x)/self.visible_size
-           if idx == len(self.hand):
-               idx-=1
-           if self.hand[idx] == self.mouse_over_card:
-               if self.state == "ecart":
-                   if self.hand[idx] in self.selected:
-                       self.selected.remove(self.hand[idx])
-                   else:
-                       self.selected.append(self.hand[idx])
-                       if len(self.selected) == 6: #TODO: use variable here, as chien len can change with variants
-                           dlg = wx.MessageDialog(self, _("Do you put these cards in chien ?"), _(u"Écart"), wx.YES_NO | wx.ICON_QUESTION)
-                           answer = dlg.ShowModal()
-                           if answer == wx.ID_YES:
-                               ecart = []
-                               for card in self.selected:
-                                   ecart.append((card.suit, card.value))
-                                   self.hand.remove(card)
-                               del self.selected[:]
-                               self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, self.parent.host.profile)
-                               self.state = "wait"
-
-                   self._recalc_ori()
-                   self.Refresh()
-               if self.state == "play":
-                   card = self.hand[idx]
-                   self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], self.parent.host.profile)
-                   del self.hand[idx]
-                   self.state = "wait"
-                   self._recalc_ori()
-                   self.Refresh()
-
-
--- a/frontends/src/wix/chat.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,289 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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_frontends.wix.constants import Const as C
-import wx
-import os.path
-import time
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.jid  import JID
-from sat_frontends.quick_frontend.quick_chat import QuickChat
-from sat_frontends.wix.contact_list import ContactList
-from sat_frontends.wix.card_game import CardPanel
-from sat_frontends.wix.quiz_game import QuizPanel
-
-idSEND           = 1
-idTAROT          = 2
-
-
-class Chat(wx.Frame, QuickChat):
-    """The chat Window for one to one conversations"""
-
-    def __init__(self, target, host, type_='one2one'):
-        wx.Frame.__init__(self, None, title=target, pos=(0,0), size=(400,200))
-        QuickChat.__init__(self, target, host, type_)
-
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.SetSizer(self.sizer)
-
-        self.splitter = wx.SplitterWindow(self, -1)
-        self.sizer.Add(self.splitter, 1, flag = wx.EXPAND)
-
-        self.conv_panel = wx.Panel(self.splitter)
-        self.conv_panel.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.subjectBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_READONLY)
-        self.chatWindow = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_MULTILINE | wx.TE_RICH | wx.TE_READONLY)
-        self.textBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_PROCESS_ENTER)
-        self.conv_panel.sizer.Add(self.subjectBox, flag=wx.EXPAND)
-        self.conv_panel.sizer.Add(self.chatWindow, 1, flag=wx.EXPAND)
-        self.conv_panel.sizer.Add(self.textBox, 0, flag=wx.EXPAND)
-        self.conv_panel.SetSizer(self.conv_panel.sizer)
-        self.splitter.Initialize(self.conv_panel)
-        self.SetMenuBar(wx.MenuBar())
-
-        #events
-        self.Bind(wx.EVT_CLOSE, self.onClose, self)
-        self.Bind(wx.EVT_TEXT_ENTER, self.onEnterPressed, self.textBox)
-
-        #fonts
-        self.font={}
-        self.font["points"] = self.chatWindow.GetFont().GetPointSize()
-        self.font["family"] = self.chatWindow.GetFont().GetFamily()
-
-
-        #misc
-        self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00  %Y")) #struct_time of day changing time
-        self.setType(self.type)
-        self.textBox.SetFocus()
-        self.Hide() #We hide because of the show toggle
-
-    def __createPresents(self):
-        """Create a list of present people in a group chat"""
-        self.present_panel = wx.Panel(self.splitter)
-        self.present_panel.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.present_panel.presents = ContactList(self.present_panel, self.host, type_='nicks')
-        self.present_panel.presents.SetMinSize(wx.Size(80,20))
-        self.present_panel.sizer.Add(self.present_panel.presents, 1, wx.EXPAND)
-        self.present_panel.SetSizer(self.present_panel.sizer)
-        self.splitter.SplitVertically(self.present_panel, self.conv_panel, 80)
-
-    def setType(self, type_):
-        QuickChat.setType(self, type_)
-        if type_ is 'group' and not self.splitter.IsSplit():
-            self.__createPresents()
-            self.subjectBox.Show()
-            self.__eraseMenus()
-            self.__createMenus_group()
-            self.sizer.Layout()
-        elif type_ is 'one2one' and self.splitter.IsSplit():
-            self.splitter.Unsplit(self.present_panel)
-            del self.present_panel
-            self.GetMenuBar().Show()
-            self.subjectBox.Hide()
-            self.__eraseMenus()
-            self.__createMenus_O2O()
-            self.nick = None
-        else:
-            self.subjectBox.Hide()
-            self.__eraseMenus()
-            self.__createMenus_O2O()
-            self.historyPrint(profile=self.host.profile)
-
-    def startGame(self, game_type, referee, players):
-        """Configure the chat window to start a game"""
-        if game_type=="Tarot":
-            log.debug (_("configure chat window for Tarot game"))
-            self.tarot_panel = CardPanel(self, referee, players, self.nick)
-            self.sizer.Prepend(self.tarot_panel, 0, flag=wx.EXPAND)
-            self.sizer.Layout()
-            self.Fit()
-            self.splitter.UpdateSize()
-        elif game_type=="Quiz":
-            log.debug (_("configure chat window for Quiz game"))
-            self.quiz_panel = QuizPanel(self, referee, players, self.nick)
-            self.sizer.Prepend(self.quiz_panel, 0, flag=wx.EXPAND)
-            self.sizer.Layout()
-            self.Fit()
-            self.splitter.UpdateSize()
-
-    def getGame(self, game_type):
-        """Return class managing the game type"""
-        #TODO: check that the game is launched, and manage errors
-        if game_type=="Tarot":
-            return self.tarot_panel
-        elif game_type=="Quiz":
-            return self.quiz_panel
-
-    def setPresents(self, nicks):
-        """Set the users presents in the contact list for a group chat
-        @param nicks: list of nicknames
-        """
-        QuickChat.setPresents(self, nicks)
-        for nick in nicks:
-            self.present_panel.presents.replace(nick)
-
-    def replaceUser(self, nick, show_info=True):
-        """Add user if it is not in the group list"""
-        log.debug (_("Replacing user %s") % nick)
-        if self.type != "group":
-            log.error (_("[INTERNAL] trying to replace user for a non group chat window"))
-            return
-        QuickChat.replaceUser(self, nick, show_info)
-        self.present_panel.presents.replace(nick)
-
-    def removeUser(self, nick, show_info=True):
-        """Remove a user from the group list"""
-        QuickChat.removeUser(self, nick, show_info)
-        self.present_panel.presents.remove(nick)
-
-    def setSubject(self, subject):
-        """Set title for a group chat"""
-        QuickChat.setSubject(self, subject)
-        self.subjectBox.SetValue(subject)
-
-    def __eraseMenus(self):
-        """erase all menus"""
-        menuBar = self.GetMenuBar()
-        for i in range(menuBar.GetMenuCount()):
-            menuBar.Remove(i)
-
-    def __createMenus_O2O(self):
-        """create menu bar for one 2 one chat"""
-        log.info("Creating menus")
-        self.__eraseMenus()
-        menuBar = self.GetMenuBar()
-        actionMenu = wx.Menu()
-        actionMenu.Append(idSEND, _("&SendFile	CTRL-s"),_(" Send a file to contact"))
-        menuBar.Append(actionMenu,_("&Action"))
-        self.host.addMenus(menuBar, C.MENU_SINGLE, {'jid': self.target})
-
-        #events
-        wx.EVT_MENU(self, idSEND, self.onSendFile)
-
-    def __createMenus_group(self):
-        """create menu bar for group chat"""
-        log.info("Creating menus")
-        self.__eraseMenus()
-        menuBar = self.GetMenuBar()
-        actionMenu = wx.Menu()
-        actionMenu.Append(idTAROT, _("Start &Tarot game	CTRL-t"),_(" Start a Tarot card game")) #tmp
-        menuBar.Append(actionMenu,_("&Games"))
-        self.host.addMenus(menuBar, C.MENU_ROOM, {'room_jid': self.target.bare})
-
-        #events
-        wx.EVT_MENU(self, idTAROT, self.onStartTarot)
-
-    def __del__(self):
-        wx.Frame.__del__(self)
-
-    def onClose(self, event):
-        """Close event: we only hide the frame."""
-        event.Veto()
-        self.Hide()
-
-    def onEnterPressed(self, event):
-        """Behaviour when enter pressed in send line."""
-        self.host.sendMessage(self.target.bare if self.type == 'group' else self.target,
-                              event.GetString(),
-                              mess_type="groupchat" if self.type == 'group' else "chat",
-                              profile_key=self.host.profile)
-        self.textBox.Clear()
-
-    def __blink(self):
-        """Do wizzz and buzzz to show window to user or
-        at least inform him of something new"""
-        #TODO: use notification system
-        if not self.IsActive():
-            self.RequestUserAttention()
-        if not self.IsShown():
-            self.Show()
-
-    def printMessage(self, from_jid, msg, profile, timestamp=None):
-        """Print the message with differents colors depending on where it comes from."""
-        try:
-            jid,nick,mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp)
-        except TypeError:
-            return
-        log.debug("printMessage, jid = %s type = %s" % (jid, self.type))
-        _font_bold = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.BOLD)
-        _font_normal = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.NORMAL)
-        _font_italic = wx.Font(self.font["points"], self.font["family"], wx.ITALIC if mymess else wx.NORMAL, wx.NORMAL)
-        self.chatWindow.SetDefaultStyle(wx.TextAttr("GREY", font=_font_normal))
-        msg_time = time.localtime(timestamp or None)
-        time_format = "%c" if msg_time < self.day_change else "%H:%M" #if the message was sent before today, we print the full date
-        self.chatWindow.AppendText("[%s]" % time.strftime(time_format, msg_time ))
-        self.chatWindow.SetDefaultStyle(wx.TextAttr( "BLACK" if mymess else "BLUE", font=_font_bold))
-        self.chatWindow.AppendText("[%s] " % nick)
-        self.chatWindow.SetDefaultStyle(wx.TextAttr("BLACK", font=_font_italic))
-        self.chatWindow.AppendText("%s\n" % msg)
-        if not mymess:
-            self.__blink()
-
-    def printInfo(self, msg, type_='normal', timestamp=None):
-        """Print general info
-        @param msg: message to print
-        @type_: one of:
-            normal: general info like "toto has joined the room"
-            me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
-        @param timestamp (float): number of seconds since epoch
-        """
-        _font_bold = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.BOLD)
-        _font_normal = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.NORMAL)
-        self.chatWindow.SetDefaultStyle(wx.TextAttr("BLACK", font=_font_bold if type_ == 'normal' else _font_normal))
-        self.chatWindow.AppendText("%s\n" % msg)
-        if type_=="me":
-            self.__blink()
-
-    ### events ###
-
-    def onSendFile(self, e):
-        log.debug(_("Send File"))
-        filename = wx.FileSelector(_("Choose a file to send"), flags = wx.FD_FILE_MUST_EXIST)
-        if filename:
-            log.debug(_("filename: %s"),filename)
-            #FIXME: check last_resource: what if self.target.resource exists ?
-            last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile)
-            if last_resource:
-                full_jid = JID("%s/%s" % (self.target.bare, last_resource))
-            else:
-                full_jid = self.target
-            id = self.host.bridge.sendFile(full_jid, filename, {}, self.host.profile)
-            self.host.waitProgress(id, _("File Transfer"), _("Copying %s") % os.path.basename(filename), self.host.profile)
-
-    def onStartTarot(self, e):
-        log.debug(_("Starting Tarot game"))
-        log.warning(_("FIXME: temporary menu, must be changed"))
-        if len(self.occupants) != 4:
-            err_dlg = wx.MessageDialog(self, _("You need to be exactly 4 peoples in the room to start a Tarot game"), _("Can't start game"), style = wx.OK | wx.ICON_ERROR) #FIXME: gof: temporary only, need to choose the people with who the game has to be started
-            err_dlg.ShowModal()
-        else:
-            self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile)
-
-    def updateChatState(self, state, nick=None):
-        """Set the chat state (XEP-0085) of the contact. Leave nick to None
-        to set the state for a one2one conversation, or give a nickname or
-        Const.ALL_OCCUPANTS to set the state of a participant within a MUC.
-        @param state: the new chat state
-        @param nick: None for one2one, the MUC user nick or Const.ALL_OCCUPANTS
-        """
-        #TODO: chat states not implemented yet
-        pass
--- a/frontends/src/wix/constants.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat.core.i18n import _
-import os.path
-import sat_frontends.wix
-from sat_frontends.quick_frontend import constants
-
-
-wix_root = os.path.dirname(sat_frontends.wix.__file__)
-
-
-class Const(constants.Const):
-
-    APP_NAME = "Wix"
-    LICENCE_PATH = os.path.join(wix_root, "COPYING")
-    msgOFFLINE = _("offline")
-    msgONLINE = _("online")
-    DEFAULT_GROUP = "Unclassed"
-    PRESENCE = [("", _("Online"), None),
-                ("chat", _("Free for chat"), "green"),
-                ("away", _("AFK"), "brown"),
-                ("dnd", _("DND"), "red"),
-                ("xa", _("Away"), "red")
-                ]
-    LOG_OPT_SECTION = APP_NAME.lower()
--- a/frontends/src/wix/contact_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,251 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat.core.i18n import _
-import wx
-from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
-from sat_frontends.wix.constants import Const
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from cgi import escape
-from sat_frontends.tools.jid  import JID
-from os.path import join
-
-
-class Group(unicode):
-    """Class used to recognize groups"""
-
-class Contact(unicode):
-    """Class used to recognize groups"""
-
-class ContactList(wx.SimpleHtmlListBox, QuickContactList):
-    """Customized control to manage contacts."""
-
-    def __init__(self, parent, host, type_="JID"):
-        """init the contact list
-        @param parent: WxWidgets parent of the widget
-        @param host: wix main app class
-        @param type_: type of contact list: "JID" for the usual big jid contact list
-                                           "CUSTOM" for a customized contact list (self._presentItem must then be overrided)
-        """
-        wx.SimpleHtmlListBox.__init__(self, parent, -1)
-        QuickContactList.__init__(self)
-        self.host = host
-        self.type = type_
-        self.__typeSwitch()
-        self.groups = {}  #list contacts in each groups, key = group
-        self.empty_avatar = join(host.media_dir, 'misc/empty_avatar')
-        self.Bind(wx.EVT_LISTBOX, self.onSelected)
-        self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated)
-
-    def __contains__(self, jid):
-        return bool(self.__find_idx(jid))
-
-    def __typeSwitch(self):
-        if self.type == "JID":
-            self._presentItem = self._presentItemJID
-        elif self.type != "CUSTOM":
-            self._presentItem = self._presentItemDefault
-
-    def __find_idx(self, entity):
-        """Find indexes of given contact (or groups) in contact list, manage jid
-        @return: list of indexes"""
-        result=[]
-        for i in range(self.GetCount()):
-            if (type(entity) == JID and type(self.GetClientData(i)) == JID and self.GetClientData(i).bare == entity.bare) or\
-                self.GetClientData(i) == entity:
-                result.append(i)
-        return result
-
-    def update_jid(self, jid):
-        self.replace(jid)
-
-    def replace(self, contact, groups=None, attributes=None):
-        """Add a contact to the list if doesn't exist, else update it.
-
-        This method can be called with groups=None for the purpose of updating
-        the contact's attributes (e.g. nickname). In that case, the groups
-        attribute must not be set to the default group but ignored. If not,
-        you may move your contact from its actual group(s) to the default one.
-
-        None value for 'groups' has a different meaning than [None] which is for the default group.
-
-        @param jid (JID)
-        @param groups (list): list of groups or None to ignore the groups membership.
-        @param attributes (dict)
-        """
-        log.debug(_("update %s") % contact)
-        if not self.__find_idx(contact):
-            self.add(contact, groups)
-        else:
-            for i in self.__find_idx(contact):
-                _present = self._presentItem(contact)
-                if _present != None:
-                    self.SetString(i, _present)
-
-    def __eraseGroup(self, group):
-        """Erase all contacts in group
-        @param group: group to erase
-        @return: True if something as been erased"""
-        erased = False
-        indexes = self.__find_idx(group)
-        for idx in indexes:
-            while idx<self.GetCount()-1 and type(self.GetClientData(idx+1)) != Group:
-                erased = True
-                self.Delete(idx+1)
-        return erased
-
-
-    def _presentGroup(self, group):
-        """Make a nice presentation for the contact groups"""
-        html = u"""-- [%s] --""" % group
-
-        return html
-
-    def _presentItemDefault(self, contact):
-        """Make a basic presentation of string contacts in the list."""
-        return contact
-
-    def _presentItemJID(self, jid):
-        """Make a nice presentation of the contact in the list for JID contacts."""
-        name = self.getCache(jid,'name')
-        nick = self.getCache(jid,'nick')
-        _show = self.getCache(jid,'show')
-        if _show == None or _show == 'unavailable':
-            return None
-        show = [x for x in Const.PRESENCE if x[0] == _show][0]
-
-        #show[0]==shortcut
-        #show[1]==human readable
-        #show[2]==color (or None)
-        show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else ""
-        status = self.getCache(jid,'status') or ''
-        avatar = self.getCache(jid,'avatar') or self.empty_avatar #XXX: there is a weird bug here: if the image has an extension (i.e. empty_avatar.png),
-        #WxPython segfault, and it doesn't without nothing. I couldn't reproduce the case with a basic test script, so it need further investigation before reporting it
-        #to WxPython dev. Anyway, the program crash with a segfault, not a python exception, so there is definitely something wrong with WxPython.
-        #The case seems to happen when SimpleHtmlListBox parse the HTML with the <img> tag
-
-        html = """
-        <table border='0'>
-        <td>
-            <img  height='64' width='64' src='%s' />
-        </td>
-        <td>
-            <b>%s</b> %s<br />
-            <i>%s</i>
-        </td>
-        </table>
-        """ % (avatar,
-               escape(nick or name or jid.node or jid.bare),
-               show_html,
-               escape(status))
-
-        return html
-
-    def clearContacts(self):
-        """Clear all the contact list"""
-        self.Clear()
-
-    def add(self, contact, groups = None):
-        """add a contact to the list"""
-        log.debug (_("adding %s"),contact)
-        if not groups:
-            _present =  self._presentItem(contact)
-            if _present:
-                idx = self.Insert(_present, 0, contact)
-        else:
-            for group in groups:
-                indexes = self.__find_idx(group)
-                gp_idx = 0
-                if not indexes:  #this is a new group, we have to create it
-                    gp_idx = self.Append(self._presentGroup(group), Group(group))
-                else:
-                    gp_idx = indexes[0]
-
-                _present = self._presentItem(contact)
-                if _present:
-                    self.Insert(_present, gp_idx+1, contact)
-
-    def setSpecial(self, special_jid, special_type, show=False):
-        """Set entity as a special
-        @param jid: jid of the entity
-        @param _type: special type (e.g.: "MUC")
-        @param show: True to display the dialog to chat with this entity
-        """
-        QuickContactList.setSpecial(self, special_jid, special_type, show)
-        if show:
-            self._showDialog(special_jid)
-
-    def _showDialog(self, jid):
-        """Show the dialog associated to the given jid."""
-        indexes = self.__find_idx(jid)
-        if not indexes:
-            return
-        self.DeselectAll()
-        self.SetSelection(indexes[0])
-        self.onActivated(wx.MouseEvent())
-
-    def remove(self, contact):
-        """remove a contact from the list"""
-        log.debug (_("removing %s"), contact)
-        list_idx = self.__find_idx(contact)
-        list_idx.reverse()  #as we make some deletions, we have to reverse the order
-        for i in list_idx:
-            self.Delete(i)
-
-    def onSelected(self, event):
-        """Called when a contact is selected."""
-        data = self.getSelection()
-        if data == None: #we have a group
-            first_visible = self.GetVisibleBegin()
-            group = self.GetClientData(self.GetSelection())
-            erased = self.__eraseGroup(group)
-            if not erased: #the group was already erased, we can add again the contacts
-                contacts = [JID(contact) for contact in self.host.bridge.getContactsFromGroup(group, self.host.profile)]
-                contacts.sort()
-                id_insert = self.GetSelection()+1
-                for contact in contacts:
-                    _present =  self._presentItem(contact)
-                    if _present:
-                        self.Insert(_present, id_insert, contact)
-            self.SetSelection(wx.NOT_FOUND)
-            self.ScrollToLine(first_visible)
-            event.Skip(False)
-        else:
-            event.Skip()
-
-    def onActivated(self, event):
-        """Called when a contact is clicked or activated with keyboard."""
-        data = self.getSelection()
-        self.onActivatedCB(data)
-        event.Skip()
-
-    def getSelection(self):
-        """Return the selected contact, or an empty string if there is not"""
-        if self.GetSelection() == wx.NOT_FOUND:
-            return None
-        data = self.GetClientData(self.GetSelection())
-        if type(data) == Group:
-            return None
-        return data
-
-    def registerActivatedCB(self, cb):
-        """Register a callback with manage contact activation."""
-        self.onActivatedCB=cb
-
--- a/frontends/src/wix/main_window.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,498 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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_frontends.wix.constants import Const as C
-from sat_frontends.quick_frontend.quick_chat_list import QuickChatList
-from sat_frontends.quick_frontend.quick_app import QuickApp
-import wx
-from sat_frontends.wix.contact_list import ContactList
-from sat_frontends.wix.chat import Chat
-from sat_frontends.wix import xmlui
-from sat_frontends.wix.profile import Profile
-from sat_frontends.wix.profile_manager import ProfileManager
-import os.path
-from sat_frontends.tools.jid  import JID
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.wix.constants import Const
-
-idCONNECT,\
-idDISCONNECT,\
-idEXIT,\
-idABOUT,\
-idPARAM,\
-idSHOW_PROFILE,\
-idJOIN_ROOM,\
- = range(7)
-
-class ChatList(QuickChatList):
-    """This class manage the list of chat windows"""
-
-    def createChat(self, target):
-        return Chat(target, self.host)
-
-class MainWindow(wx.Frame, QuickApp):
-    """main app window"""
-
-    def __init__(self):
-        QuickApp.__init__(self)
-        wx.Frame.__init__(self,None, title="SàT Wix", size=(350,500))
-
-        #sizer
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.SetSizer(self.sizer)
-
-        #Frame elements
-        self.contact_list = ContactList(self, self)
-        self.contact_list.registerActivatedCB(self.onContactActivated)
-        self.contact_list.Hide()
-        self.sizer.Add(self.contact_list, 1, flag=wx.EXPAND)
-
-        self.chat_wins=ChatList(self)
-        self.CreateStatusBar()
-
-        #ToolBar
-        self.tools=self.CreateToolBar()
-        self.statusBox = wx.ComboBox(self.tools, -1, "Online", choices=[status[1] for status in Const.PRESENCE],
-                                      style=wx.CB_DROPDOWN | wx.CB_READONLY)
-        self.tools.AddControl(self.statusBox)
-        self.tools.AddSeparator()
-        self.statusTxt = wx.TextCtrl(self.tools, -1, style=wx.TE_PROCESS_ENTER)
-        self.tools.AddControl(self.statusTxt)
-        self.Bind(wx.EVT_COMBOBOX, self.onStatusChange, self.statusBox)
-        self.Bind(wx.EVT_TEXT_ENTER, self.onStatusChange, self.statusTxt)
-        self.tools.Disable()
-
-        #tray icon
-        ticon = wx.Icon(os.path.join(self.media_dir, 'icons/crystal/32/tray_icon.xpm'), wx.BITMAP_TYPE_XPM)
-        self.tray_icon = wx.TaskBarIcon()
-        if ticon.IsOk():
-            self.tray_icon.SetIcon(ticon, _("Wix jabber client"))
-        wx.EVT_TASKBAR_LEFT_UP(self.tray_icon, self.onTrayClick)
-
-
-        #events
-        self.Bind(wx.EVT_CLOSE, self.onClose, self)
-
-
-        #profile panel
-        self.profile_pan = ProfileManager(self)
-        self.sizer.Add(self.profile_pan, 1, flag=wx.EXPAND)
-
-        self.postInit()
-
-        self.Show()
-
-    def plug_profile_1(self, profile_key='@DEFAULT@'):
-        """Hide profile panel then plug profile"""
-        log.debug (_('plugin profile %s' % profile_key))
-        self.profile_pan.Hide()
-        self.contact_list.Show()
-        self.sizer.Layout()
-        super(MainWindow, self).plug_profile_1(profile_key)
-        #menus
-        self.createMenus()
-
-    def addMenus(self, menubar, type_, menu_data=None):
-        """Add cached menus to instance
-        @param menu: wx.MenuBar instance
-        @param type_: menu type like is sat.core.sat_main.importMenu
-        @param menu_data: data to send with these menus
-
-        """
-        menus = self.profiles[self.profile]['menus'].get(type_,[])
-        for id_, path, path_i18n  in menus:
-            if len(path) != 2:
-                raise NotImplementedError("Menu with a path != 2 are not implemented yet")
-            category = path_i18n[0] # TODO: manage path with more than 2 levels
-            name = path_i18n[1]
-            menu_idx = menubar.FindMenu(category)
-            current_menu = None
-            if menu_idx == wx.NOT_FOUND:
-                #the menu is new, we create it
-                current_menu = wx.Menu()
-                menubar.Append(current_menu, category)
-            else:
-                current_menu = menubar.GetMenu(menu_idx)
-            assert(current_menu != None)
-            item_id = wx.NewId()
-            help_string = self.bridge.getMenuHelp(id_, '')
-            current_menu.Append(item_id, name, help=help_string)
-            #now we register the event
-            def event_answer(e, id_=id_):
-                self.launchAction(id_, menu_data, profile_key = self.profile)
-
-            wx.EVT_MENU(menubar.Parent, item_id, event_answer)
-
-    def createMenus(self):
-        log.info(_("Creating menus"))
-        connectMenu = wx.Menu()
-        connectMenu.Append(idCONNECT, _("&Connect	CTRL-c"),_(" Connect to the server"))
-        connectMenu.Append(idDISCONNECT, _("&Disconnect	CTRL-d"),_(" Disconnect from the server"))
-        connectMenu.Append(idPARAM,_("&Parameters"),_(" Configure the program"))
-        connectMenu.AppendSeparator()
-        connectMenu.Append(idABOUT, _("A&bout"), _(" About %s") % Const.APP_NAME)
-        connectMenu.Append(idEXIT,_("E&xit"),_(" Terminate the program"))
-        contactMenu = wx.Menu()
-        communicationMenu = wx.Menu()
-        communicationMenu.Append(idJOIN_ROOM, _("&Join Room"),_(" Join a Multi-User Chat room"))
-        self.menuBar = wx.MenuBar()
-        self.menuBar.Append(connectMenu,_("&General"))
-        self.menuBar.Append(contactMenu,_("&Contacts"))
-        self.menuBar.Append(communicationMenu,_("&Communication"))
-        self.SetMenuBar(self.menuBar)
-
-        #additionals menus
-        #FIXME: do this in a more generic way (in quickapp)
-        self.addMenus(self.menuBar, C.MENU_GLOBAL)
-
-        # menu items that should be displayed after the automatically added ones
-        contactMenu.AppendSeparator()
-        contactMenu.Append(idSHOW_PROFILE, _("&Show profile"), _(" Show contact's profile"))
-
-        #events
-        wx.EVT_MENU(self, idCONNECT, self.onConnectRequest)
-        wx.EVT_MENU(self, idDISCONNECT, self.onDisconnectRequest)
-        wx.EVT_MENU(self, idPARAM, self.onParam)
-        wx.EVT_MENU(self, idABOUT, self.onAbout)
-        wx.EVT_MENU(self, idEXIT, self.onExit)
-        wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile)
-        wx.EVT_MENU(self, idJOIN_ROOM, self.onJoinRoom)
-
-    def newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile):
-        QuickApp.newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile)
-
-    def showAlert(self, message):
-        # TODO: place this in a separate class
-        popup=wx.PopupWindow(self)
-        ### following code come from wxpython demo
-        popup.SetBackgroundColour("CADET BLUE")
-        st = wx.StaticText(popup, -1, message, pos=(10,10))
-        sz = st.GetBestSize()
-        popup.SetSize( (sz.width+20, sz.height+20) )
-        x=(wx.DisplaySize()[0]-popup.GetSize()[0])/2
-        popup.SetPosition((x,0))
-        popup.Show()
-        wx.CallLater(5000,popup.Destroy)
-
-    def showDialog(self, message, title="", type_="info", answer_cb = None, answer_data = None):
-        if type_ == 'info':
-            flags = wx.OK | wx.ICON_INFORMATION
-        elif type_ == 'error':
-            flags = wx.OK | wx.ICON_ERROR
-        elif type_ == 'yes/no':
-            flags = wx.YES_NO | wx.ICON_QUESTION
-        else:
-            flags = wx.OK | wx.ICON_INFORMATION
-            log.error(_('unmanaged dialog type: %s'), type_)
-        dlg = wx.MessageDialog(self, message, title, flags)
-        answer = dlg.ShowModal()
-        dlg.Destroy()
-        if answer_cb:
-            data = [answer_data] if answer_data else []
-            answer_cb(True if (answer == wx.ID_YES or answer == wx.ID_OK) else False, *data)
-
-    def setStatusOnline(self, online=True, show="", statuses={}):
-        """enable/disable controls, must be called when local user online status change"""
-        if online:
-            self.SetStatusText(Const.msgONLINE)
-            self.tools.Enable()
-            try:
-                presence = [x for x in Const.PRESENCE if x[0] == show][0][1]
-                self.statusBox.SetValue(presence)
-            except (TypeError, IndexError):
-                pass
-            try:
-                self.statusTxt.SetValue(statuses['default'])
-            except (TypeError, KeyError):
-                pass
-        else:
-            self.SetStatusText(Const.msgOFFLINE)
-            self.tools.Disable()
-        return
-
-    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
-        """ Launch a dynamic action
-        @param callback_id: id of the action to launch
-        @param data: data needed only for certain actions
-        @param profile_key: %(doc_profile_key)s
-
-        """
-        if data is None:
-            data = dict()
-        def action_cb(data):
-            if not data:
-                # action was a one shot, nothing to do
-                pass
-            elif "xmlui" in data:
-                log.debug (_("XML user interface received"))
-                ui = xmlui.create(self, xml_data = data['xmlui'])
-                ui.show()
-            elif "authenticated_profile" in data:
-                assert("caller" in data)
-                if data["caller"] == "profile_manager":
-                    assert(self.profile_pan.IsShown())
-                    self.profile_pan.getXMPPParams(data['authenticated_profile'])
-                elif data["caller"] == "plug_profile":
-                    self.plug_profile_1(data['authenticated_profile'])
-                else:
-                    raise NotImplementedError
-            else:
-                dlg = wx.MessageDialog(self, _(u"Unmanaged action result"),
-                                       _('Error'),
-                                       wx.OK | wx.ICON_ERROR
-                                      )
-                dlg.ShowModal()
-                dlg.Destroy()
-        def action_eb(failure):
-            dlg = wx.MessageDialog(self, failure.message,
-                                   failure.fullname,
-                                   wx.OK | wx.ICON_ERROR
-                                  )
-            dlg.ShowModal()
-            dlg.Destroy()
-
-        self.bridge.launchAction(callback_id, data, profile_key, callback=action_cb, errback=action_eb)
-
-    def askConfirmationHandler(self, confirmation_id, confirmation_type, data, profile):
-        #TODO: refactor this in QuickApp
-        if not self.check_profile(profile):
-            return
-        log.debug (_("Confirmation asked"))
-        answer_data={}
-        if confirmation_type == "FILE_TRANSFER":
-            log.debug (_("File transfer confirmation asked"))
-            dlg = wx.MessageDialog(self, _("The contact %(jid)s wants to send you the file %(filename)s\nDo you accept ?") % {'jid':data["from"], 'filename':data["filename"]},
-                                   _('File Request'),
-                                   wx.YES_NO | wx.ICON_QUESTION
-                                  )
-            answer=dlg.ShowModal()
-            if answer==wx.ID_YES:
-                filename = wx.FileSelector(_("Where do you want to save the file ?"), flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
-                if filename:
-                    answer_data["dest_path"] = filename
-                    self.bridge.confirmationAnswer(confirmation_id, True, answer_data, profile)
-                    self.waitProgress(confirmation_id, _("File Transfer"), _("Copying %s") % os.path.basename(filename), profile)
-                else:
-                    answer = wx.ID_NO
-            if answer==wx.ID_NO:
-                    self.bridge.confirmationAnswer(confirmation_id, False, answer_data, profile)
-
-            dlg.Destroy()
-
-        elif confirmation_type == "YES/NO":
-            log.debug (_("Yes/No confirmation asked"))
-            dlg = wx.MessageDialog(self, data["message"],
-                                   _('Confirmation'),
-                                   wx.YES_NO | wx.ICON_QUESTION
-                                  )
-            answer=dlg.ShowModal()
-            if answer==wx.ID_YES:
-                self.bridge.confirmationAnswer(confirmation_id, True, {}, profile)
-            if answer==wx.ID_NO:
-                self.bridge.confirmationAnswer(confirmation_id, False, {}, profile)
-
-            dlg.Destroy()
-
-    def actionResultHandler(self, type_, id_, data, profile):
-        if not self.check_profile(profile):
-            return
-        log.debug (_("actionResult: type_ = [%(type_)s] id_ = [%(id_)s] data = [%(data)s]") % {'type_':type_, 'id_':id_, 'data':data})
-        if not id_ in self.current_action_ids:
-            log.debug (_('unknown id_, ignoring'))
-            return
-        if type_ == "SUPPRESS":
-            self.current_action_ids.remove(id_)
-        elif type_ == "SUCCESS":
-            self.current_action_ids.remove(id_)
-            dlg = wx.MessageDialog(self, data["message"],
-                                   _('Success'),
-                                   wx.OK | wx.ICON_INFORMATION
-                                  )
-            dlg.ShowModal()
-            dlg.Destroy()
-        elif type_ == "ERROR":
-            self.current_action_ids.remove(id_)
-            dlg = wx.MessageDialog(self, data["message"],
-                                   _('Error'),
-                                   wx.OK | wx.ICON_ERROR
-                                  )
-            dlg.ShowModal()
-            dlg.Destroy()
-        elif type_ == "XMLUI":
-            self.current_action_ids.remove(id_)
-            log.debug (_("XML user interface received"))
-            misc = {}
-            #FIXME FIXME FIXME: must clean all this crap !
-            title = _('Form')
-            if data['type_'] == _('registration'):
-                title = _('Registration')
-                misc['target'] = data['target']
-                misc['action_back'] = self.bridge.gatewayRegister
-            xmlui.create(self, title=title, xml_data = data['xml'], misc = misc)
-        elif type_ == "RESULT":
-            self.current_action_ids.remove(id_)
-            if self.current_action_ids_cb.has_key(id_):
-                callback = self.current_action_ids_cb[id_]
-                del self.current_action_ids_cb[id_]
-                callback(data)
-        elif type_ == "DICT_DICT":
-            self.current_action_ids.remove(id_)
-            if self.current_action_ids_cb.has_key(id_):
-                callback = self.current_action_ids_cb[id_]
-                del self.current_action_ids_cb[id_]
-                callback(data)
-        else:
-            log.error (_("FIXME FIXME FIXME: type_ [%s] not implemented") % type_)
-            raise NotImplementedError
-
-
-
-    def progressCB(self, progress_id, title, message, profile):
-        data = self.bridge.getProgress(progress_id, profile)
-        if data:
-            if not self.pbar:
-                #first answer, we must construct the bar
-                self.pbar = wx.ProgressDialog(title, message, float(data['size']), None,
-                    wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME)
-                self.pbar.finish_value = float(data['size'])
-
-            self.pbar.Update(int(data['position']))
-        elif self.pbar:
-            self.pbar.Update(self.pbar.finish_value)
-            return
-
-        wx.CallLater(10, self.progressCB, progress_id, title, message, profile)
-
-    def waitProgress (self, progress_id, title, message, profile):
-        self.pbar = None
-        wx.CallLater(10, self.progressCB, progress_id, title, message, profile)
-
-
-
-    ### events ###
-
-    def onContactActivated(self, jid):
-        log.debug (_("onContactActivated: %s"), jid)
-        if self.chat_wins[jid.bare].IsShown():
-            self.chat_wins[jid.bare].Hide()
-        else:
-            self.chat_wins[jid.bare].Show()
-
-    def onConnectRequest(self, e):
-        QuickApp.asyncConnect(self, self.profile)
-
-    def onDisconnectRequest(self, e):
-        self.bridge.disconnect(self.profile)
-
-    def __updateStatus(self):
-        show = [x for x in Const.PRESENCE if x[1] == self.statusBox.GetValue()][0][0]
-        status = self.statusTxt.GetValue()
-        self.bridge.setPresence(show=show, statuses={'default': status}, profile_key=self.profile)  #FIXME: manage multilingual statuses
-
-    def onStatusChange(self, e):
-        log.debug(_("Status change request"))
-        self.__updateStatus()
-
-    def onParam(self, e):
-        log.debug(_("Param request"))
-        def success(params):
-            xmlui.create(self, xml_data=params, title=_("Configuration"))
-
-        def failure(error):
-            dlg = wx.MessageDialog(self, error.message,
-                                   error.fullname,
-                                   wx.OK | wx.ICON_ERROR
-                                  )
-            dlg.ShowModal()
-            dlg.Destroy()
-        self.bridge.getParamsUI(app=Const.APP_NAME, profile_key=self.profile, callback=success, errback=failure)
-
-    def onAbout(self, e):
-        about = wx.AboutDialogInfo()
-        about.SetName(Const.APP_NAME)
-        about.SetVersion (unicode(self.bridge.getVersion()))
-        about.SetCopyright(u"(C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson aka Goffi")
-        about.SetDescription( _(u"%(name)s is a SàT (Salut à Toi) frontend\n"+
-        u"%(name)s is based on WxPython, and is the standard graphic interface of SàT") % {'name': Const.APP_NAME})
-        about.SetWebSite(("http://www.goffi.org", "Goffi's non-hebdo (french)"))
-        about.SetDevelopers([ "Goffi (Jérôme Poisson)"])
-        try:
-            with open(Const.LICENCE_PATH, "r") as licence:
-                about.SetLicence(''.join(licence.readlines()))
-        except:
-            pass
-
-        wx.AboutBox(about)
-
-    def onExit(self, e):
-        self.Close()
-
-    def onShowProfile(self, e):
-        log.debug(_("Show contact's profile request"))
-        target = self.contact_list.getSelection()
-        if not target:
-            dlg = wx.MessageDialog(self, _("You haven't selected any contact !"),
-                                   _('Error'),
-                                   wx.OK | wx.ICON_ERROR
-                                  )
-            dlg.ShowModal()
-            dlg.Destroy()
-            return
-        _id = self.bridge.getCard(target.bare, self.profile)
-        self.current_action_ids.add(_id)
-        self.current_action_ids_cb[_id] = self.onProfileReceived
-
-    def onProfileReceived(self, data):
-        """Called when a profile is received"""
-        log.debug (_('Profile received: [%s]') % data)
-        Profile(self, data)
-
-    def onJoinRoom(self, e):
-        log.warning('FIXME: temporary menu, must be improved')
-        #TODO: a proper MUC room joining dialog with nickname etc
-        dlg = wx.TextEntryDialog(
-                self, _("Please enter MUC's JID"),
-                #_('Entering a MUC room'), 'test@conference.necton2.int')
-                _('Entering a MUC room'), 'room@muc_service.server.tld')
-        if dlg.ShowModal() == wx.ID_OK:
-            room_jid=JID(dlg.GetValue())
-            if room_jid.is_valid():
-                self.bridge.joinMUC(room_jid, self.profiles[self.profile]['whoami'].node, {}, self.profile)
-            else:
-                log.error (_("'%s' is an invalid JID !"), room_jid)
-
-    def onClose(self, e):
-        QuickApp.onExit(self)
-        log.info(_("Exiting..."))
-        for win in self.chat_wins:
-            self.chat_wins[win].Destroy()
-        self.tray_icon.Destroy()
-        e.Skip()
-
-    def onTrayClick(self, e):
-        log.debug(_("Tray Click"))
-        if self.IsShown():
-            self.Hide()
-        else:
-            self.Show()
-            self.Raise()
-        e.Skip()
--- a/frontends/src/wix/profile.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat.core.i18n import _
-import wx
-import pdb
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.jid  import JID
-
-
-class Profile(wx.Frame):
-    """This class is used to show/modify profile given by SàT"""
-
-    def __init__(self, host, data, title="Profile"):
-        super(Profile, self).__init__(None, title=title)
-        self.host = host
-
-        self.name_dict = { 'fullname': _('Full Name'),
-                           'nick' : _('Nickname'),
-                           'birthday' : _('Birthday'),
-                           'phone' : _('Phone #'),
-                           'website' : _('Website'),
-                           'email' : _('E-mail'),
-                           'avatar' : _('Avatar')
-                         }
-        self.ctl_list = {}  # usefull to access ctrl, key = (name)
-
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.notebook=wx.Notebook(self, -1)
-        self.sizer.Add(self.notebook, 1, flag=wx.EXPAND)
-        self.SetSizer(self.sizer)
-        self.SetAutoLayout(True)
-
-        #events
-        self.Bind(wx.EVT_CLOSE, self.onClose, self)
-
-        self.MakeModal()
-        self.showData(data)
-        self.Show()
-
-    def showData(self, data):
-        flags = wx.TE_READONLY
-
-        #General tab
-        generaltab = wx.Panel(self.notebook)
-        sizer = wx.FlexGridSizer(cols=2)
-        sizer.AddGrowableCol(1)
-        generaltab.SetSizer(sizer)
-        generaltab.SetAutoLayout(True)
-        for field in ['fullname','nick', 'birthday', 'phone', 'website', 'email']:
-            value = data[field] if data.has_key(field) else ''
-            label=wx.StaticText(generaltab, -1, self.name_dict[field]+": ")
-            sizer.Add(label)
-            self.ctl_list[field] = wx.TextCtrl(generaltab, -1, value, style = flags)
-            sizer.Add(self.ctl_list[field], 1, flag = wx.EXPAND)
-        #Avatar
-        if data.has_key('avatar'):
-            filename = self.host.bridge.getAvatarFile(data['avatar'])
-            label=wx.StaticText(generaltab, -1, self.name_dict['avatar']+": ")
-            sizer.Add(label)
-            img = wx.Image(filename).ConvertToBitmap()
-            self.ctl_list['avatar'] = wx.StaticBitmap(generaltab, -1, img)
-            sizer.Add(self.ctl_list['avatar'], 0)
-
-
-
-        self.notebook.AddPage(generaltab, _("General"))
-
-
-    def onClose(self, event):
-        """Close event"""
-        log.debug(_("close"))
-        self.MakeModal(False)
-        event.Skip()
-
--- a/frontends/src/wix/profile_manager.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,183 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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_frontends.primitivus.constants import Const as C
-import wx
-from sat.core.log import getLogger
-log = getLogger(__name__)
-
-
-NO_SELECTION_ENTRY = ' '
-
-
-class ProfileManager(wx.Panel):
-    def __init__(self, host):
-        super(ProfileManager, self).__init__(host)
-        self.host = host
-
-        #self.sizer = wx.FlexGridSizer(cols=2)
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.SetSizer(self.sizer)
-
-        self.selected_profile = NO_SELECTION_ENTRY  # allow to reselect the previous selection until the profile is authenticated
-        self.profile_name = wx.ComboBox(self, -1, style=wx.CB_READONLY|wx.CB_SORT)
-        self.__refillProfiles()
-        self.Bind(wx.EVT_COMBOBOX, self.onProfileChange)
-        self.panel_id = wx
-
-        self.sizer.Add(wx.Window(self, -1), 1)
-        self.sizer.Add(wx.StaticText(self, -1, _("Profile:")), 0, flag=wx.ALIGN_CENTER)
-        self.sizer.Add(self.profile_name, 0, flag=wx.ALIGN_CENTER)
-        button_panel = wx.Panel(self)
-        button_panel.sizer = wx.BoxSizer(wx.HORIZONTAL)
-        button_panel.SetSizer(button_panel.sizer)
-        button_new = wx.Button(button_panel, -1, _("New"))
-        button_del = wx.Button(button_panel, -1, _("Delete"))
-        button_panel.sizer.Add(button_new)
-        button_panel.sizer.Add(button_del)
-        self.sizer.Add(button_panel, flag=wx.CENTER)
-        self.Bind(wx.EVT_BUTTON, self.onNewProfile, button_new)
-        self.Bind(wx.EVT_BUTTON, self.onDeleteProfile, button_del)
-
-        login_box = wx.StaticBox(self, -1, _("Login"))
-        self.login_sizer = wx.StaticBoxSizer(login_box, wx.VERTICAL)
-        self.sizer.Add(self.login_sizer, 1, wx.EXPAND | wx.ALL)
-        self.login_jid = wx.TextCtrl(self, -1)
-        self.login_sizer.Add(wx.StaticText(self, -1, "JID:"), 0, flag=wx.ALIGN_CENTER)
-        self.login_sizer.Add(self.login_jid, flag=wx.EXPAND)
-        self.login_pass = wx.TextCtrl(self, -1, style = wx.TE_PASSWORD)
-        self.login_sizer.Add(wx.StaticText(self, -1, _("Password:")), 0, flag=wx.ALIGN_CENTER)
-        self.login_sizer.Add(self.login_pass, flag=wx.EXPAND)
-
-        loggin_button = wx.Button(self, -1, _("Connect"))
-        self.Bind(wx.EVT_BUTTON, self.onConnectButton, loggin_button)
-        self.login_sizer.Add(loggin_button, flag=wx.ALIGN_CENTER)
-
-        self.sizer.Add(wx.Window(self, -1), 1)
-
-        #Now we can set the default value
-        self.__setDefault()
-
-    def __setDefault(self):
-        profile_default = NO_SELECTION_ENTRY if self.host.options.profile else self.host.bridge.getProfileName("@DEFAULT@")
-        if profile_default:
-            self.profile_name.SetValue(profile_default)
-            self.onProfileChange(None)
-
-    def __refillProfiles(self):
-        """Update profiles with current names. Must be called after a profile change"""
-        self.profile_name.Clear()
-        profiles = self.host.bridge.getProfilesList()
-        profiles.sort()
-        self.profile_name.Append(NO_SELECTION_ENTRY)
-        for profile in profiles:
-            self.profile_name.Append(profile)
-
-    def onNewProfile(self, event):
-        dlg = wx.TextEntryDialog(self, _("Please enter the new profile name"), _("New profile"), style = wx.OK | wx.CANCEL)
-        if dlg.ShowModal() == wx.ID_OK:
-            name = dlg.GetValue()
-            if name:
-                if name[0]=='@':
-                    wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal()
-                else:
-                    def cb():
-                        self.__refillProfiles()
-                        self.profile_name.SetValue(name)
-                        self.selected_profile = name
-                        self.getXMPPParams(name)
-                    self.host.bridge.asyncCreateProfile(name, callback=cb)
-        dlg.Destroy()
-
-    def onDeleteProfile(self, event):
-        name = self.profile_name.GetValue()
-        if not name:
-            return
-        dlg = wx.MessageDialog(self, _("Are you sure to delete the profile [%s]") % name, _("Confirmation"), wx.ICON_QUESTION | wx.YES_NO)
-        if dlg.ShowModal() == wx.ID_YES:
-            def cb():
-                self.__refillProfiles()
-                self.__setDefault()
-            self.host.bridge.asyncDeleteProfile(name, callback=cb)
-        dlg.Destroy()
-
-    def getXMPPParams(self, profile):
-        """This is called from MainWindow.launchAction when the profile has been authenticated.
-
-        @param profile: %(doc_profile)s
-        """
-        def setJID(jabberID):
-            self.login_jid.SetValue(jabberID)
-
-        def setPassword(password):
-            self.login_pass.SetValue(password)
-
-        self.profile_name.SetValue(profile)
-        self.selected_profile = profile
-        self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError)
-        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError)
-
-    def onProfileChange(self, event):
-        """Called when a profile is choosen in the combo box"""
-        profile_name = self.profile_name.GetValue()
-        if not profile_name or profile_name == self.selected_profile:
-            return  # avoid infinite loop
-        if profile_name == NO_SELECTION_ENTRY:
-            self.selected_profile = NO_SELECTION_ENTRY
-            return
-        if self.selected_profile:
-            self.profile_name.SetValue(self.selected_profile)
-        self.host.profile = profile_name  # FIXME: EXTREMELY DIRTY, needed for sat_frontends.tools.xmlui.XMLUI.submit
-        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'profile_manager'}, profile_key=profile_name)
-
-    def onConnectButton(self, event):
-        """Called when the Connect button is pressed"""
-        name = self.profile_name.GetValue()
-        assert(name == self.selected_profile)  # if not, there's a bug somewhere...
-        if not name or name == NO_SELECTION_ENTRY:
-            wx.MessageDialog(self, _("You must select a profile or create a new one before connecting"), _("No profile selected"), wx.ICON_ERROR).ShowModal()
-            return
-        if name[0]=='@':
-            wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal()
-            return
-        profile = self.host.bridge.getProfileName(name)
-        assert(profile)
-
-        self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=lambda old_jid: self.__old_jidReceived(old_jid, profile), errback=self.getParamError)
-
-    def __old_jidReceived(self, old_jid, profile):
-        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=lambda old_pass: self.__old_passReceived(old_jid, old_pass, profile), errback=self.getParamError)
-
-    def __old_passReceived(self, old_jid, old_pass, profile):
-        new_jid = self.login_jid.GetValue()
-        new_pass = self.login_pass.GetValue()
-        if old_jid != new_jid:
-            log.debug(_('Saving new JID and server'))
-            self.host.bridge.setParam("JabberID", new_jid, "Connection", profile_key=profile)
-        if old_pass != new_pass:
-            log.debug(_('Saving new password'))
-            self.host.bridge.setParam("Password", new_pass, "Connection", profile_key=profile)
-        self.host.plug_profile(profile)
-
-
-    def getParamError(self, ignore):
-        wx.MessageDialog(self, _("Can't get profile parameter"), _("Profile error"), wx.ICON_ERROR).ShowModal()
--- a/frontends/src/wix/quiz_game.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,243 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-
-from sat.core.i18n import _
-import wx
-import os.path, glob
-import pdb
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.jid  import JID
-from time import time
-from math import sin, cos, pi
-
-CARD_WIDTH = 74
-CARD_HEIGHT = 136
-WIDTH = 800
-HEIGHT = 600
-
-class GraphicElement(object):
-    """This class is used to represent a card, graphically and logically"""
-
-    def __init__(self, file, x=0, y=0, zindex=10, transparent=True):
-        """ Image used to build the game visual
-        @param file: path of the PNG file
-        @param zindex: layer of the element (0=background; the bigger, the more in the foreground)"""
-        self.bitmap = wx.Image(file).ConvertToBitmap()
-        self.x = x
-        self.y = y
-        self.zindex = zindex
-        self.transparent = transparent
-
-    def __cmp__(self, other):
-        return self.zindex.__cmp__(other.zindex)
-
-    def draw(self, dc, x=None, y=None):
-        """Draw the card on the device context
-        @param dc: device context
-        @param x: abscissa
-        @param y: ordinate"""
-        dc.DrawBitmap(self.bitmap, x or self.x, y or self.y, self.transparent)
-
-class BaseWindow(wx.Window):
-    """This is the panel where the game is drawed, under the other widgets"""
-
-    def __init__(self, parent):
-        wx.Window.__init__(self, parent, pos=(0,0), size=(WIDTH, HEIGHT))
-        self.parent = parent
-        self.SetMinSize(wx.Size(WIDTH, HEIGHT))
-        self.Bind(wx.EVT_PAINT, self.onPaint)
-        self.graphic_elts = {}
-        self.loadImages(os.path.join(parent.parent.host.media_dir, 'games/quiz/'))
-
-    def loadImages(self, dir):
-        """Load all the images needed for the game
-        @param dir: directory where the PNG files are"""
-        x_player = 24
-        for name, sub_dir, filename, x, y, zindex, transparent in [("background", "background", "blue_background.png", 0, 0, 0, False),
-                                                             ("joueur0", "characters", "zombie.png", x_player+0*184, 170, 5, True),
-                                                             ("joueur1", "characters", "nerd.png", x_player+1*184, 170, 5, True),
-                                                             ("joueur2", "characters", "zombie.png", x_player+2*184, 170, 5, True),
-                                                             ("joueur3", "characters", "zombie.png", x_player+3*184, 170, 5, True),
-                                                             ("foreground", "foreground", "foreground.png", 0, 0, 10, True)]:
-            self.graphic_elts[name] = GraphicElement(os.path.join(dir, sub_dir, filename), x = x, y = y, zindex=zindex, transparent=transparent)
-
-        self.right_image = wx.Image(os.path.join(dir, "foreground", "right.png")).ConvertToBitmap()
-        self.wrong_image = wx.Image(os.path.join(dir, "foreground", "wrong.png")).ConvertToBitmap()
-
-    def fullPaint(self, device_context):
-        """Paint all the game on the given dc
-        @param device_context: wx.DC"""
-        elements = self.graphic_elts.values()
-        elements.sort()
-        for elem in elements:
-            elem.draw(device_context)
-
-        _font = wx.Font(65, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
-        device_context.SetFont(_font)
-        device_context.SetTextForeground(wx.BLACK)
-
-        for i in range(4):
-            answer = self.parent.players_data[i]["answer"]
-            score = self.parent.players_data[i]["score"]
-            if answer == None:
-                device_context.DrawText("%d" % score, 100 + i*184, 355)
-            else:
-                device_context.DrawBitmap(self.right_image if answer else self.wrong_image, 39+i*184, 348, True)
-
-
-        if self.parent.time_origin:
-            device_context.SetPen(wx.BLACK_PEN)
-            radius = 20
-            center_x = 760
-            center_y = 147
-            origin = self.parent.time_origin
-            current = self.parent.time_pause or time()
-            limit = self.parent.time_limit
-            total = limit - origin
-            left = self.parent.time_left = max(0,limit - current)
-            device_context.SetBrush(wx.RED_BRUSH if left/total < 1/4.0 else wx.WHITE_BRUSH)
-            if left:
-                #we now draw the timer
-                angle = ((-2*pi)*((total-left)/total) + (pi/2))
-                x = center_x + radius * cos(angle)
-                y = center_y - radius * sin(angle)
-                device_context.DrawArc(center_x, center_y-radius, x, y, center_x, center_y)
-
-    def onPaint(self, event):
-        dc = wx.PaintDC(self)
-        self.fullPaint(dc)
-
-
-
-class QuizPanel(wx.Panel):
-    """This class is used to display the quiz game"""
-
-    def __init__(self, parent, referee, players, player_nick):
-        wx.Panel.__init__(self, parent)
-        self.referee = referee
-        self.player_nick = player_nick
-        self.players = players
-        self.time_origin = None #set to unix time when the timer start
-        self.time_limit = None
-        self.time_left = None
-        self.time_pause = None
-        self.last_answer = None
-        self.parent = parent
-        self.SetMinSize(wx.Size(WIDTH, HEIGHT))
-        self.SetSize(wx.Size(WIDTH, HEIGHT))
-        self.base = BaseWindow(self)
-        self.question = wx.TextCtrl(self, -1, pos=(168,17), size=(613, 94), style=wx.TE_MULTILINE | wx.TE_READONLY)
-        self.answer = wx.TextCtrl(self, -1, pos=(410,569), size=(342, 21), style=wx.TE_PROCESS_ENTER)
-        self.players_data = [{}, {}, {}, {}]
-        for i in range(4):
-            self.players_data[i]['bubble'] = wx.TextCtrl(self, -1, pos=(39+i*184, 120), size=(180, 56), style=wx.TE_MULTILINE | wx.TE_READONLY)
-            self.players_data[i]['bubble'].Hide()
-            self.players_data[i]['answer'] = None #True if the player gave a good answer
-            self.players_data[i]['score'] = 0
-        self.answer.Bind(wx.EVT_TEXT_ENTER, self.answered)
-        self.parent.host.bridge.quizGameReady(player_nick, referee, self.parent.host.profile)
-        self.state = None
-
-    def answered(self, event):
-        """Called when the player gave an answer in the box"""
-        self.last_answer = self.answer.GetValue()
-        self.answer.Clear()
-        if self.last_answer:
-            self.parent.host.bridge.quizGameAnswer(self.player_nick, self.referee, self.last_answer, self.parent.host.profile)
-
-    def quizGameTimerExpired(self):
-        """Called when nobody answered the question in time"""
-        self.question.SetValue(_(u"Quel dommage, personne n'a trouvé la réponse\n\nAttention, la prochaine question arrive..."))
-
-    def quizGameTimerRestarted(self, time_left):
-        """Called when nobody answered the question in time"""
-        timer_orig = self.time_limit - self.time_origin
-        self.time_left = time_left
-        self.time_limit = time() + time_left
-        self.time_origin = self.time_limit - timer_orig
-        self.time_pause = None
-        self.__timer_refresh()
-
-    def startTimer(self, timer=60):
-        """Start the timer to answer the question"""
-        self.time_left = timer
-        self.time_origin = time()
-        self.time_limit = self.time_origin + timer
-        self.time_pause = None
-        self.__timer_refresh()
-
-    def __timer_refresh(self):
-        self.Refresh()
-        if self.time_left:
-            wx.CallLater(1000, self.__timer_refresh)
-
-    def quizGameNew(self, data):
-        """Start a new game, with given hand"""
-        if data.has_key('instructions'):
-            self.question.ChangeValue(data['instructions'])
-        self.Refresh()
-
-    def quizGameQuestion(self, question_id, question, timer):
-        """Called when a new question is available
-        @param question: question to ask"""
-        self.question.ChangeValue(question)
-        self.startTimer(timer)
-        self.last_answer = None
-        self.answer.Clear()
-
-    def quizGamePlayerBuzzed(self, player, pause):
-        """Called when the player pushed the buzzer
-        @param player: player who pushed the buzzer
-        @param pause: should we stop the timer ?"""
-        if pause:
-            self.time_pause = time()
-
-    def quizGamePlayerSays(self, player, text, delay):
-        """Called when the player says something
-        @param player: who is talking
-        @param text: what the player says"""
-        if player != self.player_nick and self.last_answer:
-            #if we are not the player talking, and we have an answer, that mean that our answer has not been validated
-            #we can put it again in the answering box
-            self.answer.SetValue(self.last_answer)
-        idx = self.players.index(player)
-        bubble = self.players_data[idx]['bubble']
-        bubble.SetValue(text)
-        bubble.Show()
-        self.Refresh()
-        wx.CallLater(delay * 1000, bubble.Hide)
-
-    def quizGameAnswerResult(self, player, good_answer, score):
-        """Result of the just given answer
-        @param player: who gave the answer
-        @good_answer: True if the answer is right
-        @score: dict of score"""
-        player_idx = self.players.index(player)
-        self.players_data[player_idx]['answer'] = good_answer
-        for _player in score:
-            _idx = self.players.index(_player)
-            self.players_data[_idx]['score'] = score[_player]
-        def removeAnswer():
-            self.players_data[player_idx]['answer'] = None
-            self.Refresh()
-        wx.CallLater(2000, removeAnswer)
-        self.Refresh()
--- a/frontends/src/wix/wix	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013Jé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.wix.constants import Const as C
-from sat.core import log_config
-log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
-import wx
-from sat_frontends.wix.main_window import MainWindow
-
-
-class SATApp(wx.App):
-    def __init__(self,  redirect=False, filename=None, useBestVisual=False, clearSigInt=True):
-        super(SATApp,self).__init__(redirect, filename, useBestVisual, clearSigInt)
-
-    def OnInit(self):
-        self.main = MainWindow()
-        self.main.Show(True)
-        self.SetTopWindow(self.main)
-        return True
-
-
-sat = SATApp()
-sat.MainLoop()
--- a/frontends/src/wix/xmlui.py	Thu Feb 05 11:59:26 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,428 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-
-from sat.core.i18n import _
-import wx
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools import xmlui
-from sat_frontends.constants import Const as C
-
-
-class EventWidget(object):
-    """ Used to manage change event of  widgets """
-
-    def _xmluiOnChange(self, callback):
-        """ Call callback with widget as only argument """
-        def change_cb(event):
-            callback(self)
-        self.Bind(self._xmlui_change_event, change_cb)
-
-
-class WixWidget(object):
-    _xmlui_proportion = 0
-
-
-class ValueWidget(WixWidget):
-
-    def _xmluiSetValue(self, value):
-        self.SetValue(value)
-
-    def _xmluiGetValue(self):
-        return self.GetValue()
-
-
-class EmptyWidget(WixWidget, xmlui.EmptyWidget, wx.Window):
-
-    def __init__(self, _xmlui_parent):
-        wx.Window.__init__(self, _xmlui_parent, -1)
-
-
-class TextWidget(WixWidget, xmlui.TextWidget, wx.StaticText):
-
-    def __init__(self, _xmlui_parent, value):
-        wx.StaticText.__init__(self, _xmlui_parent, -1, value)
-
-
-class LabelWidget(xmlui.LabelWidget, TextWidget):
-
-    def __init__(self, _xmlui_parent, value):
-        super(LabelWidget, self).__init__(_xmlui_parent, value+": ")
-
-
-class JidWidget(xmlui.JidWidget, TextWidget):
-    pass
-
-
-class DividerWidget(WixWidget, xmlui.DividerWidget, wx.StaticLine):
-
-    def __init__(self, _xmlui_parent, style='line'):
-        wx.StaticLine.__init__(self, _xmlui_parent, -1)
-
-
-class StringWidget(EventWidget, ValueWidget, xmlui.StringWidget, wx.TextCtrl):
-    _xmlui_change_event = wx.EVT_TEXT
-
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        style = wx.TE_READONLY if read_only else 0
-        wx.TextCtrl.__init__(self, _xmlui_parent, -1, value, style=style)
-        self._xmlui_proportion = 1
-
-
-class PasswordWidget(EventWidget, ValueWidget, xmlui.PasswordWidget, wx.TextCtrl):
-    _xmlui_change_event = wx.EVT_TEXT
-
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        style = wx.TE_PASSWORD
-        if read_only:
-            style |= wx.TE_READONLY
-        wx.TextCtrl.__init__(self, _xmlui_parent, -1, value, style=style)
-        self._xmlui_proportion = 1
-
-
-class TextBoxWidget(EventWidget, ValueWidget, xmlui.TextBoxWidget, wx.TextCtrl):
-    _xmlui_change_event = wx.EVT_TEXT
-
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        style = wx.TE_MULTILINE
-        if read_only:
-            style |= wx.TE_READONLY
-        wx.TextCtrl.__init__(self, _xmlui_parent, -1, value, style=style)
-        self._xmlui_proportion = 1
-
-
-class BoolWidget(EventWidget, ValueWidget, xmlui.BoolWidget, wx.CheckBox):
-    _xmlui_change_event = wx.EVT_CHECKBOX
-
-    def __init__(self, _xmlui_parent, state, read_only=False):
-        style = wx.CHK_2STATE
-        if read_only:
-            style |= wx.TE_READONLY
-        wx.CheckBox.__init__(self, _xmlui_parent, -1, "", style=wx.CHK_2STATE)
-        self.SetValue(state)
-        self._xmlui_proportion = 1
-
-    def _xmluiSetValue(self, value):
-        self.SetValue(value == 'true')
-
-    def _xmluiGetValue(self):
-        return "true" if self.GetValue() else "false"
-
-
-# TODO: use wx.SpinCtrl instead of wx.TextCtrl
-class IntWidget(EventWidget, ValueWidget, xmlui.IntWidget, wx.TextCtrl):
-    _xmlui_change_event = wx.EVT_TEXT
-
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        style = wx.TE_READONLY if read_only else 0
-        wx.TextCtrl.__init__(self, _xmlui_parent, -1, value, style=style)
-        self._xmlui_proportion = 1
-
-
-class ButtonWidget(EventWidget, WixWidget, xmlui.ButtonWidget, wx.Button):
-    _xmlui_change_event = wx.EVT_BUTTON
-
-    def __init__(self, _xmlui_parent, value, click_callback):
-        wx.Button.__init__(self, _xmlui_parent, -1, value)
-        self._xmlui_click_callback = click_callback
-        _xmlui_parent.Bind(wx.EVT_BUTTON, lambda evt: click_callback(evt.GetEventObject()), self)
-        self._xmlui_parent = _xmlui_parent
-
-    def _xmluiOnClick(self, callback):
-        self._xmlui_parent.Bind(wx.EVT_BUTTON, lambda evt: callback(evt.GetEventObject()), self)
-
-
-class ListWidget(EventWidget, WixWidget, xmlui.ListWidget, wx.ListBox):
-    _xmlui_change_event = wx.EVT_LISTBOX
-
-    def __init__(self, _xmlui_parent, options, selected, flags):
-        styles = wx.LB_MULTIPLE if not 'single' in flags else wx.LB_SINGLE
-        wx.ListBox.__init__(self, _xmlui_parent, -1, choices=[option[1] for option in options], style=styles)
-        self._xmlui_attr_map = {label: value for value, label in options}
-        self._xmlui_proportion = 1
-        self._xmluiSelectValues(selected)
-
-    def _xmluiSelectValue(self, value):
-        try:
-            label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0]
-        except IndexError:
-            log.warning(_("Can't find value [%s] to select" % value))
-            return
-        for idx in xrange(self.GetCount()):
-            self.SetSelection(idx, self.GetString(idx) == label)
-
-    def _xmluiSelectValues(self, values):
-        labels = [label for label, _value in self._xmlui_attr_map.items() if _value in values]
-        for idx in xrange(self.GetCount()):
-            self.SetSelection(idx, self.GetString(idx) in labels)
-
-    def _xmluiGetSelectedValues(self):
-        ret = []
-        labels = [self.GetString(idx) for idx in self.GetSelections()]
-        for label in labels:
-            ret.append(self._xmlui_attr_map[label])
-        return ret
-
-    def _xmluiAddValues(self, values, select=True):
-        selected = self._xmluiGetSelectedValues()
-        for value in values:
-            if value not in self._xmlui_attr_map.values():
-                wx.ListBox.Append(self, value)
-                self._xmlui_attr_map[value] = value
-            if value not in selected:
-                selected.append(value)
-        self._xmluiSelectValues(selected)
-
-
-class WixContainer(object):
-    _xmlui_proportion = 1
-
-    def _xmluiAppend(self, widget):
-        self.sizer.Add(widget, self._xmlui_proportion, flag=wx.EXPAND)
-
-
-class AdvancedListContainer(WixContainer, xmlui.AdvancedListContainer, wx.ScrolledWindow):
-
-    def __init__(self, _xmlui_parent, columns, selectable='no'):
-        wx.ScrolledWindow.__init__(self, _xmlui_parent)
-        self._xmlui_selectable = selectable != 'no'
-        if selectable:
-            columns += 1
-        self.sizer = wx.FlexGridSizer(cols=columns)
-        self.SetSizer(self.sizer)
-        self._xmlui_select_cb = None
-        self._xmlui_select_idx = None
-        self._xmlui_select_widgets = []
-
-    def _xmluiAddRow(self, idx):
-        # XXX: select_button is a Q&D way to implement row selection
-        # FIXME: must be done properly
-        if not self._xmlui_selectable:
-            return
-        select_button = wx.Button(self, wx.ID_OK, label=_("select"))
-        self.sizer.Add(select_button)
-        def click_cb(event, idx=idx):
-            cb = self._xmlui_select_cb
-            self._xmlui_select_idx = idx
-            # TODO: fill self._xmlui_select_widgets
-            if cb is not None:
-                cb(self)
-            event.Skip()
-        self.Bind(wx.EVT_BUTTON, click_cb)
-
-    def _xmluiGetSelectedWidgets(self):
-        return self._xmlui_select_widgets
-
-    def _xmluiGetSelectedIndex(self):
-        return self._xmlui_select_idx
-
-    def _xmluiOnSelect(self, callback):
-        self._xmlui_select_cb = callback
-
-class PairsContainer(WixContainer, xmlui.PairsContainer, wx.Panel):
-
-    def __init__(self, _xmlui_parent):
-        wx.Panel.__init__(self, _xmlui_parent)
-        self.sizer = wx.FlexGridSizer(cols=2)
-        self.sizer.AddGrowableCol(1) #The growable column need most of time to be the right one in pairs
-        self.SetSizer(self.sizer)
-
-
-class TabsContainer(WixContainer, xmlui.TabsContainer, wx.Notebook):
-
-    def __init__(self, _xmlui_parent):
-        wx.Notebook.__init__(self, _xmlui_parent, -1, style=wx.NB_LEFT if self._xmlui_main.type=='param' else 0)
-
-    def _xmluiAddTab(self, label):
-        tab_panel = wx.Panel(self, -1)
-        tab_panel.sizer = wx.BoxSizer(wx.VERTICAL)
-        tab_panel.SetSizer(tab_panel.sizer)
-        self.AddPage(tab_panel, label)
-        VerticalContainer._xmluiAdapt(tab_panel)
-        return tab_panel
-
-
-class VerticalContainer(WixContainer, xmlui.VerticalContainer, wx.Panel):
-
-    def __init__(self, _xmlui_parent):
-        wx.Panel.__init__(self, _xmlui_parent)
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.SetSizer(self.sizer)
-
-
-## Dialogs ##
-
-
-class WixDialog(object):
-
-    def __init__(self, _xmlui_parent, level):
-        self.host = _xmlui_parent.host
-        self.ok_cb = None
-        self.cancel_cb = None
-        if level == C.XMLUI_DATA_LVL_INFO:
-            self.flags = wx.ICON_INFORMATION
-        elif level == C.XMLUI_DATA_LVL_ERROR:
-            self.flags = wx.ICON_ERROR
-        else:
-            self.flags = wx.ICON_INFORMATION
-            log.warning(_("Unmanaged dialog level: %s") % level)
-
-    def _xmluiShow(self):
-        answer = self.ShowModal()
-        if answer == wx.ID_YES or answer == wx.ID_OK:
-            self._xmluiValidated()
-        else:
-            self._xmluiCancelled()
-
-    def _xmluiClose(self):
-        self.Destroy()
-
-
-class MessageDialog(WixDialog, xmlui.MessageDialog, wx.MessageDialog):
-
-    def __init__(self, _xmlui_parent, title, message, level):
-        WixDialog.__init__(self, _xmlui_parent, level)
-        xmlui.MessageDialog.__init__(self, _xmlui_parent)
-        self.flags |= wx.OK
-        wx.MessageDialog.__init__(self, _xmlui_parent.host, message, title, style = self.flags)
-
-
-class NoteDialog(xmlui.NoteDialog, MessageDialog):
-    # TODO: separate NoteDialog
-    pass
-
-
-class ConfirmDialog(WixDialog, xmlui.ConfirmDialog, wx.MessageDialog):
-
-    def __init__(self, _xmlui_parent, title, message, level, buttons_set):
-        WixDialog.__init__(self, _xmlui_parent, level)
-        xmlui.ConfirmDialog.__init__(self, _xmlui_parent)
-        if buttons_set == C.XMLUI_DATA_BTNS_SET_YESNO:
-            self.flags |= wx.YES_NO
-        else:
-            self.flags |= wx.OK | wx.CANCEL
-        wx.MessageDialog.__init__(self, _xmlui_parent.host, message, title, style = self.flags)
-
-
-class FileDialog(WixDialog, xmlui.FileDialog, wx.FileDialog):
-
-    def __init__(self, _xmlui_parent, title, message, level, filetype):
-        # TODO: message and filetype are not managed yet
-        WixDialog.__init__(self, _xmlui_parent, level)
-        self.flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT # FIXME: use the legacy flags, but must manage cases like dir or open
-        xmlui.FileDialog.__init__(self, _xmlui_parent)
-        wx.FileDialog.__init__(self, _xmlui_parent.host, title, style = self.flags)
-
-    def _xmluiShow(self):
-        answer = self.ShowModal()
-        if answer == wx.ID_OK:
-            self._xmluiValidated({'path': self.GetPath()})
-        else:
-            self._xmluiCancelled()
-
-
-class GenericFactory(object):
-
-    def __getattr__(self, attr):
-        if attr.startswith("create"):
-            cls = globals()[attr[6:]]
-            return cls
-
-
-class WidgetFactory(GenericFactory):
-
-    def __getattr__(self, attr):
-        if attr.startswith("create"):
-            cls = GenericFactory.__getattr__(self, attr)
-            cls._xmlui_main = self._xmlui_main
-            return cls
-
-
-class XMLUIPanel(xmlui.XMLUIPanel, wx.Frame):
-    """Create an user interface from a SàT XML"""
-    widget_factory = WidgetFactory()
-
-    def __init__(self, host, parsed_xml, title=None, flags = None,):
-        self.widget_factory._xmlui_main = self
-        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags)
-
-    def constructUI(self, parsed_dom):
-        style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in self.flags else wx.DEFAULT_FRAME_STYLE
-        wx.Frame.__init__(self, None, style=style)
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.SetSizer(self.sizer)
-
-        def postTreat():
-            if self.title:
-                self.SetTitle(self.title)
-
-            if self.type == 'form':
-                dialogButtons = wx.StdDialogButtonSizer()
-                submitButton = wx.Button(self.main_cont,wx.ID_OK, label=_("Submit"))
-                dialogButtons.AddButton(submitButton)
-                self.main_cont.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton)
-                if not 'NO_CANCEL' in self.flags:
-                    cancelButton = wx.Button(self.main_cont,wx.ID_CANCEL)
-                    dialogButtons.AddButton(cancelButton)
-                    self.main_cont.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton)
-                dialogButtons.Realize()
-                self.main_cont.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL)
-
-            self.sizer.Add(self.main_cont, 1, flag=wx.EXPAND)
-            self.sizer.Fit(self)
-            self.Show()
-
-        super(XMLUIPanel, self).constructUI(parsed_dom, postTreat)
-        if not 'NO_CANCEL' in self.flags:
-            self.Bind(wx.EVT_CLOSE, self.onClose, self)
-        self.MakeModal()
-
-    def _xmluiClose(self):
-        self.MakeModal(False)
-        self.Destroy()
-
-    ###events
-
-    def onParamChange(self, ctrl):
-        super(XMLUIPanel, self).onParamChange(ctrl)
-
-    def onFormSubmitted(self, event):
-        """Called when submit button is clicked"""
-        button = event.GetEventObject()
-        super(XMLUIPanel, self).onFormSubmitted(button)
-
-    def onClose(self, event):
-        """Close event: we have to send the form."""
-        log.debug(_("close"))
-        if self.type == 'param':
-            self.onSaveParams()
-        else:
-            self._xmluiClose()
-        event.Skip()
-
-
-class XMLUIDialog(xmlui.XMLUIDialog):
-    dialog_factory = WidgetFactory()
-
-
-xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel)
-xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog)
-create = xmlui.create
--- a/setup.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/setup.py	Wed Mar 18 10:52:28 2015 +0100
@@ -198,7 +198,7 @@
             if os.path.islink(dest):
                 to_remove.append(dest)
 
-        for script in ('jp', 'wix', 'primitivus'):
+        for script in ('jp', 'primitivus'):
             dest = os.path.join(self.install_scripts, script)
             if os.path.exists(dest):
                 to_remove.append(dest)
@@ -298,15 +298,14 @@
       package_dir={'sat': 'src', 'sat_frontends': 'frontends/src', 'twisted.plugins': 'src/twisted/plugins'},
       packages=['sat', 'sat.tools', 'sat.bridge', 'sat.plugins', 'sat.test', 'sat.core', 'sat.memory',
                 'sat_frontends', 'sat_frontends.bridge', 'sat_frontends.quick_frontend', 'sat_frontends.jp',
-                'sat_frontends.primitivus', 'sat_frontends.wix', 'sat_frontends.tools', 'sat.stdui',
-                'sat.tmp', 'sat.tmp.wokkel', 'twisted.plugins'],
-      package_data={'sat': ['sat.sh'],
-                    'sat_frontends': ['wix/COPYING']},
+                'sat_frontends.primitivus', 'sat_frontends.tools', 'sat.stdui','sat.tmp', 'sat.tmp.wokkel',
+                'twisted.plugins'],
+      package_data={'sat': ['sat.sh'], },
       data_files=[(os.path.join(sys.prefix, 'share/locale/fr/LC_MESSAGES'), ['i18n/fr/LC_MESSAGES/sat.mo']),
                   ('share/doc/%s' % NAME, ['CHANGELOG', 'COPYING', 'INSTALL', 'README', 'README4TRANSLATORS']),
                   (os.path.join('share', DBUS_DIR), (DBUS_FILE,)),
                   ],
-      scripts=['frontends/src/jp/jp', 'frontends/src/primitivus/primitivus', 'frontends/src/wix/wix'],
+      scripts=['frontends/src/jp/jp', 'frontends/src/primitivus/primitivus', ],
       zip_safe=False,
       dependency_links=['http://home.avvanta.com/%7Esteveha/pyfeed-0.7.4.tar.gz', 'http://home.avvanta.com/%7Esteveha/xe-0.7.4.tar.gz'],
       install_requires=['twisted', 'wokkel >= 0.7.1', 'progressbar', 'urwid >= 1.2.0', 'urwid-satext >= 0.4.0', 'pyfeed', 'xe', 'mutagen', 'pillow', 'lxml', 'pyxdg', 'markdown', 'html2text', 'pycrypto >= 2.6.1', 'python-potr'],
--- a/src/bridge/DBus.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/bridge/DBus.py	Wed Mar 18 10:52:28 2015 +0100
@@ -230,9 +230,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='',
-                         async_callbacks=None)
-    def delContact(self, entity_jid, profile_key="@DEFAULT@"):
-        return self._callback("delContact", unicode(entity_jid), unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("delContact", unicode(entity_jid), unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='(asa(sss))',
@@ -260,9 +260,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='s', out_signature='a(sa{ss}as)',
-                         async_callbacks=None)
-    def getContacts(self, profile_key="@DEFAULT@"):
-        return self._callback("getContacts", unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("getContacts", unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='as',
@@ -271,6 +271,12 @@
         return self._callback("getContactsFromGroup", unicode(group), unicode(profile_key))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='asass', out_signature='a{sa{ss}}',
+                         async_callbacks=None)
+    def getEntitiesData(self, jids, keys, profile):
+        return self._callback("getEntitiesData", jids, keys, unicode(profile))
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='sass', out_signature='a{ss}',
                          async_callbacks=None)
     def getEntityData(self, jid, keys, profile):
@@ -285,8 +291,8 @@
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='s',
                          async_callbacks=None)
-    def getLastResource(self, contact_jid, profile_key="@DEFAULT@"):
-        return self._callback("getLastResource", unicode(contact_jid), unicode(profile_key))
+    def getMainResource(self, contact_jid, profile_key="@DEFAULT@"):
+        return self._callback("getMainResource", unicode(contact_jid), unicode(profile_key))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='s',
@@ -295,7 +301,7 @@
         return self._callback("getMenuHelp", unicode(menu_id), unicode(language))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
-                         in_signature='si', out_signature='a(ssasas)',
+                         in_signature='si', out_signature='a(ssasasa{ss})',
                          async_callbacks=None)
     def getMenus(self, language, security_limit):
         return self._callback("getMenus", unicode(language), security_limit)
@@ -434,9 +440,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ssass', out_signature='',
-                         async_callbacks=None)
-    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
-        return self._callback("updateContact", unicode(entity_jid), unicode(name), groups, unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("updateContact", unicode(entity_jid), unicode(name), groups, unicode(profile_key), callback=callback, errback=errback)
 
     def __attributes(self, in_sign):
         """Return arguments to user given a in_sign
--- a/src/bridge/bridge_constructor/bridge_constructor.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/bridge/bridge_constructor/bridge_constructor.py	Wed Mar 18 10:52:28 2015 +0100
@@ -446,17 +446,35 @@
                 'args': self.getArguments(function['sig_in'], name=arg_doc, default=default)}
 
             if function["type"] == "method":
+                # XXX: we can manage blocking call in the same way as async one: if callback is None the call will be blocking
                 completion['debug'] = "" if not self.options.debug else 'log.debug ("%s")\n%s' % (section, 8 * ' ')
                 completion['args_result'] = self.getArguments(function['sig_in'], name=arg_doc)
-                completion['async_args'] = 'callback=None, errback=None' if async else ''
-                completion['async_comma'] = ', ' if async and function['sig_in'] else ''
-                completion['error_handler'] = ("error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))\n" + 8*" ") if async else ''
-                completion['async_args_result'] = 'timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler' if async else ''
+                completion['async_args'] = 'callback=None, errback=None'
+                completion['async_comma'] = ', ' if function['sig_in'] else ''
+                completion['error_handler'] = """if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        """
+                if async:
+                    completion['blocking_call'] = ''
+                    completion['async_args_result'] = 'timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler'
+                else:
+                    # XXX: To have a blocking call, we must have not reply_handler, so we test if callback exists, and add reply_handler only in this case
+                    completion['blocking_call'] = """kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        """
+                    completion['async_args_result'] = '**kwargs'
                 result = "self.db_%(category)s_iface.%(name)s(%(args_result)s%(async_comma)s%(async_args_result)s)" % completion
                 completion['result'] = ("unicode(%s)" if self.options.unicode and function['sig_out'] == 's' else "%s") % result
                 methods_part.append("""\
     def %(name)s(self, %(args)s%(async_comma)s%(async_args)s):
-        %(error_handler)s%(debug)sreturn %(result)s
+        %(error_handler)s%(blocking_call)s%(debug)sreturn %(result)s
 """ % completion)
 
         #at this point, methods_part should be filled,
--- a/src/bridge/bridge_constructor/bridge_template.ini	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Wed Mar 18 10:52:28 2015 +0100
@@ -184,12 +184,24 @@
 category=core
 sig_in=sass
 sig_out=a{ss}
-doc=Get data for an entity
+doc=Get data in cache for an entity
 doc_param_0=jid: entity's bare jid
 doc_param_1=keys: list of keys to get
 doc_param_2=%(doc_profile)s
 doc_return=dictionary of asked key,
- if key doesn't exist, the resulting dictionary will neither have the key
+ if key doesn't exist, the resulting dictionary will not have the key
+
+[getEntitiesData]
+type=method
+category=core
+sig_in=asass
+sig_out=a{sa{ss}}
+doc=Get data in cache for several entities at once
+doc_param_0=jids: list of entities bare jid, or empty list to have all jids in cache
+doc_param_1=keys: list of keys to get
+doc_param_2=%(doc_profile)s
+doc_return=dictionary with jids as keys and dictionary of asked key as values
+ if key doesn't exist for a jid, the resulting dictionary will not have it
 
 [asyncCreateProfile]
 async=
@@ -255,12 +267,13 @@
 doc_param_0=%(doc_profile_key)s
 
 [getContacts]
+async=
 type=method
 category=core
 sig_in=s
 sig_out=a(sa{ss}as)
 param_0_default="@DEFAULT@"
-doc=Return information about all contacts
+doc=Return information about all contacts (the roster)
 doc_param_0=%(doc_profile_key)s
 doc_return=array of tuples with the following values:
  - JID of the contact
@@ -278,7 +291,7 @@
 doc_param_1=%(doc_profile_key)s
 doc_return=array of jids
 
-[getLastResource]
+[getMainResource]
 type=method
 category=core
 sig_in=ss
@@ -287,7 +300,7 @@
 doc=Return the last resource connected for a contact
 doc_param_0=contact_jid: jid of the contact
 doc_param_1=%(doc_profile_key)s
-doc_return=the last resource connected of the contact, or ""
+doc_return=the resource connected of the contact with highest priority, or ""
 
 [getPresenceStatuses]
 type=method
@@ -508,6 +521,7 @@
 doc_param_1=%(doc_profile_key)s
 
 [updateContact]
+async=
 type=method
 category=core
 sig_in=ssass
@@ -520,6 +534,7 @@
 doc_param_3=%(doc_profile_key)s
 
 [delContact]
+async=
 type=method
 category=core
 sig_in=ss
@@ -571,7 +586,7 @@
 type=method
 category=core
 sig_in=si
-sig_out=a(ssasas)
+sig_out=a(ssasasa{ss})
 doc=Get all additional menus
 doc_param_0=language: language in which the menu should be translated (empty string for default)
 doc_param_1=security_limit: %(doc_security_limit)s
@@ -581,6 +596,7 @@
     * NORMAL: Classical application menu
  - menu_path: raw path of the menu
  - menu_path_i18n: translated path of the menu
+ - extra: extra data, like icon name
 
 [getMenuHelp]
 type=method
--- a/src/bridge/bridge_constructor/dbus_frontend_template.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/bridge/bridge_constructor/dbus_frontend_template.py	Wed Mar 18 10:52:28 2015 +0100
@@ -102,10 +102,10 @@
                 async = False
 
                 if kwargs:
-                    if 'callback' in kwargs and 'errback' in kwargs:
+                    if 'callback' in kwargs:
                         async = True
                         _callback = kwargs.pop('callback')
-                        _errback = kwargs.pop('errback')
+                        _errback = kwargs.pop('errback', lambda failure: log.error(unicode(failure)))
                 elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]):
                     async = True
                     args = list(args)
@@ -122,4 +122,5 @@
                 return method(*args, **kwargs)
 
             return getPluginMethod
+
 ##METHODS_PART##
--- a/src/core/constants.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/core/constants.py	Wed Mar 18 10:52:28 2015 +0100
@@ -70,18 +70,36 @@
     MENU_JID_CONTEXT = "JID_CONTEXT"
     MENU_ROSTER_JID_CONTEXT = "ROSTER_JID_CONTEXT"
     MENU_ROSTER_GROUP_CONTEXT = "MENU_ROSTER_GROUP_CONTEXT"
+    MENU_ROOM_OCCUPANT_CONTEXT = "MENU_ROOM_OCCUPANT_CONTEXT"
 
 
     ## Profile and entities ##
     PROF_KEY_NONE = '@NONE@'
     PROF_KEY_DEFAULT = '@DEFAULT@'
     ENTITY_ALL = '@ALL@'
-    ENTITY_LAST_RESOURCE = 'LAST_RESOURCE'
+    ENTITY_ALL_RESOURCES = '@ALL_RESOURCES@'
+    ENTITY_MAIN_RESOURCE = '@MAIN_RESOURCE@'
     ENTITY_CAP_HASH = 'CAP_HASH'
 
 
     ## Messages ##
     MESS_TYPE_INFO = 'info'
+    MESS_TYPE_CHAT = 'chat'
+    MESS_TYPE_ERROR = 'error'
+    MESS_TYPE_GROUPCHAT = 'groupchat'
+    MESS_TYPE_HEADLINE = 'headline'
+    MESS_TYPE_NORMAL = 'normal'
+
+    ## PRESENCE ##
+    PRESENCE_UNAVAILABLE = 'unavailable'
+    PRESENCE_SHOW_AWAY = 'away'
+    PRESENCE_SHOW_CHAT = 'chat'
+    PRESENCE_SHOW_DND = 'dnd'
+    PRESENCE_SHOW_XA = 'xa'
+    PRESENCE_SHOW = 'show'
+    PRESENCE_STATUSES = 'statuses'
+    PRESENCE_PRIORITY = 'priority'
+
 
     ## Configuration ##
     if BaseDirectory:  # skipped when xdg module is not available (should not happen in backend)
@@ -202,14 +220,12 @@
 
     @classmethod
     def bool(cls, value):
-        """@return: bool value for any type"""
-        if isinstance(value, str) or isinstance(value, unicode):  # dbus.String is unicode but not str
-            return value.lower() == cls.BOOL_TRUE
-        return bool(value)
+        """@return (bool): bool value for associated constant"""
+        assert isinstance(value, basestring)
+        return value.lower() == cls.BOOL_TRUE
 
     @classmethod
-    def str(cls, value):
-        """@return: str text value for any type"""
-        if isinstance(value, bool):
-            return cls.BOOL_TRUE if value else cls.BOOL_FALSE
-        return str(value)
+    def boolConst(cls, value):
+        """@return (str): constant associated to bool value"""
+        assert isinstance(value, bool)
+        return cls.BOOL_TRUE if value else cls.BOOL_FALSE
--- a/src/core/exceptions.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/core/exceptions.py	Wed Mar 18 10:52:28 2015 +0100
@@ -93,5 +93,10 @@
 class PasswordError(Exception):
     pass
 
+
+class PermissionError(Exception):
+    pass
+
+
 class SkipHistory(Exception): # used in MessageReceivedTrigger to avoid history writting
     pass
--- a/src/core/sat_main.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/core/sat_main.py	Wed Mar 18 10:52:28 2015 +0100
@@ -83,14 +83,15 @@
         self.bridge.register("getVersion", lambda: C.APP_VERSION)
         self.bridge.register("getProfileName", self.memory.getProfileName)
         self.bridge.register("getProfilesList", self.memory.getProfilesList)
-        self.bridge.register("getEntityData", lambda _jid, keys, profile: self.memory.getEntityData(jid.JID(_jid), keys, profile))
+        self.bridge.register("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile))
+        self.bridge.register("getEntitiesData", self.memory._getEntitiesData)
         self.bridge.register("asyncCreateProfile", self.memory.asyncCreateProfile)
         self.bridge.register("asyncDeleteProfile", self.memory.asyncDeleteProfile)
         self.bridge.register("asyncConnect", self.asyncConnect)
         self.bridge.register("disconnect", self.disconnect)
         self.bridge.register("getContacts", self.getContacts)
         self.bridge.register("getContactsFromGroup", self.getContactsFromGroup)
-        self.bridge.register("getLastResource", self.memory._getLastResource)
+        self.bridge.register("getMainResource", self.memory._getMainResource)
         self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses)
         self.bridge.register("getWaitingSub", self.memory.getWaitingSub)
         self.bridge.register("getWaitingConf", self.getWaitingConf)
@@ -264,9 +265,13 @@
         for plugin in self.plugins.iteritems():
             if plugin[1].is_handler:
                 plugin[1].getHandler(profile).setHandlerParent(current)
-            connected_cb = getattr(plugin[1], "profileConnected", None)
+            connected_cb = getattr(plugin[1], "profileConnected", None) # profile connected is called after client is ready and roster is got
             if connected_cb:
                 plugin_conn_cb.append((plugin[0], connected_cb))
+            try:
+                yield plugin[1].profileConnecting(profile) # profile connecting is called before actually starting client
+            except AttributeError:
+                pass
 
         current.startService()
 
@@ -289,7 +294,7 @@
                                   {'name': plugin_conn_cb[idx][0], 'failure': result})
 
         yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
-        # TODO: mesure time to launch of each plugin
+        # TODO: mesure launch time of each plugin
 
     def _authenticateProfile(self, password, profile):
         """Authenticate the profile.
@@ -331,12 +336,15 @@
 
     def getContacts(self, profile_key):
         client = self.getClient(profile_key)
-        ret = []
-        for item in client.roster.getItems():  # we get all items for client's roster
-            # and convert them to expected format
-            attr = client.roster.getAttributes(item)
-            ret.append([item.jid.userhost(), attr, item.groups])
-        return ret
+        def got_roster(dummy):
+            ret = []
+            for item in client.roster.getItems():  # we get all items for client's roster
+                # and convert them to expected format
+                attr = client.roster.getAttributes(item)
+                ret.append([item.jid.userhost(), attr, item.groups])
+            return ret
+
+        return client.roster.got_roster.addCallback(got_roster)
 
     def getContactsFromGroup(self, group, profile_key):
         client = self.getClient(profile_key)
@@ -456,7 +464,7 @@
     def sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE):
         #FIXME: check validity of recipient
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         client = self.profiles[profile]
         if extra is None:
             extra = {}
@@ -479,7 +487,7 @@
                 # we may have a groupchat message, we check if the we know this jid
                 try:
                     entity_type = self.memory.getEntityData(mess_data["to"], ['type'], profile)["type"]
-                    #FIXME: should entity_type manage ressources ?
+                    #FIXME: should entity_type manage resources ?
                 except (exceptions.UnknownEntityError, KeyError):
                     entity_type = "contact"
 
@@ -497,7 +505,7 @@
             if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile):
                 return defer.succeed(None)
 
-        log.debug(_("Sending jabber message of type [%(type)s] to %(to)s...") % {"type": mess_data["type"], "to": to_jid.full()})
+        log.debug(_(u"Sending message (type {type}, to {to})").format(type=mess_data["type"], to=to_jid.full()))
 
         def cancelErrorTrap(failure):
             """A message sending can be cancelled by a plugin treatment"""
@@ -572,7 +580,7 @@
         if statuses is None:
             statuses = {}
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         priority = int(self.memory.getParamA("Priority", "Connection", profile_key=profile))
         self.profiles[profile].presence.available(to_jid, show, statuses, priority)
         #XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource)
@@ -588,7 +596,7 @@
         @param raw_jid: unicode entity's jid
         @param profile_key: profile"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         to_jid = jid.JID(raw_jid)
         log.debug(_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type': subs_type, 'jid': to_jid.full()})
         if subs_type == "subscribe":
@@ -606,8 +614,8 @@
     def addContact(self, to_jid, profile_key):
         """Add a contact in roster list"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
-        #self.profiles[profile].roster.addItem(to_jid)  #XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
+        assert profile
+        # presence is sufficient, as a roster push will be sent according to RFC 6121 §3.1.2
         self.profiles[profile].presence.subscribe(to_jid)
 
     def _updateContact(self, to_jid_s, name, groups, profile_key):
@@ -616,12 +624,12 @@
     def updateContact(self, to_jid, name, groups, profile_key):
         """update a contact in roster list"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         groups = set(groups)
         roster_item = RosterItem(to_jid)
         roster_item.name = name or None
         roster_item.groups = set(groups)
-        self.profiles[profile].roster.updateItem(roster_item)
+        return self.profiles[profile].roster.setItem(roster_item)
 
     def _delContact(self, to_jid_s, profile_key):
         return self.delContact(jid.JID(to_jid_s), profile_key)
@@ -629,10 +637,9 @@
     def delContact(self, to_jid, profile_key):
         """Remove contact from roster list"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
-        self.profiles[profile].roster.removeItem(to_jid)
-        self.profiles[profile].presence.unsubscribe(to_jid)
-
+        assert profile
+        self.profiles[profile].presence.unsubscribe(to_jid)  # is not asynchronous
+        return self.profiles[profile].roster.removeItem(to_jid)
 
     ## Discovery ##
     # discovery methods are shortcuts to self.memory.disco
@@ -770,6 +777,9 @@
         @profile_key: %(doc_profile_key)s
         @return: a deferred which fire a dict where key can be:
             - xmlui: a XMLUI need to be displayed
+            - validated: if present, can be used to launch a callback, it can have the values
+                - C.BOOL_TRUE
+                - C.BOOL_FALSE
         """
         profile = self.memory.getProfileName(profile_key)
         if not profile:
@@ -848,6 +858,7 @@
 
     def getMenus(self, language='', security_limit=C.NO_SECURITY_LIMIT):
         """Return all menus registered
+
         @param language: language used for translation, or empty string for default
         @param security_limit: %(doc_security_limit)s
         @return: array of tuple with:
@@ -855,7 +866,9 @@
             - menu type
             - raw menu path (array of strings)
             - translated menu path
-
+            - extra (dict(unicode, unicode)): extra data where key can be:
+                - icon: name of the icon to use (TODO)
+                - help_url: link to a page with more complete documentation (TODO)
         """
         ret = []
         for menu_id, menu_data in self._menus.iteritems():
@@ -867,7 +880,8 @@
             languageSwitch(language)
             path_i18n = [_(elt) for elt in path]
             languageSwitch()
-            ret.append((menu_id, type_, path, path_i18n))
+            extra = {} # TODO: manage extra data like icon
+            ret.append((menu_id, type_, path, path_i18n, extra))
 
         return ret
 
--- a/src/core/xmpp.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/core/xmpp.py	Wed Mar 18 10:52:28 2015 +0100
@@ -174,14 +174,42 @@
     def __init__(self, host):
         xmppim.RosterClientProtocol.__init__(self)
         self.host = host
-        self.got_roster = defer.Deferred()
+        self.got_roster = defer.Deferred() # called when roster is received and ready
         #XXX: the two following dicts keep a local copy of the roster
-        self._groups = {}  # map from groups to bare jids: key=group value=set of bare jids
-        self._jids = {}  # map from bare jids to RosterItem: key=jid value=RosterItem
+        self._groups = {}  # map from groups to jids: key=group value=set of jids
+        self._jids = None  # map from jids to RosterItem: key=jid value=RosterItem
 
     def rosterCb(self, roster):
-        for raw_jid, item in roster.iteritems():
-            self.onRosterSet(item)
+        assert roster is not None # FIXME: must be managed with roster versioning
+        self._jids = roster
+        for roster_item in roster.itervalues():
+            self._registerItem(roster_item)
+
+    def _registerItem(self, item):
+        """Register item in local cache
+
+        item must be already registered in self._jids before this method is called
+        @param item (RosterIem): item added
+        """
+        log.debug(u"registering item: {}".format(item.jid.full()))
+        if item.entity.resource:
+            log.warning(u"Received a roster item with a resource, this is not common but not restricted by RFC 6121, this case may be not well tested.")
+        if not item.subscriptionTo and not item.subscriptionFrom and not item.ask:
+            #XXX: current behaviour: we don't want contact in our roster list
+            # if there is no presence subscription
+            # may change in the future
+            self.removeItem(item.jid) # FIXME: to be checked
+            return
+        if not item.subscriptionTo:
+            if not item.subscriptionFrom:
+                log.info(_(u"There's no subscription between you and [{}]!").format(item.jid.full()))
+            else:
+                log.info(_(u"You are not subscribed to [{}]!").format(item.jid.full()))
+        if not item.subscriptionFrom:
+            log.info(_(u"[{}] is not subscribed to you!").format(item.jid.full()))
+
+        for group in item.groups:
+            self._groups.setdefault(group, set()).add(item.entity)
 
     def requestRoster(self):
         """ ask the server for Roster list """
@@ -192,31 +220,9 @@
     def removeItem(self, to_jid):
         """Remove a contact from roster list
         @param to_jid: a JID instance
-        """
-        xmppim.RosterClientProtocol.removeItem(self, to_jid)
-        #TODO: check IQ result
-
-    #XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
-    #def addItem(self, to):
-        #"""Add a contact to roster list"""
-        #xmppim.RosterClientProtocol.addItem(self, to)
-        #TODO: check IQ result"""
-
-    def updateItem(self, roster_item):
+        @return: Deferred
         """
-        Update an item of the contact list.
-
-        @param roster_item: item to update
-        """
-        iq = compat.IQ(self.xmlstream, 'set')
-        iq.addElement((xmppim.NS_ROSTER, 'query'))
-        item = iq.query.addElement('item')
-        item['jid'] = roster_item.jid.userhost()
-        if roster_item.name:
-            item['name'] = roster_item.name
-        for group in roster_item.groups:
-            item.addElement('group', content=group)
-        return iq.send()
+        return xmppim.RosterClientProtocol.removeItem(self, to_jid)
 
     def getAttributes(self, item):
         """Return dictionary of attributes as used in bridge from a RosterItem
@@ -230,72 +236,54 @@
             item_attr['name'] = item.name
         return item_attr
 
-    def onRosterSet(self, item):
-        """Called when a new/update roster item is received"""
-        #TODO: send a signal to frontends
-        if not item.subscriptionTo and not item.subscriptionFrom and not item.ask:
-            #XXX: current behaviour: we don't want contact in our roster list
-            # if there is no presence subscription
-            # may change in the future
-            self.removeItem(item.jid)
-            return
-        log.debug(_("New contact in roster list: %s") % item.jid.full())
-        if not item.subscriptionTo:
-            if not item.subscriptionFrom:
-                log.info(_("There's no subscription between you and [%s]!") % item.jid.full())
-            else:
-                log.info(_("You are not subscribed to [%s]!") % item.jid.full())
-        if not item.subscriptionFrom:
-            log.info(_("[%s] is not subscribed to you!") % item.jid.full())
-        #self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile)
-
-        bare_jid = item.jid.userhostJID()
-        self._jids[bare_jid] = item
-        for group in item.groups:
-            self._groups.setdefault(group, set()).add(bare_jid)
+    def setReceived(self, request):
+        #TODO: implement roster versioning (cf RFC 6121 §2.6)
+        item = request.item
+        self._jids[item.entity] = item
         self.host.bridge.newContact(item.jid.full(), self.getAttributes(item), item.groups, self.parent.profile)
 
-    def onRosterRemove(self, entity):
-        """Called when a roster removal event is received"""
-        print _("removing %s from roster list") % entity.full()
-        bare_jid = entity.userhostJID()
+    def removeReceived(self, request):
+        entity = request.item.entity
+        log.info(u"removing %s from roster list" % entity.full())
 
         # we first remove item from local cache (self._groups and self._jids)
         try:
-            item = self._jids.pop(bare_jid)
+            item = self._jids.pop(entity)
         except KeyError:
-            log.warning("Received a roster remove event for an item not in cache")
+            log.error("Received a roster remove event for an item not in cache ({})".format(entity))
             return
         for group in item.groups:
             try:
                 jids_set = self._groups[group]
-                jids_set.remove(bare_jid)
+                jids_set.remove(entity)
                 if not jids_set:
                     del self._groups[group]
             except KeyError:
                 log.warning("there is not cache for the group [%(groups)s] of the removed roster item [%(jid)s]" %
-                        {"group": group, "jid": bare_jid})
+                        {"group": group, "jid": entity})
 
         # then we send the bridge signal
-        self.host.bridge.contactDeleted(entity.userhost(), self.parent.profile)
+        self.host.bridge.contactDeleted(entity.full(), self.parent.profile)
 
     def getGroups(self):
         """Return a list of groups"""
         return self._groups.keys()
 
-    def getItem(self, jid):
+    def getItem(self, entity_jid):
         """Return RosterItem for a given jid
-        @param jid: jid of the contact
-        @return: RosterItem or None if contact is not in cache"""
-        return self._jids.get(jid.userhostJID(), None)
 
-    def getBareJids(self):
-        """Return all bare jids (as unicode) of the roster"""
+        @param entity_jid: jid of the contact
+        @return: RosterItem or None if contact is not in cache
+        """
+        return self._jids.get(entity_jid, None)
+
+    def getJids(self):
+        """Return all jids of the roster"""
         return self._jids.keys()
 
     def isJidInRoster(self, entity_jid):
         """Return True if jid is in roster"""
-        return entity_jid.userhostJID() in self._jids
+        return entity_jid in self._jids
 
     def getItems(self):
         """Return all items of the roster"""
@@ -305,7 +293,7 @@
         try:
             return self._groups[group]
         except KeyError:
-            raise exceptions.UnknownGroupError
+            raise exceptions.UnknownGroupError(group)
 
 
 class SatPresenceProtocol(xmppim.PresenceClientProtocol):
@@ -320,7 +308,7 @@
         super(SatPresenceProtocol, self).send(obj)
 
     def availableReceived(self, entity, show=None, statuses=None, priority=0):
-        log.debug(_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity': entity, 'show': show, 'statuses': statuses, 'priority': priority})
+        log.debug(_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity': entity, C.PRESENCE_SHOW: show, C.PRESENCE_STATUSES: statuses, C.PRESENCE_PRIORITY: priority})
 
         if not statuses:
             statuses = {}
@@ -333,17 +321,13 @@
                                            int(priority), statuses,
                                            self.parent.profile)
 
-        # uncomment these two lines if you need the trigger
-        #if not self.host.trigger.point("presenceReceived", entity, "unavailable", 0, statuses, self.parent.profile):
-        #    return
-
         # now it's time to notify frontends
         self.host.bridge.presenceUpdate(entity.full(), show or "",
                                         int(priority), statuses,
                                         self.parent.profile)
 
     def unavailableReceived(self, entity, statuses=None):
-        log.debug(_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity': entity, 'statuses': statuses})
+        log.debug(_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity': entity, C.PRESENCE_STATUSES: statuses})
 
         if not statuses:
             statuses = {}
--- a/src/memory/disco.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/memory/disco.py	Wed Mar 18 10:52:28 2015 +0100
@@ -102,18 +102,19 @@
             jid_ = jid.JID(client.jid.host)
         try:
             cap_hash = self.host.memory.getEntityData(jid_, [C.ENTITY_CAP_HASH], client.profile)[C.ENTITY_CAP_HASH]
-            disco_infos = self.hashes[cap_hash]
-            return defer.succeed(disco_infos)
-        except KeyError:
+        except (KeyError, exceptions.UnknownEntityError):
             # capability hash is not available, we'll compute one
             def infosCb(disco_infos):
                 cap_hash = self.generateHash(disco_infos)
                 self.hashes[cap_hash] = disco_infos
-                self.host.memory.updateEntityData(jid_, C.ENTITY_CAP_HASH, cap_hash, client.profile)
+                self.host.memory.updateEntityData(jid_, C.ENTITY_CAP_HASH, cap_hash, profile_key=client.profile)
                 return disco_infos
             d = client.disco.requestInfo(jid_)
             d.addCallback(infosCb)
             return d
+        else:
+            disco_infos = self.hashes[cap_hash]
+            return defer.succeed(disco_infos)
 
     @defer.inlineCallbacks
     def getItems(self, jid_=None, nodeIdentifier='', profile_key=C.PROF_KEY_NONE):
@@ -131,10 +132,10 @@
             try:
                 items = self.host.memory.getEntityData(jid_, ["DISCO_ITEMS"], client.profile)["DISCO_ITEMS"]
                 log.debug("[%s] disco items are in cache" % jid_.full())
-            except KeyError:
+            except (KeyError, exceptions.UnknownEntityError):
                 log.debug("Caching [%s] disco items" % jid_.full())
                 items = yield client.disco.requestItems(jid_, nodeIdentifier)
-                self.host.memory.updateEntityData(jid_, "DISCO_ITEMS", items, client.profile)
+                self.host.memory.updateEntityData(jid_, "DISCO_ITEMS", items, profile_key=client.profile)
         else:
             items = yield client.disco.requestItems(jid_, nodeIdentifier)
 
--- a/src/memory/memory.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/memory/memory.py	Wed Mar 18 10:52:28 2015 +0100
@@ -19,11 +19,14 @@
 
 from sat.core.i18n import _
 
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
 import os.path
+import copy
+from collections import namedtuple
 from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
 from uuid import uuid4
-from sat.core.log import getLogger
-log = getLogger(__name__)
 from twisted.internet import defer, reactor
 from twisted.words.protocols.jabber import jid
 from sat.core import exceptions
@@ -36,6 +39,8 @@
 from sat.tools import config as tools_config
 
 
+PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses'))
+
 class Sessions(object):
     """Sessions are data associated to key used for a temporary moment, with optional profile checking."""
     DEFAULT_TIMEOUT = 600
@@ -212,6 +217,9 @@
         self.host = host
         self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
                                   #     /!\ an entity is not necessarily in roster
+                                  #     main key is bare jid, value is a dict
+                                  #     where main key is resource, or None for bare jid
+        self._key_signals = set() # key which need a signal to frontends when updated
         self.subscriptions = {}
         self.auth_sessions = PasswordSessions()  # remember the authenticated profiles
         self.disco = Discovery(host)
@@ -228,6 +236,8 @@
         d.addCallback(lambda ignore: self.memory_data.load())
         d.chainDeferred(self.initialized)
 
+    ## Configuration ##
+
     def parseMainConf(self):
         """look for main .ini configuration file, and parse it"""
         config = SafeConfigParser(defaults=C.DEFAULT_CONFIG)
@@ -265,6 +275,24 @@
                 log.error(_("Can't load parameters from file: %s") % e)
         return False
 
+    def save_xml(self, filename):
+        """Save parameters template to xml file
+
+        @param filename (str): output file
+        @return: bool: True in case of success
+        """
+        if not filename:
+            return False
+        #TODO: need to encrypt files (at least passwords !) and set permissions
+        filename = os.path.expanduser(filename)
+        try:
+            self.params.save_xml(filename)
+            log.debug(_("Parameters saved to file: %s") % filename)
+            return True
+        except Exception as e:
+            log.error(_("Can't save parameters to file: %s") % e)
+        return False
+
     def load(self):
         """Load parameters and all memory things from db"""
         #parameters data
@@ -275,6 +303,8 @@
         @param profile: %(doc_profile)s"""
         return self.params.loadIndParams(profile)
 
+    ## Profiles/Sessions management ##
+
     def startProfileSession(self, profile):
         """"Iniatialise session for a profile
         @param profile: %(doc_profile)s"""
@@ -309,31 +339,16 @@
         except KeyError:
             log.error(_("Trying to purge roster status cache for a profile not in memory: [%s]") % profile)
 
-    def save_xml(self, filename):
-        """Save parameters template to xml file
-
-        @param filename (str): output file
-        @return: bool: True in case of success
-        """
-        if not filename:
-            return False
-        #TODO: need to encrypt files (at least passwords !) and set permissions
-        filename = os.path.expanduser(filename)
-        try:
-            self.params.save_xml(filename)
-            log.debug(_("Parameters saved to file: %s") % filename)
-            return True
-        except Exception as e:
-            log.error(_("Can't save parameters to file: %s") % e)
-        return False
-
     def getProfilesList(self):
         return self.storage.getProfilesList()
 
     def getProfileName(self, profile_key, return_profile_keys=False):
         """Return name of profile from keyword
+
         @param profile_key: can be the profile name or a keywork (like @DEFAULT@)
-        @return: profile name or None if it doesn't exist"""
+        @param return_profile_keys: if True, return unmanaged profile keys (like "@ALL@"). This keys must be managed by the caller
+        @return: requested profile name or emptry string if it doesn't exist
+        """
         return self.params.getProfileName(profile_key, return_profile_keys)
 
     def asyncCreateProfile(self, name, password=''):
@@ -342,6 +357,10 @@
         @param password: profile password
         @return: Deferred
         """
+        if not name:
+            raise ValueError("Empty profile name")
+        if name[0] == '@':
+            raise ValueError("A profile name can't start with a '@'")
         personal_key = BlockCipher.getRandomKey(base64=True)  # generated once for all and saved in a PersistentDict
         self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=name)  # will be encrypted by setParam
         d = self.params.asyncCreateProfile(name)
@@ -358,6 +377,8 @@
         self.auth_sessions.profileDelUnique(name)
         return self.params.asyncDeleteProfile(name, force)
 
+    ## History ##
+
     def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE):
         assert profile != C.PROF_KEY_NONE
         if extra is None:
@@ -386,163 +407,305 @@
             return defer.succeed([])
         return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
 
-    def _getLastResource(self, jid_s, profile_key):
-        jid_ = jid.JID(jid_s)
-        return self.getLastResource(jid_, profile_key) or ""
-
-    def getLastResource(self, entity_jid, profile_key):
-        """Return the last resource used by an entity
-        @param entity_jid: entity jid
-        @param profile_key: %(doc_profile_key)s"""
-        data = self.getEntityData(entity_jid.userhostJID(), [C.ENTITY_LAST_RESOURCE], profile_key)
-        try:
-            return data[C.ENTITY_LAST_RESOURCE]
-        except KeyError:
-            return None
+    ## Statuses ##
 
     def _getPresenceStatuses(self, profile_key):
         ret = self.getPresenceStatuses(profile_key)
         return {entity.full():data for entity, data in ret.iteritems()}
 
     def getPresenceStatuses(self, profile_key):
-        """Get all the presence status of a profile
+        """Get all the presence statuses of a profile
+
         @param profile_key: %(doc_profile_key)s
         @return: presence data: key=entity JID, value=presence data for this entity
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
+        profile_cache = self._getProfileCache(profile_key)
         entities_presence = {}
-        for entity in self._entities_cache[profile]:
-            if "presence" in self._entities_cache[profile][entity]:
-                entities_presence[entity] = self._entities_cache[profile][entity]["presence"]
 
-        log.debug("Memory getPresenceStatus (%s)" % entities_presence)
+        for entity_jid, entity_data in profile_cache.iteritems():
+            for resource, resource_data in entity_data.iteritems():
+                full_jid = copy.copy(entity_jid)
+                full_jid.resource = resource
+                try:
+                    presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
+                except KeyError:
+                    continue
+                entities_presence.setdefault(entity_jid, {})[resource or ''] = presence_data
+
         return entities_presence
 
-    def isContactConnected(self, entity_jid, profile_key):
-        """Tell from the presence information if the given contact is connected.
+    def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
+        """Change the presence status of an entity
+
+        @param entity_jid: jid.JID of the entity
+        @param show: show status
+        @param priority: priority
+        @param statuses: dictionary of statuses
+        @param profile_key: %(doc_profile_key)s
+        """
+        presence_data = PresenceTuple(show, priority, statuses)
+        self.updateEntityData(entity_jid, "presence", presence_data, profile_key=profile_key)
+        if entity_jid.resource and show != C.PRESENCE_UNAVAILABLE:
+            # If a resource is available, bare jid should not have presence information
+            try:
+                self.delEntityDatum(entity_jid.userhostJID(), "presence", profile_key)
+            except (KeyError, exceptions.UnknownEntityError):
+                pass
+
+    ## Resources ##
+
+    def _getAllResource(self, jid_s, profile_key):
+        jid_ = jid.JID(jid_s)
+        return self.getAllResources(jid_, profile_key)
+
+    def getAllResources(self, entity_jid, profile_key):
+        """Return all resource from jid for which we have had data in this session
+
+        @param entity_jid: bare jid of the entit
+        @param profile_key: %(doc_profile_key)s
+        return (list[unicode]): list of resources
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        """
+        if entity_jid.resource:
+            raise ValueError("getAllResources must be used with a bare jid (got {})".format(entity_jid))
+        profile_cache = self._getProfileCache(profile_key)
+        try:
+            entity_data = profile_cache[entity_jid.userhostJID()]
+        except KeyError:
+            raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
+        resources= set(entity_data.keys())
+        resources.discard(None)
+        return resources
+
+    def getAvailableResources(self, entity_jid, profile_key):
+        """Return available resource for entity_jid
+
+        This method differs from getAllResources by returning only available resources
+        @param entity_jid: bare jid of the entit
+        @param profile_key: %(doc_profile_key)s
+        return (list[unicode]): list of available resources
 
-        @param entity_jid (JID): the entity to check
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        """
+        available = []
+        for resource in self.getAllResources(entity_jid, profile_key):
+            full_jid = copy.copy(entity_jid)
+            full_jid.resource = resource
+            try:
+                presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
+            except KeyError:
+                log.debug("Can't get presence data for {}".format(full_jid))
+            else:
+                if presence_data.show != C.PRESENCE_UNAVAILABLE:
+                    available.append(resource)
+        return available
+
+    def _getMainResource(self, jid_s, profile_key):
+        jid_ = jid.JID(jid_s)
+        return self.getMainResource(jid_, profile_key) or ""
+
+    def getMainResource(self, entity_jid, profile_key):
+        """Return the main resource used by an entity
+
+        @param entity_jid: bare entity jid
         @param profile_key: %(doc_profile_key)s
-        @return: boolean
+        @return (unicode): main resource or None
+        """
+        if entity_jid.resource:
+            raise ValueError("getMainResource must be used with a bare jid (got {})".format(entity_jid))
+        resources = self.getAllResources(entity_jid, profile_key)
+        priority_resources = []
+        for resource in resources:
+            full_jid = copy.copy(entity_jid)
+            full_jid.resource = resource
+            try:
+                presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
+            except KeyError:
+                log.debug("No presence information for {}".format(full_jid))
+                continue
+            priority_resources.append((resource, presence_data.priority))
+        try:
+            return max(priority_resources, key=lambda res_tuple: res_tuple[1])[0]
+        except ValueError:
+            log.warning("No resource found at all for {}".format(entity_jid))
+            return None
+
+    ## Entities data ##
+
+    def _getProfileCache(self, profile_key):
+        """Check profile validity and return its cache
+
+        @param profile: %(doc_profile_key)_s
+        @return (dict): profile cache
+
+        @raise exceptions.ProfileUnknownError: if profile doesn't exist
+        @raise exceptions.ProfileNotInCacheError: if there is no cache for this profile
         """
         profile = self.getProfileName(profile_key)
         if not profile:
             raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
         try:
-            presence = self._entities_cache[profile][entity_jid]['presence']
-            return len([True for status in presence.values() if status[0] != 'unavailable']) > 0
+            profile_cache = self._entities_cache[profile]
         except KeyError:
-            return False
+            raise exceptions.ProfileNotInCacheError
+        return profile_cache
+
+    def setSignalOnUpdate(self, key, signal=True):
+        """Set a signal flag on the key
+
+        When the key will be updated, a signal will be sent to frontends
+        @param key: key to signal
+        @param signal(boolean): if True, do the signal
+        """
+        if signal:
+            self._key_signals.add(key)
+        else:
+            self._key_signals.discard(key)
+
+    def getAllEntitiesIter(self, with_bare=False, profile_key=C.PROF_KEY_NONE):
+        """Return an iterator of full jids of all entities in cache
 
-    def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
-        """Change the presence status of an entity
-        @param entity_jid: jid.JID of the entity
-        @param show: show status
-        @param priotity: priotity
-        @param statuses: dictionary of statuses
+        @param with_bare: if True, include bare jids
+        @param profile_key: %(doc_profile_key)s
+        @return (list[unicode]): list of jids
+        """
+        profile_cache = self._getProfileCache(profile_key)
+        # we construct a list of all known full jids (bare jid of entities x resources)
+        for bare_jid, entity_data in profile_cache.iteritems():
+            for resource in entity_data.iterkeys():
+                if resource is None:
+                    continue
+                full_jid = copy.copy(bare_jid)
+                full_jid.resource = resource
+                yield full_jid
+
+    def updateEntityData(self, entity_jid, key, value, silent=False, profile_key=C.PROF_KEY_NONE):
+        """Set a misc data for an entity
+
+        If key was registered with setSignalOnUpdate, a signal will be sent to frontends
+        @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
+                           C.ENTITY_ALL for all entities (all resources + bare jids)
+        @param key: key to set (eg: "type")
+        @param value: value for this key (eg: "chatroom")
+        @param silent(bool): if True, doesn't send signal to frontend, even there is a signal flag (see setSignalOnUpdate)
         @param profile_key: %(doc_profile_key)s
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
-        resource = entity_jid.resource
-        if resource:
-            try:
-                type_ = self.getEntityDatum(entity_jid.userhostJID(), 'type', profile)
-            except KeyError:
-                type_ = 'contact'
-            if type_ != 'chatroom':
-                self.updateEntityData(entity_jid.userhostJID(), C.ENTITY_LAST_RESOURCE, resource, profile)
-        entity_data.setdefault("presence", {})[resource or ''] = (show, priority, statuses)
+        profile_cache = self._getProfileCache(profile_key)
+        if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
+            entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
+        else:
+            entities = (entity_jid,)
 
-    def _getEntitiesData(self, entity_jid, profile):
-        """Get data dictionary for entities
-        @param entity_jid: JID of the entity, or C.ENTITY_ALL for all entities)
-        @param profile: %(doc_profile)s
-        @return: entities_data (key=jid, value=entity_data)
-        @raise: exceptions.ProfileNotInCacheError if profile is not in cache
-        """
-        if not profile in self._entities_cache:
-            raise exceptions.ProfileNotInCacheError
-        if entity_jid == C.ENTITY_ALL:
-            entities_data = self._entities_cache[profile]
-        else:
-            entity_data = self._entities_cache[profile].setdefault(entity_jid, {})
-            entities_data = {entity_jid: entity_data}
-        return entities_data
+        for jid_ in entities:
+            entity_data = profile_cache.setdefault(jid_.userhostJID(),{}).setdefault(jid_.resource, {})
 
-    def _updateEntityResources(self, entity_jid, profile):
-        """Add a known resource to bare entity_jid cache
-        @param entity_jid: full entity_jid (with resource)
-        @param profile: %(doc_profile)s
-        """
-        assert(entity_jid.resource)
-        entity_data = self._getEntitiesData(entity_jid.userhostJID(), profile)[entity_jid.userhostJID()]
-        resources = entity_data.setdefault('resources', set())
-        resources.add(entity_jid.resource)
+            entity_data[key] = value
+            if key in self._key_signals and not silent:
+                if not isinstance(value, basestring):
+                    log.error(u"Setting a non string value ({}) for a key ({}) which has a signal flag".format(value, key))
+                else:
+                    self.host.bridge.entityDataUpdated(jid_.full(), key, value, self.getProfileName(profile_key))
 
-    def updateEntityData(self, entity_jid, key, value, profile_key):
-        """Set a misc data for an entity
-        @param entity_jid: JID of the entity, or C.ENTITY_ALL to update all entities)
-        @param key: key to set (eg: "type")
-        @param value: value for this key (eg: "chatroom")
-        @param profile_key: %(doc_profile_key)s
-        """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        entities_data = self._getEntitiesData(entity_jid, profile)
-        if entity_jid != C.ENTITY_ALL and entity_jid.resource:
-            self._updateEntityResources(entity_jid, profile)
+    def delEntityDatum(self, entity_jid, key, profile_key):
+        """Delete a data for an entity
 
-        for jid_ in entities_data:
-            entity_data = entities_data[jid_]
-            if value == C.PROF_KEY_NONE and key in entity_data:
-                del entity_data[key]
-            else:
-                entity_data[key] = value
-            if isinstance(value, basestring):
-                self.host.bridge.entityDataUpdated(jid_.full(), key, value, profile)
-
-    def delEntityData(self, entity_jid, key, profile_key):
-        """Delete data for an entity
-        @param entity_jid: JID of the entity, or C.ENTITY_ALL to delete data from all entities)
+        @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
+                           C.ENTITY_ALL for all entities (all resources + bare jids)
         @param key: key to delete (eg: "type")
         @param profile_key: %(doc_profile_key)s
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        @raise KeyError: key is not in cache
         """
-        entities_data = self._getEntitiesData(entity_jid, profile_key)
-        for entity_jid in entities_data:
-            entity_data = entities_data[entity_jid]
+        profile_cache = self._getProfileCache(profile_key)
+        if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
+            entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
+        else:
+            entities = (entity_jid,)
+
+        for jid_ in entities:
+            try:
+                entity_data = profile_cache[jid_.userhostJID()][jid_.resource]
+            except KeyError:
+                raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(jid_))
             try:
                 del entity_data[key]
-            except KeyError:
-                log.debug("Key [%s] doesn't exist for [%s] in entities_cache" % (key, entity_jid.full()))
+            except KeyError as e:
+                if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
+                    continue # we ignore KeyError when deleting keys from several entities
+                else:
+                    raise e
 
-    def getEntityData(self, entity_jid, keys_list, profile_key):
-        """Get a list of cached values for entity
+    def _getEntitiesData(self, entities_jids, keys_list, profile_key):
+        ret = self.getEntitiesData([jid.JID(jid_) for jid_ in entities_jids], keys_list, profile_key)
+        return {jid_.full(): data for jid_, data in ret.iteritems()}
 
-        @param entity_jid: JID of the entity
-        @param keys_list (iterable): list of keys to get, empty list for everything
+    def getEntitiesData(self, entities_jids, keys_list=None, profile_key=C.PROF_KEY_NONE):
+        """Get a list of cached values for several entities at once
+
+        @param entities_jids: jids of the entities, or empty list for all entities in cache
+        @param keys_list (iterable,None): list of keys to get, None for everything
         @param profile_key: %(doc_profile_key)s
         @return: dict withs values for each key in keys_list.
                  if there is no value of a given key, resulting dict will
                  have nothing with that key nether
-        @raise: exceptions.UnknownEntityError if entity is not in cache
+                 if an entity doesn't exist in cache, it will not appear
+                 in resulting dict
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
-        if not keys_list:
+        def fillEntityData(entity_cache_data):
+            entity_data = {}
+            if keys_list is None:
+                entity_data = entity_cache_data
+            else:
+                for key in keys_list:
+                    try:
+                        entity_data[key] = entity_cache_data[key]
+                    except KeyError:
+                        continue
             return entity_data
-        ret = {}
-        for key in keys_list:
-            if key in entity_data:
-                ret[key] = entity_data[key]
-        return ret
+
+        profile_cache = self._getProfileCache(profile_key)
+        ret_data = {}
+        if entities_jids:
+            for entity in entities_jids:
+                try:
+                    entity_cache_data = profile_cache[entity.userhostJID()][entity.resource]
+                except KeyError:
+                    continue
+                ret_data[entity.full()] = fillEntityData(entity_cache_data, keys_list)
+        else:
+            for bare_jid, data in profile_cache.iteritems():
+                for resource, entity_cache_data in data.iteritems():
+                    full_jid = copy.copy(bare_jid)
+                    full_jid.resource = resource
+                    ret_data[full_jid] = fillEntityData(entity_cache_data)
+
+        return ret_data
+
+    def getEntityData(self, entity_jid, keys_list=None, profile_key=C.PROF_KEY_NONE):
+        """Get a list of cached values for entity
+
+        @param entity_jid: JID of the entity
+        @param keys_list (iterable,None): list of keys to get, None for everything
+        @param profile_key: %(doc_profile_key)s
+        @return: dict withs values for each key in keys_list.
+                 if there is no value of a given key, resulting dict will
+                 have nothing with that key nether
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        """
+        profile_cache = self._getProfileCache(profile_key)
+        try:
+            entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource]
+        except KeyError:
+            raise exceptions.UnknownEntityError(u"Entity {} not in cache (was requesting {})".format(entity_jid, keys_list))
+        if keys_list is None:
+            return entity_data
+
+        return {key: entity_data[key] for key in keys_list if key in entity_data}
 
     def getEntityDatum(self, entity_jid, key, profile_key):
         """Get a datum from entity
@@ -552,34 +715,36 @@
         @param profile_key: %(doc_profile_key)s
         @return: requested value
 
-        @raise: exceptions.UnknownEntityError if entity is not in cache
-        @raise: KeyError if there is no value for this key and this entity
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        @raise KeyError: if there is no value for this key and this entity
         """
         return self.getEntityData(entity_jid, (key,), profile_key)[key]
 
     def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
-        """Remove cached data for entity
+        """Remove all cached data for entity
+
         @param entity_jid: JID of the entity to delete
-        @param delete_all_resources: if True also delete all known resources form cache
+        @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case)
         @param profile_key: %(doc_profile_key)s
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        to_delete = set([entity_jid])
+        profile_cache = self._getProfileCache(profile_key)
 
         if delete_all_resources:
             if entity_jid.resource:
                 raise ValueError(_("Need a bare jid to delete all resources"))
-            entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
-            resources = entity_data.setdefault('resources', set())
-            to_delete.update([jid.JID("%s/%s" % (entity_jid.userhost(), resource)) for resource in resources])
+            try:
+                del profile_cache[entity_jid]
+            except KeyError:
+                raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
+        else:
+            try:
+                del profile_cache[entity_jid.userhostJID()][entity_jid.resource]
+            except KeyError:
+                raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
 
-        for entity in to_delete:
-            try:
-                del self._entities_cache[profile][entity]
-            except KeyError:
-                log.debug("Can't delete entity [%s]: not in cache" % entity.full())
+    ## Encryption ##
 
     def encryptValue(self, value, profile):
         """Encrypt a value for the given profile. The personal key must be loaded
@@ -637,6 +802,8 @@
         d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
         return d.addCallback(gotIndMemory).addCallback(done)
 
+    ## Subscription requests ##
+
     def addWaitingSub(self, type_, entity_jid, profile_key):
         """Called when a subcription request is received"""
         profile = self.getProfileName(profile_key)
@@ -663,6 +830,8 @@
 
         return self.subscriptions[profile]
 
+    ## Parameters ##
+
     def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
         return self.params.getStringParamA(name, category, attr, profile_key)
 
@@ -698,3 +867,21 @@
 
     def setDefault(self, name, category, callback, errback=None):
         return self.params.setDefault(name, category, callback, errback)
+
+    ## Misc ##
+
+    def isEntityAvailable(self, entity_jid, profile_key):
+        """Tell from the presence information if the given entity is available.
+
+        @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested)
+        @param profile_key: %(doc_profile_key)s
+        @return (bool): True if entity is available
+        """
+        if not entity_jid.resource:
+            return bool(self.getAvailableResources) # is any resource is available, entity is available
+        try:
+            presence_data = self.getEntityDatum(entity_jid, "presence", profile_key)
+        except KeyError:
+            log.debug("No presence information for {}".format(entity_jid))
+            return False
+        return presence_data.show != C.PRESENCE_UNAVAILABLE
--- a/src/memory/params.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/memory/params.py	Wed Mar 18 10:52:28 2015 +0100
@@ -49,14 +49,14 @@
             <param name="%(show_empty_groups)s" label="%(show_empty_groups_label)s" value="true" type="bool" security="0" />
         </category>
         <category name="Connection" label="%(category_connection)s">
-            <param name="JabberID" value="name@example.org/SàT" type="string" />
+            <param name="JabberID" value="name@example.org/SàT" type="string" security="0" />
             <param name="Password" value="" type="password" />
             <param name="Priority" value="50" type="int" constraint="-128;127"/>
             <param name="%(force_server_param)s" value="" type="string" />
             <param name="%(force_port_param)s" value="" type="int" constraint="1;65535" />
             <param name="NewAccount" label="%(new_account_label)s" type="button" callback_id="registerNewAccount"/>
-            <param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" />
-            <param name="autodisconnect" label="%(autodisconnect_label)s" value="false"  type="bool" />
+            <param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" security="0" />
+            <param name="autodisconnect" label="%(autodisconnect_label)s" value="false"  type="bool" security="0" />
         </category>
         <category name="Misc" label="%(category_misc)s">
             <param name="Watched" value="test@Jabber.goffi.int" type="string" />
@@ -179,7 +179,7 @@
                 return defer.fail(Failure(exceptions.ConnectedProfileError))
         return self.storage.deleteProfile(profile)
 
-    def getProfileName(self, profile_key, return_profile_keys = False):
+    def getProfileName(self, profile_key, return_profile_keys=False):
         """return profile according to profile_key
 
         @param profile_key: profile name or key which can be
@@ -473,12 +473,12 @@
         node = self._getParamNode(name, category)
         if not node:
             log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
-            return defer.succeed(None)
+            raise ValueError("Requested param doesn't exist")
 
         if not self.checkSecurityLimit(node[1], security_limit):
             log.warning(_("Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!"
                       % {'param': name, 'cat': category}))
-            return defer.succeed(None)
+            raise exceptions.PermissionError
 
         if node[0] == C.GENERAL:
             value = self._getParam(category, name, C.GENERAL)
--- a/src/memory/persistent.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/memory/persistent.py	Wed Mar 18 10:52:28 2015 +0100
@@ -53,6 +53,12 @@
             d = self.storage.loadIndPrivates(self._cache, self.namespace, self.profile)
         return d.addCallback(lambda dummy: self)
 
+    def iteritems(self):
+        return self._cache.iteritems()
+
+    def items(self):
+        return self._cache.items()
+
     def __repr__(self):
         return self._cache.__repr__()
 
@@ -112,6 +118,13 @@
     def get(self, key, default=None):
         return self._cache.get(key, default)
 
+    def setdefault(self, key, default):
+        try:
+            return self._cache[key]
+        except:
+            self.__setitem__(key, default)
+            return default
+
     def force(self, name):
         """Force saving of an attribute to storage
         @return: deferred fired when data is actually saved"""
--- a/src/plugins/plugin_exp_parrot.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_exp_parrot.py	Wed Mar 18 10:52:28 2015 +0100
@@ -91,7 +91,7 @@
                     entity_type = "contact"
                 if entity_type == 'chatroom':
                     src_txt = from_jid.resource
-                    if src_txt == self.host.plugins["XEP-0045"].getRoomNick(from_jid.userhost(), profile):
+                    if src_txt == self.host.plugins["XEP-0045"].getRoomNick(from_jid.userhostJID(), profile):
                         #we won't repeat our own messages
                         return True
                 else:
--- a/src/plugins/plugin_misc_quiz.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_misc_quiz.py	Wed Mar 18 10:52:28 2015 +0100
@@ -58,9 +58,9 @@
         log.info(_("Plugin Quiz initialization"))
         self.inheritFromRoomGame(host)
         RoomGame._init_(self, host, PLUGIN_INFO, (NS_QG, QG_TAG), game_init={'stage': None}, player_init={'score': 0})
-        host.bridge.addMethod("quizGameLaunch", ".plugin", in_sign='asss', out_sign='', method=self.prepareRoom)  # args: players, room_jid, profile
-        host.bridge.addMethod("quizGameCreate", ".plugin", in_sign='sass', out_sign='', method=self.createGame)  # args: room_jid, players, profile
-        host.bridge.addMethod("quizGameReady", ".plugin", in_sign='sss', out_sign='', method=self.playerReady)  # args: player, referee, profile
+        host.bridge.addMethod("quizGameLaunch", ".plugin", in_sign='asss', out_sign='', method=self._prepareRoom)  # args: players, room_jid, profile
+        host.bridge.addMethod("quizGameCreate", ".plugin", in_sign='sass', out_sign='', method=self._createGame)  # args: room_jid, players, profile
+        host.bridge.addMethod("quizGameReady", ".plugin", in_sign='sss', out_sign='', method=self._playerReady)  # args: player, referee, profile
         host.bridge.addMethod("quizGameAnswer", ".plugin", in_sign='ssss', out_sign='', method=self.playerAnswer)
         host.bridge.addSignal("quizGameStarted", ".plugin", signature='ssass')  # args: room_jid, referee, players, profile
         host.bridge.addSignal("quizGameNew", ".plugin",
@@ -192,7 +192,7 @@
 
     def timerExpired(self, room_jid, profile):
         """Called when nobody answered the question in time"""
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         game_data['stage'] = 'expired'
         mess = self.createGameElt(room_jid)
         expired_elt = mess.firstChildElement().addElement('timer_expired')
@@ -201,7 +201,7 @@
 
     def pauseTimer(self, room_jid):
         """Stop the timer and save the time left"""
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         left = max(0, game_data["timer"].getTime() - time())
         game_data['timer'].cancel()
         game_data['time_left'] = int(left)
@@ -210,7 +210,7 @@
 
     def restartTimer(self, room_jid, profile):
         """Restart a timer with the saved time"""
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         assert game_data['time_left'] is not None
         mess = self.createGameElt(room_jid)
         restarted_elt = mess.firstChildElement().addElement('timer_restarted')
@@ -223,7 +223,7 @@
 
     def askQuestion(self, room_jid, profile):
         """Ask a new question"""
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         game_data['stage'] = "question"
         game_data['question_id'] = "1"
         timer = 30
@@ -235,7 +235,7 @@
 
     def checkAnswer(self, room_jid, player, answer, profile):
         """Check if the answer given is right"""
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         players_data = game_data['players_data']
         good_answer = game_data['question_id'] == "1" and answer == "42"
         players_data[player]['score'] += 1 if good_answer else -1
@@ -264,7 +264,7 @@
         from_jid = jid.JID(mess_elt['from'])
         room_jid = jid.JID(from_jid.userhost())
         game_elt = mess_elt.firstChildElement()
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         if 'players_data' in game_data:
             players_data = game_data['players_data']
 
@@ -278,8 +278,8 @@
 
             elif elt.name == 'player_ready':  # ready to play
                 player = elt['player']
-                status = self.games[room_jid.userhost()]['status']
-                nb_players = len(self.games[room_jid.userhost()]['players'])
+                status = self.games[room_jid]['status']
+                nb_players = len(self.games[room_jid]['players'])
                 status[player] = 'ready'
                 log.debug(_('Player %(player)s is ready to start [status: %(status)s]') % {'player': player, 'status': status})
                 if status.values().count('ready') == nb_players:  # everybody is ready, we can start the game
@@ -294,7 +294,7 @@
             elif elt.name == 'player_answer':
                 player = elt['player']
                 pause = game_data['stage'] == 'question'  # we pause the game only if we are have a question at the moment
-                #we first send a buzzer message
+                # we first send a buzzer message
                 mess = self.createGameElt(room_jid)
                 buzzer_elt = mess.firstChildElement().addElement('player_buzzed')
                 buzzer_elt['player'] = player
@@ -302,7 +302,7 @@
                 self.host.profiles[profile].xmlstream.send(mess)
                 if pause:
                     self.pauseTimer(room_jid)
-                    #and we send the player answer
+                    # and we send the player answer
                     mess = self.createGameElt(room_jid)
                     _answer = unicode(elt)
                     say_elt = mess.firstChildElement().addElement('player_says')
--- a/src/plugins/plugin_misc_radiocol.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_misc_radiocol.py	Wed Mar 18 10:52:28 2015 +0100
@@ -67,10 +67,10 @@
         log.info(_("Radio collective initialization"))
         self.inheritFromRoomGame(host)
         RoomGame._init_(self, host, PLUGIN_INFO, (NC_RADIOCOL, RADIOC_TAG),
-                          game_init={'queue': [], 'upload': True, 'playing': None, 'playing_time': 0, 'to_delete': {}})
+                        game_init={'queue': [], 'upload': True, 'playing': None, 'playing_time': 0, 'to_delete': {}})
         self.host = host
-        host.bridge.addMethod("radiocolLaunch", ".plugin", in_sign='asss', out_sign='', method=self.prepareRoom, async=True)
-        host.bridge.addMethod("radiocolCreate", ".plugin", in_sign='sass', out_sign='', method=self.createGame)
+        host.bridge.addMethod("radiocolLaunch", ".plugin", in_sign='asss', out_sign='', method=self._prepareRoom, async=True)
+        host.bridge.addMethod("radiocolCreate", ".plugin", in_sign='sass', out_sign='', method=self._createGame)
         host.bridge.addMethod("radiocolSongAdded", ".plugin", in_sign='sss', out_sign='', method=self.radiocolSongAdded, async=True)
         host.bridge.addSignal("radiocolPlayers", ".plugin", signature='ssass')  # room_jid, referee, players, profile
         host.bridge.addSignal("radiocolStarted", ".plugin", signature='ssasais')  # room_jid, referee, players, [QUEUE_TO_START, QUEUE_LIMIT], profile
@@ -96,7 +96,7 @@
         @param profile_key: %(doc_profile_key)s
         @return: a Deferred instance
         """
-        #XXX: this is a Q&D way for the proof of concept. In the future, the song should
+        # XXX: this is a Q&D way for the proof of concept. In the future, the song should
         #     be streamed to the backend using XMPP file copy
         #     Here we cheat because we know we are on the same host, and we don't
         #     check data. Referee will have to parse the song himself to check it
@@ -115,7 +115,7 @@
             else:
                 song = OggVorbis(song_path)
         except (OggVorbisHeaderError, HeaderNotFoundError):
-            #this file is not ogg vorbis nor mp3, we reject it
+            # this file is not ogg vorbis nor mp3, we reject it
             self.deleteFile(song_path)  # FIXME: same host trick (see note above)
             return defer.fail(exceptions.DataError(D_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")))
 
@@ -125,16 +125,16 @@
                  'album': song.get("album", ["Unknown"])[0],
                  'length': str(song.info.length)
                  }
-        radio_data = self.games[jid.JID(referee).userhost()]  # FIXME: referee comes from Libervia's client side, it's unsecure
+        radio_data = self.games[jid.JID(referee).userhostJID()]  # FIXME: referee comes from Libervia's client side, it's unsecure
         radio_data['to_delete'][attrs['filename']] = song_path  # FIXME: works only because of the same host trick, see the note under the docstring
         return self.send(jid.JID(referee), ('', 'song_added'), attrs, profile=profile)
 
     def playNext(self, room_jid, profile):
         """"Play next song in queue if exists, and put a timer
         which trigger after the song has been played to play next one"""
-        #TODO: songs need to be erased once played or found invalids
+        # TODO: songs need to be erased once played or found invalids
         #      ==> unlink done the Q&D way with the same host trick (see above)
-        radio_data = self.games[room_jid.userhost()]
+        radio_data = self.games[room_jid]
         if len(radio_data['players']) == 0:
             log.debug(_('No more participants in the radiocol: cleaning data'))
             radio_data['queue'] = []
@@ -143,7 +143,7 @@
             radio_data['to_delete'] = {}
         queue = radio_data['queue']
         if not queue:
-            #nothing left to play, we need to wait for uploads
+            # nothing left to play, we need to wait for uploads
             radio_data['playing'] = None
             return
         song = queue.pop(0)
@@ -153,12 +153,12 @@
         radio_data['playing_time'] = time.time()
 
         if not radio_data['upload'] and len(queue) < QUEUE_LIMIT:
-            #upload is blocked and we now have resources to get more, we reactivate it
+            # upload is blocked and we now have resources to get more, we reactivate it
             self.send(room_jid, ('', 'upload_ok'), profile=profile)
             radio_data['upload'] = True
 
         reactor.callLater(length, self.playNext, room_jid, profile)
-        #we wait more than the song length to delete the file, to manage poorly reactive networks/clients
+        # we wait more than the song length to delete the file, to manage poorly reactive networks/clients
         reactor.callLater(length + 90, self.deleteFile, filename, radio_data)  # FIXME: same host trick (see above)
 
     def deleteFile(self, filename, radio_data=None):
@@ -185,17 +185,17 @@
 
     def room_game_cmd(self, mess_elt, profile):
         from_jid = jid.JID(mess_elt['from'])
-        room_jid = jid.JID(from_jid.userhost())
-        nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid.userhost(), profile)
+        room_jid = from_jid.userhostJID()
+        nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile)
 
         radio_elt = mess_elt.firstChildElement()
-        radio_data = self.games[room_jid.userhost()]
+        radio_data = self.games[room_jid]
         if 'queue' in radio_data:
             queue = radio_data['queue']
 
-        from_referee = self.isReferee(room_jid.userhost(), from_jid.resource)
-        to_referee = self.isReferee(room_jid.userhost(), jid.JID(mess_elt['to']).user)
-        is_player = self.isPlayer(room_jid.userhost(), nick)
+        from_referee = self.isReferee(room_jid, from_jid.resource)
+        to_referee = self.isReferee(room_jid, jid.JID(mess_elt['to']).user)
+        is_player = self.isPlayer(room_jid, nick)
         for elt in radio_elt.elements():
             if not from_referee and not (to_referee and elt.name == 'song_added'):
                 continue  # sender must be referee, expect when a song is submitted
@@ -219,20 +219,20 @@
             elif elt.name == 'upload_ok':
                 self.host.bridge.radiocolUploadOk(room_jid.userhost(), profile)
             elif elt.name == 'song_added':  # a song has been added
-                #FIXME: we are KISS for the proof of concept: every song is added, to a limit of 3 in queue.
+                # FIXME: we are KISS for the proof of concept: every song is added, to a limit of 3 in queue.
                 #       Need to manage some sort of rules to allow peoples to send songs
                 if len(queue) >= QUEUE_LIMIT:
-                    #there are already too many songs in queue, we reject this one
-                    #FIXME: add an error code
+                    # there are already too many songs in queue, we reject this one
+                    # FIXME: add an error code
                     self.send(from_jid, ('', 'song_rejected'), {'reason': "Too many songs in queue"}, profile=profile)
                     return
 
-                #The song is accepted and added in queue
+                # The song is accepted and added in queue
                 preload_elt = self.__create_preload_elt(from_jid.resource, elt)
                 queue.append(preload_elt)
 
                 if len(queue) >= QUEUE_LIMIT:
-                    #We are at the limit, we refuse new upload until next play
+                    # We are at the limit, we refuse new upload until next play
                     self.send(room_jid, ('', 'no_upload'), profile=profile)
                     radio_data['upload'] = False
 
@@ -244,8 +244,8 @@
             else:
                 log.error(_('Unmanaged game element: %s') % elt.name)
 
-    def getSyncDataForPlayer(self, room_jid_s, nick):
-        game_data = self.games[room_jid_s]
+    def getSyncDataForPlayer(self, room_jid, nick):
+        game_data = self.games[room_jid]
         elements = []
         if game_data['playing']:
             preload = copy.deepcopy(game_data['playing'])
--- a/src/plugins/plugin_misc_room_game.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_misc_room_game.py	Wed Mar 18 10:52:28 2015 +0100
@@ -21,7 +21,7 @@
 from sat.core.constants import Const as C
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from twisted.words.protocols.jabber.jid import JID
+from twisted.words.protocols.jabber import jid
 from twisted.words.xish import domish
 from twisted.internet import defer
 from time import time
@@ -51,7 +51,7 @@
 class RoomGame(object):
     """This class is used to help launching a MUC game.
 
-    Bridge methods callbacks: prepareRoom, playerReady, createGame
+    Bridge methods callbacks: _prepareRoom, _playerReady, _createGame
     Triggered methods: userJoinedTrigger, userLeftTrigger
     Also called from subclasses: newRound
 
@@ -107,7 +107,7 @@
         self.game_init = game_init
         self.player_init = player_init
         self.games = {}
-        self.invitations = {}  # list of couple (x, y) with x the time and y a list of users
+        self.invitations = {}  # values are a couple (x, y) with x the time and y a list of users
 
         # These are the default settings, which can be overwritten by child class after initialization
         self.invite_mode = self.FROM_PLAYERS if self.player_init == {} else self.FROM_NONE
@@ -126,129 +126,134 @@
     def _createOrInvite(self, room, other_players, profile):
         """
         This is called only when someone explicitly wants to play.
+
         The game will not be created if one already exists in the room,
         also its creation could be postponed until all the expected players
         join the room (in that case it will be created from userJoinedTrigger).
-        @param room: instance of wokkel.muc.Room
-        @param other_players: list for other players JID userhosts
+        @param room (wokkel.muc.Room): the room
+        @param other_players (list[jid.JID]): list of the other players JID (bare) 
         """
         user_jid = self.host.getJidNStream(profile)[0]
-        room_jid_s = room.occupantJID.userhost()
-        nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid_s, profile)
+        room_jid = room.occupantJID.userhostJID()
+        nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile)
         nicks = [nick]
-        if self._gameExists(room_jid_s):
-            if not self._checkJoinAuth(room_jid_s, user_jid.userhost(), nick):
+        if self._gameExists(room_jid):
+            if not self._checkJoinAuth(room_jid, user_jid, nick):
                 return
             nicks.extend(self._invitePlayers(room, other_players, nick, profile))
-            self._updatePlayers(room_jid_s, nicks, True, profile)
+            self._updatePlayers(room_jid, nicks, True, profile)
         else:
-            self._initGame(room_jid_s, nick)
+            self._initGame(room_jid, nick)
             (auth, waiting, missing) = self._checkWaitAuth(room, other_players)
             nicks.extend(waiting)
             nicks.extend(self._invitePlayers(room, missing, nick, profile))
             if auth:
-                self.createGame(room_jid_s, nicks, profile)
+                self.createGame(room_jid, nicks, profile)
             else:
-                self._updatePlayers(room_jid_s, nicks, False, profile)
+                self._updatePlayers(room_jid, nicks, False, profile)
+
+    def _initGame(self, room_jid, referee_nick):
+        """
 
-    def _initGame(self, room_jid_s, referee_nick):
-        """Important: do not add the referee to 'players' yet. For a
-        <players /> message to be emitted whenever a new player is joining,
-        it is necessary to not modify 'players' outside of _updatePlayers.
+        @param room_jid (jid.JID): JID of the room
+        @param referee_nick (unicode): nickname of the referee
         """
-        referee = room_jid_s + '/' + referee_nick
-        self.games[room_jid_s] = {'referee': referee, 'players': [], 'started': False, 'status': {}}
-        self.games[room_jid_s].update(copy.deepcopy(self.game_init))
-        self.invitations.setdefault(room_jid_s, [])
+        # Important: do not add the referee to 'players' yet. For a
+        # <players /> message to be emitted whenever a new player is joining,
+        # it is necessary to not modify 'players' outside of _updatePlayers.
+        referee_jid = jid.JID(room_jid.userhost() + '/' + referee_nick)
+        self.games[room_jid] = {'referee': referee_jid, 'players': [], 'started': False, 'status': {}}
+        self.games[room_jid].update(copy.deepcopy(self.game_init))
+        self.invitations.setdefault(room_jid, [])
 
-    def _gameExists(self, room_jid_s, started=False):
+    def _gameExists(self, room_jid, started=False):
         """Return True if a game has been initialized/started.
         @param started: if False, the game must be initialized to return True,
         otherwise it must be initialized and started with createGame.
         @return: True if a game is initialized/started in that room"""
-        return room_jid_s in self.games and (not started or self.games[room_jid_s]['started'])
+        return room_jid in self.games and (not started or self.games[room_jid]['started'])
 
-    def _checkJoinAuth(self, room_jid_s, user_jid_s=None, nick="", verbose=False):
+    def _checkJoinAuth(self, room_jid, user_jid=None, nick="", verbose=False):
         """Checks if this profile is allowed to join the game.
+
         The parameter nick is used to check if the user is already
         a player in that game. When this method is called from
         userJoinedTrigger, nick is also used to check the user
         identity instead of user_jid_s (see TODO comment below).
-        @param room_jid_s: the room hosting the game
-        @param user_jid_s: JID userhost of the user
-        @param nick: nick of the user
+        @param room_jid (jid.JID): the JID of the room hosting the game
+        @param user_jid (jid.JID): JID of the user
+        @param nick (unicode): nick of the user
         @return: True if this profile can join the game
         """
         auth = False
-        if not self._gameExists(room_jid_s):
+        if not self._gameExists(room_jid):
             auth = False
-        elif self.join_mode == self.ALL or self.isPlayer(room_jid_s, nick):
+        elif self.join_mode == self.ALL or self.isPlayer(room_jid, nick):
             auth = True
         elif self.join_mode == self.INVITED:
-            user_jid_s = JID(user_jid_s).userhost()
             # considering all the batches of invitations
-            for invitations in self.invitations[room_jid_s]:
-                if user_jid_s is not None:
-                    if user_jid_s in invitations[1]:
+            for invitations in self.invitations[room_jid]:
+                if user_jid is not None:
+                    if user_jid.userhostJID() in invitations[1]:
                         auth = True
                         break
                 else:
                     # TODO: that's not secure enough but what to do if
                     # wokkel.muc.User's 'entity' attribute is not set?!
-                    if nick in [JID(invited).user for invited in invitations[1]]:
+                    if nick in [invited.user for invited in invitations[1]]:
                         auth = True
                         break
 
         if not auth and (verbose or _DEBUG):
-            log.debug(_("%(user)s not allowed to join the game %(game)s in %(room)s") % {'user': user_jid_s or nick, 'game': self.name, 'room': room_jid_s})
+            log.debug(_("%(user)s not allowed to join the game %(game)s in %(room)s") % {'user': user_jid.userhost() or nick, 'game': self.name, 'room': room_jid.userhost()})
         return auth
 
-    def _updatePlayers(self, room_jid_s, nicks, sync, profile):
+    def _updatePlayers(self, room_jid, nicks, sync, profile):
         """Update the list of players and signal to the room that some players joined the game.
         If sync is True, the news players are synchronized with the game data they have missed.
-        Remark: self.games[room_jid_s]['players'] should not be modified outside this method.
-        @param room_jid_s: room userhost
-        @param nicks: list of players nicks in the room (referee included, in first position)
-        @param sync: set to True to send synchronization data to the new players
-        @param profile
+        Remark: self.games[room_jid]['players'] should not be modified outside this method.
+        @param room_jid (jid.JID): JID of the room
+        @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position)
+        @param sync (bool): set to True to send synchronization data to the new players
+        @param profile (unicode): %(doc_profile)s
         """
         if nicks == []:
             return
         # this is better than set(nicks).difference(...) as it keeps the order
-        new_nicks = [nick for nick in nicks if nick not in self.games[room_jid_s]['players']]
+        new_nicks = [nick for nick in nicks if nick not in self.games[room_jid]['players']]
         if len(new_nicks) == 0:
             return
 
         def setStatus(status):
             for nick in new_nicks:
-                self.games[room_jid_s]['status'][nick] = status
+                self.games[room_jid]['status'][nick] = status
 
-        sync = sync and self._gameExists(room_jid_s, True) and len(self.games[room_jid_s]['players']) > 0
+        sync = sync and self._gameExists(room_jid, True) and len(self.games[room_jid]['players']) > 0
         setStatus('desync' if sync else 'init')
-        self.games[room_jid_s]['players'].extend(new_nicks)
-        self._synchronizeRoom(room_jid_s, [JID(room_jid_s)], profile)
+        self.games[room_jid]['players'].extend(new_nicks)
+        self._synchronizeRoom(room_jid, [room_jid], profile)
         if sync:
             setStatus('init')
 
-    def _synchronizeRoom(self, room_jid_s, recipients, profile):
+    def _synchronizeRoom(self, room_jid, recipients, profile):
         """Communicate the list of players to the whole room or only to some users,
         also send the synchronization data to the players who recently joined the game.
-        @param room_jid_s: room userhost
-        @recipients: list of JIDs, the recipients of the message could be:
-        - room JID
-        - room JID + "/" + user nick
-        @param profile
+        @param room_jid (jid.JID): JID of the room
+        @recipients (list[jid.JID]): list of JIDs, the recipients of the message could be:
+            - room JID
+            - room JID + "/" + user nick
+        @param profile (unicode): %(doc_profile)s
         """
-        if self._gameExists(room_jid_s, started=True):
-            element = self._createStartElement(self.games[room_jid_s]['players'])
+        if self._gameExists(room_jid, started=True):
+            element = self._createStartElement(self.games[room_jid]['players'])
         else:
-            element = self._createStartElement(self.games[room_jid_s]['players'], name="players")
+            element = self._createStartElement(self.games[room_jid]['players'], name="players")
         elements = [(element, None, None)]
 
         sync_args = []
-        sync_data = self._getSyncData(room_jid_s)
+        sync_data = self._getSyncData(room_jid)
         for nick in sync_data:
-            user_jid = JID(room_jid_s + '/' + nick)
+            user_jid = jid.JID(room_jid.userhost() + '/' + nick)
             if user_jid in recipients:
                 user_elements = copy.deepcopy(elements)
                 for child in sync_data[nick]:
@@ -263,18 +268,18 @@
         for args, kwargs in sync_args:
             self._sendElements(*args, **kwargs)
 
-    def _getSyncData(self, room_jid_s, force_nicks=None):
+    def _getSyncData(self, room_jid, force_nicks=None):
         """The synchronization data are returned for each player who
         has the state 'desync' or if he's been contained by force_nicks.
-        @param room_jid_s: room userhost
+        @param room_jid (jid.JID): JID of the room
         @param force_nicks: force the synchronization for this list of the nicks
         @return: a mapping between player nicks and a list of elements to
         be sent by self._synchronizeRoom for the game to be synchronized.
         """
-        if not self._gameExists(room_jid_s):
+        if not self._gameExists(room_jid):
             return {}
         data = {}
-        status = self.games[room_jid_s]['status']
+        status = self.games[room_jid]['status']
         nicks = [nick for nick in status if status[nick] == 'desync']
         if force_nicks is None:
             force_nicks = []
@@ -282,14 +287,14 @@
             if nick not in nicks:
                 nicks.append(nick)
         for nick in nicks:
-            elements = self.getSyncDataForPlayer(room_jid_s, nick)
+            elements = self.getSyncDataForPlayer(room_jid, nick)
             if elements:
                 data[nick] = elements
         return data
 
-    def getSyncDataForPlayer(self, room_jid_s, nick):
+    def getSyncDataForPlayer(self, room_jid, nick):
         """This method may (and should probably) be overwritten by a child class.
-        @param room_jid_s: room userhost
+        @param room_jid (jid.JID): JID of the room
         @param nick: the nick of the player to be synchronized
         @return: a list of elements to synchronize this player with the game.
         """
@@ -297,20 +302,19 @@
 
     def _invitePlayers(self, room, other_players, nick, profile):
         """Invite players to a room, associated game may exist or not.
-        @param room: wokkel.muc.Room instance
-        @param other_players: list of JID userhosts to invite
-        @param nick: nick of the user who send the invitation
-        @return: list of room nicks for invited players who are already in the room
+
+        @param room (wokkel.muc.Room): the room
+        @param other_players (list[jid.JID]): list of the players to invite
+        @param nick (unicode): nick of the user who send the invitation
+        @return: list[unicode] of room nicks for invited players who are already in the room
         """
         room_jid = room.occupantJID.userhostJID()
-        room_jid_s = room.occupantJID.userhost()
-        if not self._checkInviteAuth(room_jid_s, nick):
+        if not self._checkInviteAuth(room_jid, nick):
             return []
         # TODO: remove invitation waiting for too long, using the time data
-        players_jids = [JID(player) for player in other_players]
-        self.invitations[room_jid_s].append((time(), [player.userhost() for player in players_jids]))
+        self.invitations[room_jid].append((time(), [player.userhostJID() for player in other_players]))
         nicks = []
-        for player_jid in [player.userhostJID() for player in players_jids]:
+        for player_jid in [player.userhostJID() for player in other_players]:
             # TODO: find a way to make it secure
             other_nick = self.host.plugins["XEP-0045"].getRoomNickOfUser(room, player_jid, secure=self.testing)
             if other_nick is None:
@@ -319,58 +323,59 @@
                 nicks.append(other_nick)
         return nicks
 
-    def _checkInviteAuth(self, room_jid_s, nick, verbose=False):
+    def _checkInviteAuth(self, room_jid, nick, verbose=False):
         """Checks if this user is allowed to invite players
-        @param room_jid_s: room userhost
+
+        @param room_jid (jid.JID): JID of the room
         @param nick: user nick in the room
         @param verbose: display debug message
         @return: True if the user is allowed to invite other players
         """
         auth = False
-        if self.invite_mode == self.FROM_ALL or not self._gameExists(room_jid_s):
+        if self.invite_mode == self.FROM_ALL or not self._gameExists(room_jid):
             auth = True
         elif self.invite_mode == self.FROM_NONE:
-            auth = not self._gameExists(room_jid_s, started=True) and self.isReferee(room_jid_s, nick)
+            auth = not self._gameExists(room_jid, started=True) and self.isReferee(room_jid, nick)
         elif self.invite_mode == self.FROM_REFEREE:
-            auth = self.isReferee(room_jid_s, nick)
+            auth = self.isReferee(room_jid, nick)
         elif self.invite_mode == self.FROM_PLAYERS:
-            auth = self.isPlayer(room_jid_s, nick)
+            auth = self.isPlayer(room_jid, nick)
         if not auth and (verbose or _DEBUG):
-            log.debug(_("%(user)s not allowed to invite for the game %(game)s in %(room)s") % {'user': nick, 'game': self.name, 'room': room_jid_s})
+            log.debug(_("%(user)s not allowed to invite for the game %(game)s in %(room)s") % {'user': nick, 'game': self.name, 'room': room_jid.userhost()})
         return auth
 
-    def isReferee(self, room_jid_s, nick):
+    def isReferee(self, room_jid, nick):
         """Checks if the player with this nick is the referee for the game in this room"
-        @param room_jid_s: room userhost
+        @param room_jid (jid.JID): room JID
         @param nick: user nick in the room
         @return: True if the user is the referee of the game in this room
         """
-        if not self._gameExists(room_jid_s):
+        if not self._gameExists(room_jid):
             return False
-        return room_jid_s + '/' + nick == self.games[room_jid_s]['referee']
+        return jid.JID(room_jid.userhost() + '/' + nick) == self.games[room_jid]['referee']
 
-    def isPlayer(self, room_jid_s, nick):
+    def isPlayer(self, room_jid, nick):
         """Checks if the user with this nick is a player for the game in this room.
-        @param room_jid_s: room userhost
+        @param room_jid (jid.JID): JID of the room
         @param nick: user nick in the room
         @return: True if the user is a player of the game in this room
         """
-        if not self._gameExists(room_jid_s):
+        if not self._gameExists(room_jid):
             return False
         # Important: the referee is not in the 'players' list right after
         # the game initialization, that's why we do also check with isReferee
-        return nick in self.games[room_jid_s]['players'] or self.isReferee(room_jid_s, nick)
+        return nick in self.games[room_jid]['players'] or self.isReferee(room_jid, nick)
 
     def _checkWaitAuth(self, room, other_players, verbose=False):
         """Check if we must wait for other players before starting the game.
 
-        @param room: wokkel.muc.Room instance
-        @param other_players: list of players JID userhosts without the referee
-        @param verbose: display debug message
+        @param room (wokkel.muc.Room): the room
+        @param other_players (list[jid.JID]): list of the players without the referee
+        @param verbose (bool): display debug message
         @return: (x, y, z) with:
-        x: False if we must wait, True otherwise
-        y: the nicks of the players that have been checked and confirmed
-        z: the players that have not been checked or that are missing
+            x: False if we must wait, True otherwise
+            y: the nicks of the players that have been checked and confirmed
+            z: the JID of the players that have not been checked or that are missing
         """
         if self.wait_mode == self.FOR_NONE or other_players == []:
             result = (True, [], other_players)
@@ -388,20 +393,25 @@
     def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE):
         """Generate unique room name
 
-        @param muc_service: you can leave empty to autofind the muc service
-        @param profile_key: %(doc_profile_key)s
-        @return: a unique name for a new room to be created
+        @param muc_service (jid.JID): you can leave empty to autofind the muc service
+        @param profile_key (unicode): %(doc_profile_key)s
+        @return: jid.JID (unique name for a new room to be created)
         """
         # FIXME: jid.JID must be used instead of strings
         room = self.host.plugins["XEP-0045"].getUniqueName(muc_service, profile_key=profile_key)
-        return "sat_%s_%s" % (self.name.lower(), room.full())
+        return jid.JID("sat_%s_%s" % (self.name.lower(), room.userhost()))
 
-    def prepareRoom(self, other_players=None, room_jid_s=None, profile_key=C.PROF_KEY_NONE):
+    def _prepareRoom(self, other_players=None, room_jid_s='', profile_key=C.PROF_KEY_NONE):
+        room_jid = jid.JID(room_jid_s) if room_jid_s else None
+        other_players = [jid.JID(player).userhostJID() for player in other_players]
+        return self.prepareRoom(other_players, room_jid, profile_key)
+
+    def prepareRoom(self, other_players=None, room_jid=None, profile_key=C.PROF_KEY_NONE):
         """Prepare the room for a game: create it if it doesn't exist and invite players.
 
-        @param other_players: list for other players JID userhosts
-        @param room_jid_s: JID userhost of the room, or None to generate a unique name
-        @param profile_key
+        @param other_players (list[JID]): list of other players JID (bare)
+        @param room_jid (jid.JID): JID of the room, or None to generate a unique name
+        @param profile_key (unicode): %(doc_profile_key)s
         """
         log.debug(_('Preparing room for %s game') % self.name)
         profile = self.host.memory.getProfileName(profile_key)
@@ -413,19 +423,18 @@
 
         def roomJoined(room):
             """@param room: instance of wokkel.muc.Room"""
-            self._createOrInvite(room, [JID(player).userhost() for player in other_players], profile)
+            self._createOrInvite(room, other_players, profile)
 
         # Create/join the given room, or a unique generated one if no room is specified.
-        if room_jid_s is not None and room_jid_s != "":  # a room name has been specified
-            if room_jid_s in self.host.plugins["XEP-0045"].clients[profile].joined_rooms:
-                roomJoined(self.host.plugins["XEP-0045"].clients[profile].joined_rooms[room_jid_s])
-                return defer.succeed(None)
+        if room_jid is None:
+            room_jid = self.getUniqueName(profile_key=profile_key)
         else:
-            room_jid_s = self.getUniqueName(profile_key=profile_key)
-            if room_jid_s == "":
+            if room_jid in self.host.plugins["XEP-0045"].clients[profile].joined_rooms:
+                roomJoined(self.host.plugins["XEP-0045"].clients[profile].joined_rooms[room_jid])
                 return defer.succeed(None)
+
         user_jid = self.host.getJidNStream(profile)[0]
-        d = self.host.plugins["XEP-0045"].join(JID(room_jid_s), user_jid.user, {}, profile)
+        d = self.host.plugins["XEP-0045"].join(room_jid, user_jid.user, {}, profile)
         return d.addCallback(roomJoined)
 
     def userJoinedTrigger(self, room, user, profile):
@@ -435,29 +444,29 @@
         @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID
         @return: True to not interrupt the main process.
         """
-        room_jid_s = room.occupantJID.userhost()
+        room_jid = room.occupantJID.userhostJID()
         profile_nick = room.occupantJID.resource
-        if not self.isReferee(room_jid_s, profile_nick):
+        if not self.isReferee(room_jid, profile_nick):
             return True  # profile is not the referee
-        if not self._checkJoinAuth(room_jid_s, user.entity.userhost() if user.entity else None, user.nick):
+        if not self._checkJoinAuth(room_jid, user.entity if user.entity else None, user.nick):
             # user not allowed but let him know that we are playing :p
-            self._synchronizeRoom(room_jid_s, [JID(room_jid_s + '/' + user.nick)], profile)
+            self._synchronizeRoom(room_jid, [jid.JID(room_jid.userhost() + '/' + user.nick)], profile)
             return True
         if self.wait_mode == self.FOR_ALL:
             # considering the last batch of invitations
-            batch = len(self.invitations[room_jid_s]) - 1
+            batch = len(self.invitations[room_jid]) - 1
             if batch < 0:
-                log.error("Invitations from %s to play %s in %s have been lost!" % (profile_nick, self.name, room_jid_s))
+                log.error("Invitations from %s to play %s in %s have been lost!" % (profile_nick, self.name, room_jid.userhost()))
                 return True
-            other_players = self.invitations[room_jid_s][batch][1]
+            other_players = self.invitations[room_jid][batch][1]
             (auth, nicks, dummy) = self._checkWaitAuth(room, other_players)
             if auth:
-                del self.invitations[room_jid_s][batch]
+                del self.invitations[room_jid][batch]
                 nicks.insert(0, profile_nick)  # add the referee
-                self.createGame(room_jid_s, nicks, profile_key=profile)
+                self.createGame(room_jid, nicks, profile_key=profile)
                 return True
         # let the room know that a new player joined
-        self._updatePlayers(room_jid_s, [user.nick], True, profile)
+        self._updatePlayers(room_jid, [user.nick], True, profile)
         return True
 
     def userLeftTrigger(self, room, user, profile):
@@ -467,89 +476,95 @@
         @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID
         @return: True to not interrupt the main process.
         """
-        room_jid_s = room.occupantJID.userhost()
+        room_jid = room.occupantJID.userhostJID()
         profile_nick = room.occupantJID.resource
-        if not self.isReferee(room_jid_s, profile_nick):
+        if not self.isReferee(room_jid, profile_nick):
             return True  # profile is not the referee
-        if self.isPlayer(room_jid_s, user.nick):
+        if self.isPlayer(room_jid, user.nick):
             try:
-                self.games[room_jid_s]['players'].remove(user.nick)
+                self.games[room_jid]['players'].remove(user.nick)
             except ValueError:
                 pass
-            if len(self.games[room_jid_s]['players']) == 0:
-                del self.games[room_jid_s]  # finish the game
+            if len(self.games[room_jid]['players']) == 0:
                 return True
             if self.wait_mode == self.FOR_ALL:
                 # allow this user to join the game again
-                user_jid_s = user.entity.userhost()
-                if len(self.invitations[room_jid_s]) == 0:
-                    self.invitations[room_jid_s].append((time(), [user_jid_s]))
+                user_jid = user.entity.userhostJID()
+                if len(self.invitations[room_jid]) == 0:
+                    self.invitations[room_jid].append((time(), [user_jid]))
                 else:
                     batch = 0  # add to the first batch of invitations
-                    if user_jid_s not in self.invitations[room_jid_s][batch][1]:
-                        self.invitations[room_jid_s][batch][1].append(user_jid_s)
+                    if user_jid not in self.invitations[room_jid][batch][1]:
+                        self.invitations[room_jid][batch][1].append(user_jid)
         return True
 
-    def _checkCreateGameAndInit(self, room_jid_s, profile):
-        """Check if that profile can create the game. If the game can be created but is not initialized yet, this method will also do the initialization.
+    def _checkCreateGameAndInit(self, room_jid, profile):
+        """Check if that profile can create the game. If the game can be created
+        but is not initialized yet, this method will also do the initialization.
 
-        @param room_jid_s: room userhost
+        @param room_jid (jid.JID): JID of the room
         @param profile
         @return: a couple (create, sync) with:
                 - create: set to True to allow the game creation
                 - sync: set to True to advice a game synchronization
         """
-        user_nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid_s, profile)
+        user_nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile)
         if not user_nick:
-            log.error('Internal error: profile %s has not joined the room %s' % (profile, room_jid_s))
+            log.error('Internal error: profile %s has not joined the room %s' % (profile, room_jid.userhost()))
             return False, False
-        if self._gameExists(room_jid_s):
-            is_referee = self.isReferee(room_jid_s, user_nick)
-            if self._gameExists(room_jid_s, started=True):
-                log.warning(_("%(game)s game already created in room %(room)s") % {'game': self.name, 'room': room_jid_s})
+        if self._gameExists(room_jid):
+            is_referee = self.isReferee(room_jid, user_nick)
+            if self._gameExists(room_jid, started=True):
+                log.warning(_("%(game)s game already created in room %(room)s") % {'game': self.name, 'room': room_jid.userhost()})
                 return False, is_referee
             elif not is_referee:
-                log.warning(_("%(game)s game in room %(room)s can only be created by %(user)s") % {'game': self.name, 'room': room_jid_s, 'user': user_nick})
+                log.warning(_("%(game)s game in room %(room)s can only be created by %(user)s") % {'game': self.name, 'room': room_jid.userhost(), 'user': user_nick})
                 return False, False
         else:
-            self._initGame(room_jid_s, user_nick)
+            self._initGame(room_jid, user_nick)
         return True, False
 
-    def createGame(self, room_jid_s, nicks=None, profile_key=C.PROF_KEY_NONE):
+    def _createGame(self, room_jid_s, nicks=None, profile_key=C.PROF_KEY_NONE):
+        self.createGame(jid.JID(room_jid_s), nicks, profile_key)
+
+    def createGame(self, room_jid, nicks=None, profile_key=C.PROF_KEY_NONE):
         """Create a new game.
 
         This can be called directly from a frontend and skips all the checks and invitation system,
         but the game must not exist and all the players must be in the room already.
-        @param room_jid: JID userhost of the room
-        @param nicks: list of players nicks in the room (referee included, in first position)
-        @param profile_key: %(doc_profile_key)s
+        @param room_jid (jid.JID): JID of the room
+        @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position)
+        @param profile_key (unicode): %(doc_profile_key)s
         """
-        log.debug(_("Creating %(game)s game in room %(room)s") % {'game': self.name, 'room': room_jid_s})
+        log.debug(_("Creating %(game)s game in room %(room)s") % {'game': self.name, 'room': room_jid})
         profile = self.host.memory.getProfileName(profile_key)
         if not profile:
             log.error(_("profile %s is unknown") % profile_key)
             return
-        (create, sync) = self._checkCreateGameAndInit(room_jid_s, profile)
+        (create, sync) = self._checkCreateGameAndInit(room_jid, profile)
         if nicks is None:
             nicks = []
         if not create:
             if sync:
-                self._updatePlayers(room_jid_s, nicks, True, profile)
+                self._updatePlayers(room_jid, nicks, True, profile)
             return
-        self.games[room_jid_s]['started'] = True
-        self._updatePlayers(room_jid_s, nicks, False, profile)
+        self.games[room_jid]['started'] = True
+        self._updatePlayers(room_jid, nicks, False, profile)
         if self.player_init:
             # specific data to each player (score, private data)
-            self.games[room_jid_s].setdefault('players_data', {})
+            self.games[room_jid].setdefault('players_data', {})
             for nick in nicks:
                 # The dict must be COPIED otherwise it is shared between all users
-                self.games[room_jid_s]['players_data'][nick] = copy.deepcopy(self.player_init)
+                self.games[room_jid]['players_data'][nick] = copy.deepcopy(self.player_init)
 
-    def playerReady(self, player, referee, profile_key=C.PROF_KEY_NONE):
+    def _playerReady(self, player_nick, referee_jid_s, profile_key=C.PROF_KEY_NONE):
+        self.playerReady(player_nick, jid.JID(referee_jid_s), profile_key)
+
+    def playerReady(self, player_nick, referee_jid, profile_key=C.PROF_KEY_NONE):
         """Must be called when player is ready to start a new game
 
         @param player: the player nick in the room
-        @param referee: referee userhost
+        @param referee_jid (jid.JID): JID of the referee
         """
         profile = self.host.memory.getProfileName(profile_key)
         if not profile:
@@ -557,7 +572,7 @@
             return
         log.debug('new player ready: %s' % profile)
         # TODO: we probably need to add the game and room names in the sent message
-        self.send(JID(referee), 'player_ready', {'player': player}, profile=profile)
+        self.send(referee_jid, 'player_ready', {'player': player_nick}, profile=profile)
 
     def newRound(self, room_jid, data, profile):
         """Launch a new round (reinit the user data)
@@ -569,7 +584,7 @@
         @param profile
         """
         log.debug(_('new round for %s game') % self.name)
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         players = game_data['players']
         players_data = game_data['players_data']
         game_data['stage'] = "init"
@@ -578,7 +593,7 @@
 
         if isinstance(msg_elts, dict):
             for player in players:
-                to_jid = JID(room_jid.userhost() + "/" + player)  # FIXME: gof:
+                to_jid = jid.JID(room_jid.userhost() + "/" + player)  # FIXME: gof:
                 elem = msg_elts[player] if isinstance(msg_elts[player], domish.Element) else None
                 self.send(to_jid, elem, profile=profile)
         elif isinstance(msg_elts, domish.Element):
--- a/src/plugins/plugin_misc_tarot.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_misc_tarot.py	Wed Mar 18 10:52:28 2015 +0100
@@ -59,12 +59,12 @@
         self._sessions = memory.Sessions()
         self.inheritFromRoomGame(host)
         RoomGame._init_(self, host, PLUGIN_INFO, (NS_CG, CG_TAG),
-                          game_init={'hand_size': 18, 'init_player': 0, 'current_player': None, 'contrat': None, 'stage': None},
-                          player_init={'score': 0})
+                        game_init={'hand_size': 18, 'init_player': 0, 'current_player': None, 'contrat': None, 'stage': None},
+                        player_init={'score': 0})
         self.contrats = [_('Passe'), _('Petite'), _('Garde'), _('Garde Sans'), _('Garde Contre')]
-        host.bridge.addMethod("tarotGameLaunch", ".plugin", in_sign='asss', out_sign='', method=self.prepareRoom, async=True)  # args: players, room_jid, profile
-        host.bridge.addMethod("tarotGameCreate", ".plugin", in_sign='sass', out_sign='', method=self.createGame)  # args: room_jid, players, profile
-        host.bridge.addMethod("tarotGameReady", ".plugin", in_sign='sss', out_sign='', method=self.playerReady)  # args: player, referee, profile
+        host.bridge.addMethod("tarotGameLaunch", ".plugin", in_sign='asss', out_sign='', method=self._prepareRoom, async=True)  # args: players, room_jid, profile
+        host.bridge.addMethod("tarotGameCreate", ".plugin", in_sign='sass', out_sign='', method=self._createGame)  # args: room_jid, players, profile
+        host.bridge.addMethod("tarotGameReady", ".plugin", in_sign='sss', out_sign='', method=self._playerReady)  # args: player, referee, profile
         host.bridge.addMethod("tarotGamePlayCards", ".plugin", in_sign='ssa(ss)s', out_sign='', method=self.play_cards)  # args: player, referee, cards, profile
         host.bridge.addSignal("tarotGamePlayers", ".plugin", signature='ssass')  # args: room_jid, referee, players, profile
         host.bridge.addSignal("tarotGameStarted", ".plugin", signature='ssass')  # args: room_jid, referee, players, profile
@@ -84,7 +84,6 @@
         self.__choose_contrat_id = host.registerCallback(self._contratChoosed, with_data=True)
         self.__score_id = host.registerCallback(self._scoreShowed, with_data=True)
 
-
     def __card_list_to_xml(self, cards_list, elt_name):
         """Convert a card list to domish element"""
         cards_list_elt = domish.Element((None, elt_name))
@@ -183,17 +182,17 @@
         @param game_data: data of the game
         @param played: cards currently on the table
         @param winner: nick of the trick winner"""
-        #TODO: manage the case where excuse is played on the last trick (and lost)
+        # TODO: manage the case where excuse is played on the last trick (and lost)
         players_data = game_data['players_data']
         excuse = TarotCard(("atout", "excuse"))
 
-        #we first check if the Excuse was already played
-        #and if somebody is waiting for a card
+        # we first check if the Excuse was already played
+        # and if somebody is waiting for a card
         for player in game_data['players']:
             if players_data[player]['wait_for_low']:
-                #the excuse owner has to give a card to somebody
+                # the excuse owner has to give a card to somebody
                 if winner == player:
-                    #the excuse owner win the trick, we check if we have something to give
+                    # the excuse owner win the trick, we check if we have something to give
                     for card in played:
                         if card.points == 0.5:
                             pl_waiting = players_data[player]['wait_for_low']
@@ -203,8 +202,8 @@
                             return
                 return
 
-        if not excuse in played:
-            #the Excuse is not on the table, nothing to do
+        if excuse not in played:
+            # the Excuse is not on the table, nothing to do
             return
 
         excuse_player = None  # Who has played the Excuse ?
@@ -216,15 +215,15 @@
         if excuse_player == winner:
             return  # the excuse player win the trick, nothing to do
 
-        #first we remove the excuse from played cards
+        # first we remove the excuse from played cards
         played.remove(excuse)
-        #then we give it back to the original owner
+        # then we give it back to the original owner
         owner_levees = players_data[excuse_player]['levees']
         owner_levees.append(excuse)
-        #finally we give a low card to the trick winner
+        # finally we give a low card to the trick winner
         low_card = None
-        #We look backward in cards won by the Excuse owner to
-        #find a low value card
+        # We look backward in cards won by the Excuse owner to
+        # find a low value card
         for card_idx in range(len(owner_levees) - 1, -1, -1):
             if owner_levees[card_idx].points == 0.5:
                 low_card = owner_levees[card_idx]
@@ -233,7 +232,7 @@
                 log.debug(_('Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for Excuse compensation') % {"excuse_owner": excuse_player, "card_waited": low_card, "player_waiting": winner})
                 break
         if not low_card:  # The player has no low card yet
-            #TODO: manage case when player never win a trick with low card
+            # TODO: manage case when player never win a trick with low card
             players_data[excuse_player]['wait_for_low'] = winner
             log.debug(_("%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is waiting for one") % {'excuse_owner': excuse_player, 'winner': winner})
 
@@ -265,7 +264,7 @@
                 bouts.append(card.value)
             score += card.points
 
-        #We we do a basic check on score calculation
+        # We do a basic check on score calculation
         check_score = 0
         defenseurs = game_data['players'][:]
         defenseurs.remove(game_data['attaquant'])
@@ -305,8 +304,8 @@
         loosers = []
         player_score = {}
         for player in game_data['players']:
-            #TODO: adjust this for 3 and 5 players variants
-            #TODO: manage bonuses (petit au bout, poignée, chelem)
+            # TODO: adjust this for 3 and 5 players variants
+            # TODO: manage bonuses (petit au bout, poignée, chelem)
             player_score[player] = points_defenseur if player != game_data['attaquant'] else points_defenseur * -3
             players_data[player]['score'] += player_score[player]  # we add score of this game to the global score
             if player_score[player] > 0:
@@ -314,7 +313,7 @@
             else:
                 loosers.append(player)
 
-        scores_str = _('The attacker (%(attaquant)s) makes %(points)i and needs to make %(point_limit)i (%(nb_bouts)s oulder%(plural)s%(separator)s%(bouts)s): he %(victory)s') % {'attaquant': game_data['attaquant'], 'points': score, 'point_limit': point_limit, 'nb_bouts': nb_bouts, 'plural': 's' if nb_bouts > 1 else '', 'separator': ': ' if nb_bouts != 0 else '', 'bouts': ','.join(map(str, bouts)), 'victory': 'win' if victory else 'loose'}
+        scores_str = _('The attacker (%(attaquant)s) makes %(points)i and needs to make %(point_limit)i (%(nb_bouts)s oulder%(plural)s%(separator)s%(bouts)s): (s)he %(victory)s') % {'attaquant': game_data['attaquant'], 'points': score, 'point_limit': point_limit, 'nb_bouts': nb_bouts, 'plural': 's' if nb_bouts > 1 else '', 'separator': ': ' if nb_bouts != 0 else '', 'bouts': ','.join(map(str, bouts)), 'victory': 'wins' if victory else 'looses'}
         scores_str += '\n'
         for player in game_data['players']:
             scores_str += _("\n--\n%(player)s:\nscore for this game ==> %(score_game)i\ntotal score ==> %(total_score)i") % {'player': player, 'score_game': player_score[player], 'total_score': players_data[player]['score']}
@@ -332,7 +331,7 @@
             for card in cards:
                 if card.bout or card.value == "roi":
                     forbidden_cards.append(card)
-                #TODO: manage case where atouts (trumps) are in the dog
+                # TODO: manage case where atouts (trumps) are in the dog
         elif game_data['stage'] == 'play':
             biggest_atout = None
             suit_asked = None
@@ -342,7 +341,7 @@
             current_idx = game_data['current_player']
             current_player = players[current_idx]
             if idx == current_idx:
-                #the player is the first to play, he can play what he wants
+                # the player is the first to play, he can play what he wants
                 return forbidden_cards
             while (idx != current_idx):
                 player = players[idx]
@@ -398,13 +397,13 @@
             # TODO: send error dialog
             return defer.succeed({})
 
-        room_jid_s = session_data['room_jid'].userhost()
-        referee = self.games[room_jid_s]['referee']
-        player = self.host.plugins["XEP-0045"].getRoomNick(room_jid_s, profile)
+        room_jid = session_data['room_jid']
+        referee_jid = self.games[room_jid]['referee']
+        player = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile)
         data = xml_tools.XMLUIResult2DataFormResult(raw_data)
         contrat = data['contrat']
         log.debug(_('contrat [%(contrat)s] choosed by %(profile)s') % {'contrat': contrat, 'profile': profile})
-        d = self.send(jid.JID(referee), ('', 'contrat_choosed'), {'player': player}, content=contrat, profile=profile)
+        d = self.send(referee_jid, ('', 'contrat_choosed'), {'player': player}, content=contrat, profile=profile)
         d.addCallback(lambda ignore: {})
         del self._sessions[raw_data["session_id"]]
         return d
@@ -443,7 +442,7 @@
         self.send(jid.JID(referee), elem, {'player': player}, profile=profile)
 
     def newRound(self, room_jid, profile):
-        game_data = self.games[room_jid.userhost()]
+        game_data = self.games[room_jid]
         players = game_data['players']
         game_data['first_player'] = None  # first player for the current trick
         game_data['contrat'] = None
@@ -480,11 +479,11 @@
         """
         from_jid = jid.JID(mess_elt['from'])
         room_jid = jid.JID(from_jid.userhost())
-        nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid.userhost(), profile)
+        nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile)
 
         game_elt = mess_elt.firstChildElement()
-        game_data = self.games[room_jid.userhost()]
-        is_player = self.isPlayer(room_jid.userhost(), nick)
+        game_data = self.games[room_jid]
+        is_player = self.isPlayer(room_jid, nick)
         if 'players_data' in game_data:
             players_data = game_data['players_data']
 
@@ -501,8 +500,8 @@
 
             elif elt.name == 'player_ready':  # ready to play
                 player = elt['player']
-                status = self.games[room_jid.userhost()]['status']
-                nb_players = len(self.games[room_jid.userhost()]['players'])
+                status = self.games[room_jid]['status']
+                nb_players = len(self.games[room_jid]['players'])
                 status[player] = 'ready'
                 log.debug(_('Player %(player)s is ready to start [status: %(status)s]') % {'player': player, 'status': status})
                 if status.values().count('ready') == nb_players:  # everybody is ready, we can start the game
@@ -519,13 +518,13 @@
                 self.host.bridge.tarotGameChooseContrat(room_jid.userhost(), xml_data, profile)
 
             elif elt.name == 'contrat_choosed':
-                #TODO: check we receive the contrat from the right person
-                #TODO: use proper XEP-0004 way for answering form
+                # TODO: check we receive the contrat from the right person
+                # TODO: use proper XEP-0004 way for answering form
                 player = elt['player']
                 players_data[player]['contrat'] = unicode(elt)
                 contrats = [players_data[player]['contrat'] for player in game_data['players']]
                 if contrats.count(None):
-                    #not everybody has choosed his contrat, it's next one turn
+                    # not everybody has choosed his contrat, it's next one turn
                     player = self.__next_player(game_data)
                     to_jid = jid.JID(room_jid.userhost() + "/" + player)  # FIXME: gof:
                     self.send(to_jid, self.__ask_contrat(), profile=profile)
@@ -553,16 +552,16 @@
                         self.__start_play(room_jid, game_data, profile)
                         game_data['attaquant'] = best_contrat[0]
                     else:
-                        #Time to show the chien to everybody
+                        # Time to show the chien to everybody
                         to_jid = jid.JID(room_jid.userhost())  # FIXME: gof:
                         elem = self.__card_list_to_xml(game_data['chien'], 'chien')
                         self.send(to_jid, elem, {'attaquant': best_contrat[0]}, profile=profile)
-                        #the attacker (attaquant) get the chien
+                        # the attacker (attaquant) get the chien
                         game_data['hand'][best_contrat[0]].extend(game_data['chien'])
                         del game_data['chien'][:]
 
                     if game_data['contrat'] == "Garde Sans":
-                        #The chien go into attaquant's (attacker) levees
+                        # The chien go into attaquant's (attacker) levees
                         players_data[best_contrat[0]]['levees'].extend(game_data['chien'])
                         del game_data['chien'][:]
 
@@ -575,17 +574,17 @@
 
             elif elt.name == 'cards_played':
                 if game_data['stage'] == "ecart":
-                    #TODO: show atouts (trumps) if player put some in écart
+                    # TODO: show atouts (trumps) if player put some in écart
                     assert (game_data['attaquant'] == elt['player'])  # TODO: throw an xml error here
                     list_cards = TarotCard.from_tuples(self.__xml_to_list(elt))
-                    #we now check validity of card
+                    # we now check validity of card
                     invalid_cards = self.__invalid_cards(game_data, list_cards)
                     if invalid_cards:
                         elem = self.__invalid_cards_elt(list_cards, invalid_cards, game_data['stage'])
                         self.send(jid.JID(room_jid.userhost() + '/' + elt['player']), elem, profile=profile)
                         return
 
-                    #FIXME: gof: manage Garde Sans & Garde Contre cases
+                    # FIXME: gof: manage Garde Sans & Garde Contre cases
                     players_data[elt['player']]['levees'].extend(list_cards)  # we add the chien to attaquant's levées
                     for card in list_cards:
                         game_data['hand'][elt['player']].remove(card)
@@ -599,46 +598,46 @@
                     if mess_elt['type'] == 'groupchat':
                         self.host.bridge.tarotGameCardsPlayed(room_jid.userhost(), elt['player'], self.__xml_to_list(elt), profile)
                     else:
-                        #we first check validity of card
+                        # we first check validity of card
                         invalid_cards = self.__invalid_cards(game_data, cards)
                         if invalid_cards:
                             elem = self.__invalid_cards_elt(cards, invalid_cards, game_data['stage'])
                             self.send(jid.JID(room_jid.userhost() + '/' + current_player), elem, profile=profile)
                             return
-                        #the card played is ok, we forward it to everybody
-                        #first we remove it from the hand and put in on the table
+                        # the card played is ok, we forward it to everybody
+                        # first we remove it from the hand and put in on the table
                         game_data['hand'][current_player].remove(cards[0])
                         players_data[current_player]['played'] = cards[0]
 
-                        #then we forward the message
+                        # then we forward the message
                         self.send(room_jid, elt, profile=profile)
 
-                        #Did everybody played ?
+                        # Did everybody played ?
                         played = [players_data[player]['played'] for player in game_data['players']]
                         if all(played):
-                            #everybody has played
+                            # everybody has played
                             winner = self.__winner(game_data)
                             log.debug(_('The winner of this trick is %s') % winner)
-                            #the winner win the trick
+                            # the winner win the trick
                             self.__excuse_hack(game_data, played, winner)
                             players_data[elt['player']]['levees'].extend(played)
-                            #nothing left on the table
+                            # nothing left on the table
                             for player in game_data['players']:
                                 players_data[player]['played'] = None
                             if len(game_data['hand'][current_player]) == 0:
-                                #no card lef: the game is finished
+                                # no card left: the game is finished
                                 elem = self.__give_scores(*self.__calculate_scores(game_data))
                                 self.send(room_jid, elem, profile=profile)
                                 game_data['init_player'] = (game_data['init_player'] + 1) % len(game_data['players'])  # we change the dealer
                                 for player in game_data['players']:
                                     game_data['status'][player] = "init"
                                 return
-                            #next player is the winner
+                            # next player is the winner
                             next_player = game_data['first_player'] = self.__next_player(game_data, winner)
                         else:
                             next_player = self.__next_player(game_data)
 
-                        #finally, we tell to the next player to play
+                        # finally, we tell to the next player to play
                         to_jid = jid.JID(room_jid.userhost() + "/" + next_player)
                         self.send(to_jid, 'your_turn', profile=profile)
 
@@ -668,5 +667,5 @@
             else:
                 log.error(_('Unmanaged card game element: %s') % elt.name)
 
-    def getSyncDataForPlayer(self, room_jid_s, nick):
+    def getSyncDataForPlayer(self, room_jid, nick):
         return []
--- a/src/plugins/plugin_misc_text_commands.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_misc_text_commands.py	Wed Mar 18 10:52:28 2015 +0100
@@ -209,7 +209,7 @@
                 return False
 
         if not target_jid.resource:
-            target_jid.resource = self.host.memory.getLastResource(target_jid, profile)
+            target_jid.resource = self.host.memory.getMainResource(target_jid, profile)
 
         whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}]
 
--- a/src/plugins/plugin_sec_otr.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_sec_otr.py	Wed Mar 18 10:52:28 2015 +0100
@@ -29,8 +29,9 @@
 from twisted.words.protocols.jabber import jid
 from twisted.python import failure
 from twisted.internet import defer
+from sat.memory import persistent
 import potr
-from sat.memory import persistent
+import copy
 
 NS_OTR = "otr_plugin"
 PRIVATE_KEY = "PRIVATE KEY"
@@ -269,7 +270,7 @@
         try:
             to_jid = jid.JID(menu_data['jid'])
             if not to_jid.resource:
-                to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
+                to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
         except KeyError:
             log.error(_("jid key is not present !"))
             return defer.fail(exceptions.DataError)
@@ -287,7 +288,7 @@
         try:
             to_jid = jid.JID(menu_data['jid'])
             if not to_jid.resource:
-                to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
+                to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
         except KeyError:
             log.error(_("jid key is not present !"))
             return defer.fail(exceptions.DataError)
@@ -304,7 +305,7 @@
         try:
             to_jid = jid.JID(menu_data['jid'])
             if not to_jid.resource:
-                to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
+                to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
         except KeyError:
             log.error(_("jid key is not present !"))
             return defer.fail(exceptions.DataError)
@@ -374,7 +375,7 @@
         try:
             to_jid = jid.JID(menu_data['jid'])
             if not to_jid.resource:
-                to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
+                to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
         except KeyError:
             log.error(_("jid key is not present !"))
             return defer.fail(exceptions.DataError)
@@ -447,9 +448,9 @@
     def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
         if profile in self.skipped_profiles:
             return True
-        to_jid = mess_data['to']
+        to_jid = copy.copy(mess_data['to'])
         if mess_data['type'] != 'groupchat' and not to_jid.resource:
-            to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
+            to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
         otrctx = self.context_managers[profile].getContextForUser(to_jid)
         if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT:
             if otrctx.state == potr.context.STATE_ENCRYPTED:
@@ -475,7 +476,7 @@
         if show != "unavailable":
             return True
         if not entity.resource:
-            entity.resource = self.host.memory.getLastResource(entity, profile)  # FIXME: temporary and unsecure, must be changed when frontends are refactored
+            entity.resource = self.host.memory.getMainResource(entity, profile)  # FIXME: temporary and unsecure, must be changed when frontends are refactored
         otrctx = self.context_managers[profile].getContextForUser(entity)
         otrctx.disconnect()
         return True
--- a/src/plugins/plugin_xep_0045.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_xep_0045.py	Wed Mar 18 10:52:28 2015 +0100
@@ -65,7 +65,7 @@
         self.host = host
         self.clients = {}
         self._sessions = memory.Sessions()
-        host.bridge.addMethod("joinMUC", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join)
+        host.bridge.addMethod("joinMUC", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True)
         host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self.mucNick)
         host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True)
         host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sass)', method=self.getRoomsJoined)
@@ -117,7 +117,7 @@
         profile = self.host.memory.getProfileName(profile_key)
         if not self.checkClient(profile):
             raise exceptions.ProfileUnknownError("Unknown or disconnected profile")
-        if room_jid.userhost() not in self.clients[profile].joined_rooms:
+        if room_jid not in self.clients[profile].joined_rooms:
             raise UnknownRoom("This room has not been joined")
         return profile
 
@@ -127,12 +127,11 @@
         def _sendBridgeSignal(ignore=None):
             self.host.bridge.roomJoined(room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick, profile)
 
-        room_jid_s = room.roomJID.userhost()
-        self.clients[profile].joined_rooms[room_jid_s] = room
+        self.clients[profile].joined_rooms[room.roomJID] = room
         if room.locked:
-            #FIXME: the current behaviour is to create an instant room
-            #and send the signal only when the room is unlocked
-            #a proper configuration management should be done
+            # FIXME: the current behaviour is to create an instant room
+            # and send the signal only when the room is unlocked
+            # a proper configuration management should be done
             print "room locked !"
             self.clients[profile].configure(room.roomJID, {}).addCallbacks(_sendBridgeSignal, lambda x: log.error(_('Error while configuring the room')))
         else:
@@ -164,27 +163,25 @@
             result.append((room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick))
         return result
 
-    def getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
+    def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE):
         """return nick used in room by user
 
-        @param room_jid_s: unicode room id
+        @param room_jid (jid.JID): JID of the room
         @profile_key: profile
         @return: nick or empty string in case of error"""
         profile = self.host.memory.getProfileName(profile_key)
-        if not self.checkClient(profile) or room_jid_s not in self.clients[profile].joined_rooms:
+        if not self.checkClient(profile) or room_jid not in self.clients[profile].joined_rooms:
             return ''
-        return self.clients[profile].joined_rooms[room_jid_s].nick
+        return self.clients[profile].joined_rooms[room_jid].nick
 
     def getRoomNickOfUser(self, room, user_jid, secure=True):
         """Returns the nick of the given user in the room.
 
-        @room: instance of wokkel.muc.Room
-        @user: JID or unicode (JID userhost).
-        @param secure: set to True for a secure check
-        @return: the nick or None if the user didn't join the room.
+        @param room (wokkel.muc.Room): the room
+        @param user (jid.JID): bare JID of the user
+        @param secure (bool): set to True for a secure check
+        @return: unicode or None if the user didn't join the room.
         """
-        if not isinstance(user_jid, jid.JID):
-            user_jid = jid.JID(user_jid).userhostJID()
         for user in room.roster.values():
             if user.entity is not None:
                 if user.entity.userhostJID() == user_jid.userhostJID():
@@ -199,11 +196,12 @@
     def getRoomNicksOfUsers(self, room, users=[], secure=True):
         """Returns the nicks of the given users in the room.
 
-        @room: instance of wokkel.muc.Room
-        @users: list of JID or unicode (JID userhost).
-        @param secure: set to True for a secure check
-        @return: (x, y) with x a list containing the nicks of
-        the users who are in the room, and y the missing users.
+        @param room (wokkel.muc.Room): the room
+        @param users (list[jid.JID]): list of users
+        @param secure (True): set to True for a secure check
+        @return: a couple (x, y) with:
+            - x (list[unicode]): nicks of the users who are in the room
+            - y (list[jid.JID]): JID of the missing users.
         """
         nicks = []
         missing = []
@@ -231,6 +229,7 @@
         except KeyError:
             log.error(_("room_jid key is not present !"))
             return defer.fail(exceptions.DataError)
+
         def xmluiReceived(xmlui):
             return {"xmlui": xmlui.toXml()}
         return self.configureRoom(room_jid, profile).addCallback(xmluiReceived)
@@ -277,7 +276,7 @@
     def isNickInRoom(self, room_jid, nick, profile):
         """Tell if a nick is currently present in a room"""
         profile = self.getProfileAssertInRoom(room_jid, profile)
-        return self.clients[profile].joined_rooms[room_jid.userhost()].inRoster(muc.User(nick))
+        return self.clients[profile].joined_rooms[room_jid].inRoster(muc.User(nick))
 
     def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE):
         """Return received subjects of rooms"""
@@ -296,7 +295,7 @@
         muc_service = None
         services = yield self.host.findServiceEntities("conference", "text", jid_, profile_key=profile_key)
         for service in services:
-            if not ".irc." in service.userhost():
+            if ".irc." not in service.userhost():
                 # FIXME:
                 # This ugly hack is here to avoid an issue with openfire: the IRC gateway
                 # use "conference/text" identity (instead of "conference/irc")
@@ -310,10 +309,10 @@
     def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE):
         """Return unique name for a room, avoiding collision
 
-        @param muc_service: leave empty string to use the default service
-        @return: unique room userhost, or '' if an error occured.
+        @param muc_service (jid.JID) : leave empty string to use the default service
+        @return: jid.JID (unique room bare JID)
         """
-        #TODO: we should use #RFC-0045 10.1.4 when available here
+        # TODO: we should use #RFC-0045 10.1.4 when available here
         client = self.host.getClient(profile_key)
         room_name = uuid.uuid1()
         if muc_service is None:
@@ -337,7 +336,7 @@
         profile = self.host.memory.getProfileName(profile_key)
         if not self.checkClient(profile):
             return _errDeferred()
-        if room_jid.userhost() in self.clients[profile].joined_rooms:
+        if room_jid in self.clients[profile].joined_rooms:
             log.warning(_('%(profile)s is already in room %(room_jid)s') % {'profile': profile, 'room_jid': room_jid.userhost()})
             return _errDeferred()
         log.info(_("[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': profile, 'room': room_jid.userhost(), 'nick': nick})
@@ -358,23 +357,24 @@
 
     def _join(self, room_jid_s, nick, options={}, profile_key=C.PROF_KEY_NONE):
         """join method used by bridge: use the join method, but doesn't return any deferred
-        @return the room userhost (given value or unique generated name)
+        @return: unicode (the room bare)
         """
         profile = self.host.memory.getProfileName(profile_key)
         if not self.checkClient(profile):
             return
-        if room_jid_s == "":
-            room_jid_s = self.getUniqueName(profile_key=profile_key)
-        try:
-            room_jid = jid.JID(room_jid_s)
-        except:
-            mess = _("Invalid room jid: %s") % room_jid_s
-            log.warning(mess)
-            self.host.bridge.newAlert(mess, _("Group chat error"), "ERROR", profile)
-            return
-        self.join(room_jid, nick, options, profile)
+        if room_jid_s:
+            try:
+                room_jid = jid.JID(room_jid_s)
+            except (RuntimeError, jid.InvalidFormat, AttributeError):
+                mess = _("Invalid room JID: %s") % room_jid_s
+                log.warning(mess)
+                self.host.bridge.newAlert(mess, _("Group chat error"), "ERROR", profile)
+                return defer.succeed(None)
+        else:
+            room_jid = self.getUniqueName(profile_key=profile_key)
         # TODO: error management + signal in bridge
-        return room_jid_s
+        d = self.join(room_jid, nick, options, profile)
+        return d.addCallback(lambda room: room.roomJID.userhost())
 
     def nick(self, room_jid, nick, profile_key):
         profile = self.getProfileAssertInRoom(room_jid, profile_key)
@@ -471,10 +471,10 @@
             return False
 
         if mess_data["unparsed"].strip():
-            room = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
-            nick = (self.getRoomNick(mess_data["to"].userhost(), profile) or
+            room_jid = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
+            nick = (self.getRoomNick(room_jid, profile) or
                     self.host.getClient(profile).jid.user)
-            self.join(room, nick, {}, profile)
+            self.join(room_jid, nick, {}, profile)
 
         return False
 
@@ -630,10 +630,10 @@
         """ Add MUC user information to whois """
         if mess_data['type'] != "groupchat":
             return
-        if target_jid.userhost() not in self.clients[profile].joined_rooms:
+        if target_jid.userhostJID() not in self.clients[profile].joined_rooms:
             log.warning(_("This room has not been joined"))
             return
-        user = self.clients[profile].joined_rooms[target_jid.userhost()].getUser(target_jid.resource)
+        user = self.clients[profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource)
         whois_msg.append(_("Nickname: %s") % user.nick)
         if user.entity:
             whois_msg.append(_("Entity: %s") % user.entity)
@@ -649,32 +649,35 @@
     def presenceTrigger(self, presence_elt, client):
         # XXX: shouldn't it be done by the server ?!!
         muc_client = self.clients[client.profile]
-        for room_jid_s, room in muc_client.joined_rooms.items():
+        for room_jid, room in muc_client.joined_rooms.iteritems():
             elt = copy.deepcopy(presence_elt)
-            elt['to'] = room_jid_s + '/' + room.nick
+            elt['to'] = room_jid.userhost() + '/' + room.nick
             client.presence.send(elt)
         return True
 
 
 class SatMUCClient (muc.MUCClient):
-    #implements(iwokkel.IDisco)
+    # implements(iwokkel.IDisco)
 
     def __init__(self, plugin_parent):
         self.plugin_parent = plugin_parent
         self.host = plugin_parent.host
         muc.MUCClient.__init__(self)
-        self.joined_rooms = {}  # FIXME: seem to do the same thing as MUCClient's _rooms attribute, must be removed
         self.rec_subjects = {}
         self.__changing_nicks = set()  # used to keep trace of who is changing nick,
                                        # and to discard userJoinedRoom signal in this case
         print "init SatMUCClient OK"
 
+    @property
+    def joined_rooms(self):
+        return self._rooms
+
     def subject(self, room, subject):
         return muc.MUCClientProtocol.subject(self, room, subject)
 
     def unavailableReceived(self, presence):
-        #XXX: we override this method to manage nickname change
-        #TODO: feed this back to Wokkel
+        # XXX: we override this method to manage nickname change
+        # TODO: feed this back to Wokkel
         """
         Unavailable presence was received.
 
@@ -696,7 +699,7 @@
             self.userLeftRoom(room, user)
 
     def userJoinedRoom(self, room, user):
-        self.host.memory.updateEntityData(room.roomJID, "type", "chatroom", self.parent.profile)
+        self.host.memory.updateEntityData(room.roomJID, "type", "chatroom", profile_key=self.parent.profile)
         if user.nick in self.__changing_nicks:
             self.__changing_nicks.remove(user.nick)
         else:
@@ -713,9 +716,8 @@
             # we left the room
             room_jid_s = room.roomJID.userhost()
             log.info(_("Room [%(room)s] left (%(profile)s))") % {"room": room_jid_s,
-                                                             "profile": self.parent.profile})
+                                                                 "profile": self.parent.profile})
             self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile)
-            del self.plugin_parent.clients[self.parent.profile].joined_rooms[room_jid_s]
             self.host.bridge.roomLeft(room.roomJID.userhost(), self.parent.profile)
         else:
             log.debug(_("user %(nick)s left room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()})
@@ -733,7 +735,7 @@
 
     def receivedHistory(self, room, user, message):
         # http://xmpp.org/extensions/xep-0045.html#enter-history
-        #log.debug('receivedHistory: room=%s user=%s body=%s' % (room.roomJID.full(), user, message))
+        # log.debug('receivedHistory: room=%s user=%s body=%s' % (room.roomJID.full(), user, message))
         pass
 
     def receivedSubject(self, room, user, subject):
--- a/src/plugins/plugin_xep_0054.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_xep_0054.py	Wed Mar 18 10:52:28 2015 +0100
@@ -36,7 +36,7 @@
 from base64 import b64decode, b64encode
 from hashlib import sha1
 from sat.core import exceptions
-from sat.memory.persistent import PersistentDict
+from sat.memory import persistent
 from PIL import Image
 from cStringIO import StringIO
 
@@ -56,6 +56,8 @@
 NS_VCARD_UPDATE = 'vcard-temp:x:update'
 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]'
 
+CACHED_DATA = {'avatar', 'nick'}
+
 PLUGIN_INFO = {
     "name": "XEP 0054 Plugin",
     "import_name": "XEP-0054",
@@ -79,67 +81,101 @@
         self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH)
         if not os.path.exists(self.avatar_path):
             os.makedirs(self.avatar_path)
-        self.avatars_cache = PersistentDict(NS_VCARD)
-        self.avatars_cache.load()  # FIXME: resulting deferred must be correctly managed
-        host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self.getCard)
+        self.cache = {}
+        host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self._getCard)
         host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile)
         host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True)
         host.trigger.add("presence_available", self.presenceTrigger)
+        host.memory.setSignalOnUpdate("avatar")
+        host.memory.setSignalOnUpdate("nick")
 
     def getHandler(self, profile):
         return XEP_0054_handler(self)
 
     def presenceTrigger(self, presence_elt, client):
-        if client.jid.userhost() in self.avatars_cache:
+        if client.jid.userhost() in self.cache[client.profile]:
             x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
-            x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()])
+            x_elt.addElement('photo', content=self.cache[client.profile][client.jid.userhost()]['avatar'])
             presence_elt.addChild(x_elt)
 
         return True
 
-    def _fillCachedValues(self, result, client):
+    def _fillCachedValues(self, profile):
         #FIXME: this is really suboptimal, need to be reworked
         #       the current naive approach keeps a map between all jids of all profiles
-        #       in persistent cache, and check if cached jid are in roster, then put avatar
-        #       hashs in memory.
-        for _jid in client.roster.getBareJids() + [client.jid.userhost()]:
-            if _jid in self.avatars_cache:
-                self.host.memory.updateEntityData(jid.JID(_jid), "avatar", self.avatars_cache[_jid], client.profile)
+        #       in persistent cache, then put avatar
+        #       hashs in memory. Hashed should be shared between profiles
+        for jid_s, data in self.cache[profile].iteritems():
+            jid_ = jid.JID(jid_s)
+            for name in CACHED_DATA:
+                try:
+                    self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile)
+                except KeyError:
+                    pass
 
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
-        client.roster.got_roster.addCallback(self._fillCachedValues, client)
+    @defer.inlineCallbacks
+    def profileConnecting(self, profile):
+        self.cache[profile] = persistent.PersistentBinaryDict(NS_VCARD, profile)
+        yield self.cache[profile].load()
+        self._fillCachedValues(profile)
 
-    def update_cache(self, jid, name, value, profile):
+    def profileDisconnected(self, profile):
+        log.debug(u"Deleting profile cache for avatars")
+        del self.cache[profile]
+
+    def updateCache(self, jid_, name, value, profile):
         """update cache value
-        - save value in memory in case of change
-        @param jid: jid of the owner of the vcard
-        @param name: name of the item which changed
-        @param value: new value of the item
-        @param profile: profile which received the update
+
+        save value in memory in case of change
+        @param jid_(jid.JID): jid of the owner of the vcard
+        @param name(str): name of the item which changed
+        @param value(unicode): new value of the item
+        @param profile(unicode): profile which received the update
         """
-        try:
-            cached = self.host.memory.getEntityData(jid, [name], profile)
-        except exceptions.UnknownEntityError:
-            cached = {}
-        if not name in cached or cached[name] != value:
-            self.host.memory.updateEntityData(jid, name, value, profile)
-            if name == "avatar":
-                self.avatars_cache[jid.userhost()] = value
+        assert not jid_.resource # VCard are retrieved with bare jid
+        self.host.memory.updateEntityData(jid_, name, value, profile_key=profile)
+        if name in CACHED_DATA:
+            jid_s = jid_.userhost()
+            self.cache[profile].setdefault(jid_s, {})[name] = value
+            self.cache[profile].force(jid_s)
 
-    def get_cache(self, entity_jid, name, profile):
+    def getCache(self, entity_jid, name, profile):
         """return cached value for jid
+
         @param entity_jid: target contact
         @param name: name of the value ('nick' or 'avatar')
         @param profile: %(doc_profile)s
         @return: wanted value or None"""
+        assert not entity_jid.resource # VCard are retrieved with bare jid
         try:
             data = self.host.memory.getEntityData(entity_jid, [name], profile)
         except exceptions.UnknownEntityError:
             return None
         return data.get(name)
 
-    def save_photo(self, photo_xml):
+    def _getFilename(self, hash_):
+        """Get filename from hash
+
+        @param hash_: hash of the avatar
+        @return (str): absolute filename of the avatar
+        """
+        return os.path.join(self.avatar_path, hash_)
+
+    def saveAvatarFile(self, data, hash_):
+        """Save the avatar picture if it doesn't already exists
+
+        @param data(str): binary image of the avatar
+        @param hash_(str): hash of the binary data (will be used for the filename)
+        """
+        filename = self._getFilename(hash_)
+        if not os.path.exists(filename):
+            with open(filename, 'wb') as file_:
+                file_.write(data)
+            log.debug(_("file saved to %s") % hash_)
+        else:
+            log.debug(_("file [%s] already in cache") % hash_)
+
+    def savePhoto(self, photo_xml):
         """Parse a <PHOTO> elem and save the picture"""
         for elem in photo_xml.elements():
             if elem.name == 'TYPE':
@@ -148,13 +184,7 @@
                 log.debug(_('Decoding binary'))
                 decoded = b64decode(str(elem))
                 image_hash = sha1(decoded).hexdigest()
-                filename = self.avatar_path + '/' + image_hash
-                if not os.path.exists(filename):
-                    with open(filename, 'wb') as file_:
-                        file_.write(decoded)
-                    log.debug(_("file saved to %s") % image_hash)
-                else:
-                    log.debug(_("file [%s] already in cache") % image_hash)
+                self.saveAvatarFile(decoded, image_hash)
                 return image_hash
 
     @defer.inlineCallbacks
@@ -168,7 +198,7 @@
                 dictionary['fullname'] = unicode(elem)
             elif elem.name == 'NICKNAME':
                 dictionary['nick'] = unicode(elem)
-                self.update_cache(target, 'nick', dictionary['nick'], profile)
+                self.updateCache(target, 'nick', dictionary['nick'], profile)
             elif elem.name == 'URL':
                 dictionary['website'] = unicode(elem)
             elif elem.name == 'EMAIL':
@@ -176,17 +206,24 @@
             elif elem.name == 'BDAY':
                 dictionary['birthday'] = unicode(elem)
             elif elem.name == 'PHOTO':
-                dictionary["avatar"] = yield threads.deferToThread(self.save_photo, elem)
+                dictionary["avatar"] = yield threads.deferToThread(self.savePhoto, elem)
                 if not dictionary["avatar"]:  # can happen in case of e.g. empty photo elem
                     del dictionary['avatar']
                 else:
-                    self.update_cache(target, 'avatar', dictionary['avatar'], profile)
+                    self.updateCache(target, 'avatar', dictionary['avatar'], profile)
             else:
                 log.info(_('FIXME: [%s] VCard tag is not managed yet') % elem.name)
 
+        # if a data in cache doesn't exist anymore, we need to reset it
+        # so we check CACHED_DATA no gotten (i.e. not in dictionary keys)
+        # and we reset them
+        for datum in CACHED_DATA.difference(dictionary.keys()):
+            log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=target.full()))
+            self.updateCache(target, datum, '', profile)
+
         defer.returnValue(dictionary)
 
-    def vcard_ok(self, answer, profile):
+    def _VCardCb(self, answer, profile):
         """Called after the first get IQ"""
         log.debug(_("VCard found"))
 
@@ -202,30 +239,35 @@
             log.error(_("FIXME: vCard not found as first child element"))
             self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile)  # FIXME: maybe an error message would be better
 
-    def vcard_err(self, failure, profile):
+    def _VCardEb(self, failure, profile):
         """Called when something is wrong with registration"""
         try:
-            log.error(_("Can't find VCard of %s") % failure.value.stanza['from'])
+            log.warning(_("Can't find VCard of %s") % failure.value.stanza['from'])
             self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile)  # FIXME: maybe an error message would be better
+            self.updateCache(jid.JID(failure.value.stanza['from']), "avatar", '', profile)
         except AttributeError:  # 'ConnectionLost' object has no attribute 'stanza'
-            log.error(_("Can't find VCard: %s") % failure.getErrorMessage())
+            log.warning(_("Can't find VCard: %s") % failure.getErrorMessage())
 
-    def getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
+    def _getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
+        return self.getCard(jid.JID(target_s), profile_key)
+
+    def getCard(self, target, profile_key=C.PROF_KEY_NONE):
         """Ask server for VCard
-        @param target_s: jid from which we want the VCard
-        @result: id to retrieve the profile"""
+
+        @param target(jid.JID): jid from which we want the VCard
+        @result: id to retrieve the profile
+        """
         current_jid, xmlstream = self.host.getJidNStream(profile_key)
         if not xmlstream:
-            log.error(_('Asking vcard for a non-existant or not connected profile'))
-            return ""
+            raise exceptions.ProfileUnknownError('Asking vcard for a non-existant or not connected profile ({})'.format(profile_key))
         profile = self.host.memory.getProfileName(profile_key)
-        to_jid = jid.JID(target_s)
+        to_jid = target.userhostJID()
         log.debug(_("Asking for %s's VCard") % to_jid.userhost())
         reg_request = IQ(xmlstream, 'get')
         reg_request["from"] = current_jid.full()
         reg_request["to"] = to_jid.userhost()
         reg_request.addElement('vCard', NS_VCARD)
-        reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile])
+        reg_request.send(to_jid.userhost()).addCallbacks(self._VCardCb, self._VCardEb, callbackArgs=[profile], errbackArgs=[profile])
         return reg_request["id"]
 
     def getAvatarFile(self, avatar_hash):
@@ -266,6 +308,7 @@
         photo_elt.addElement('TYPE', content='image/png')
         photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
         img_hash = sha1(img_buf.getvalue()).hexdigest()
+        self.saveAvatarFile(img_buf.getvalue(), img_hash)
         return (vcard_set, img_hash)
 
     def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE):
@@ -281,9 +324,8 @@
         def elementBuilt(result):
             """Called once the image is at the right size/format, and the vcard set element is build"""
             set_avatar_elt, img_hash = result
-            self.avatars_cache[client.jid.userhost()] = img_hash  # we need to update the hash, so we can send a new presence
-                                                                 # element with the right hash
-            return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available())
+            self.updateCache(client.jid.userhostJID(), 'avatar', img_hash, client.profile)
+            return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" !
 
         d.addCallback(elementBuilt)
 
@@ -307,16 +349,31 @@
         return []
 
     def update(self, presence):
-        """Request for VCard's nickname
-        return the cached nickname if exists, else get VCard
+        """Called on <presence/> stanza with vcard data
+
+        Check for avatar information, and get VCard if needed
+        @param presend(domish.Element): <presence/> stanza
         """
-        from_jid = jid.JID(presence['from'])
+        # FIXME: doesn't manage MUC correctly
+        from_jid = jid.JID(presence['from']).userhostJID()
         #FIXME: wokkel's data_form should be used here
         x_elem = filter(lambda x: x.name == "x", presence.elements())[0]  # We only want the "x" element
         for elem in x_elem.elements():
             if elem.name == 'photo':
-                _hash = str(elem)
-                old_avatar = self.plugin_parent.get_cache(from_jid, 'avatar', self.parent.profile)
-                if not old_avatar or old_avatar != _hash:
-                    log.debug(_('New avatar found, requesting vcard'))
-                    self.plugin_parent.getCard(from_jid.userhost(), self.parent.profile)
+                hash_ = str(elem)
+                old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', self.parent.profile)
+                filename = self.plugin_parent._getFilename(hash_)
+                if not old_avatar or old_avatar != hash_:
+                    if os.path.exists(filename):
+                        log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(from_jid.full()))
+                        self.plugin_parent.updateCache(from_jid, 'avatar', hash_, self.parent.profile)
+                    else:
+                        log.debug(u'New avatar found for [{}], requesting vcard'.format(from_jid.full()))
+                        self.plugin_parent.getCard(from_jid, self.parent.profile)
+                else:
+                    if os.path.exists(filename):
+                        log.debug(u"avatar for {} already in cache".format(from_jid.full()))
+                    else:
+                        log.error(u"Avatar for [{}] should be in cache but it is not ! We get it".format(from_jid.full()))
+                        self.plugin_parent.getCard(from_jid, self.parent.profile)
+
--- a/src/plugins/plugin_xep_0060.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_xep_0060.py	Wed Mar 18 10:52:28 2015 +0100
@@ -55,7 +55,7 @@
     OPT_PUBLISH_MODEL = 'pubsub#publish_model'
 
     def __init__(self, host):
-        log.info(_("PubSub plugin initialization"))
+        log.info(_(u"PubSub plugin initialization"))
         self.host = host
         self.managedNodes = []
         self.clients = {}
@@ -202,7 +202,7 @@
         d_dict = {}
         for publisher, node in data.items():
             if node not in found_nodes:
-                log.debug("Skip the items retrieval for [{node}]: node doesn't exist".format(node=node))
+                log.debug(u"Skip the items retrieval for [{node}]: node doesn't exist".format(node=node))
                 continue  # avoid pubsub "item-not-found" error
             d_dict[publisher] = self.getItems(service, node, max_items, None, sub_id, rsm, profile)
         defer.returnValue(d_dict)
@@ -248,7 +248,7 @@
         d_list = []
         for nodeIdentifier in (set(nodeIdentifiers) - set(subscribed_nodes)):
             if nodeIdentifier not in found_nodes:
-                log.debug("Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier))
+                log.debug(u"Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier))
                 continue  # avoid sat-pubsub "SubscriptionExists" error
             d_list.append(client.subscribe(service, nodeIdentifier, sub_jid or client.parent.jid.userhostJID(), options=options))
         defer.returnValue(d_list)
@@ -278,7 +278,7 @@
 
     def deleteReceived(self, event):
         #TODO: manage delete event
-        log.debug(_("Publish node deleted"))
+        log.debug(_(u"Publish node deleted"))
 
     # def purgeReceived(self, event):
 
--- a/src/plugins/plugin_xep_0085.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_xep_0085.py	Wed Mar 18 10:52:28 2015 +0100
@@ -39,6 +39,7 @@
 PARAM_KEY = "Notifications"
 PARAM_NAME = "Enable chat state notifications"
 ENTITY_KEY = PARAM_KEY + "_" + PARAM_NAME
+DELETE_VALUE = "DELETE"
 
 PLUGIN_INFO = {
     "name": "Chat State Notifications Protocol Plugin",
@@ -125,29 +126,32 @@
             self.map[profile][to_jid]._onEvent('gone')
         del self.map[profile]
 
-    def updateEntityData(self, entity_jid, value, profile):
-        """
-        Update the entity data of the given profile for one or all contacts.
+    def updateCache(self, entity_jid, value, profile):
+        """Update the entity data of the given profile for one or all contacts.
         Reset the chat state(s) display if the notification has been disabled.
-        @param entity_jid: contact's JID, or '@ALL@' to update all contacts.
-        @param value: True, False or C.PROF_KEY_NONE to delete the entity data
+
+        @param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts.
+        @param value: True, False or DELETE_VALUE to delete the entity data
         @param profile: current profile
         """
-        self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile)
-        if not value or value == C.PROF_KEY_NONE:
-            # disable chat state for this or these contact(s)
-            self.host.bridge.chatStateReceived(unicode(entity_jid), "", profile)
+        if value == DELETE_VALUE:
+            self.host.memory.delEntityDatum(entity_jid, ENTITY_KEY, profile)
+        else:
+            self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile_key=profile)
+        if not value or value == DELETE_VALUE:
+            # reinit chat state UI for this or these contact(s)
+            self.host.bridge.chatStateReceived(entity_jid.full(), "", profile)
 
     def paramUpdateTrigger(self, name, value, category, type_, profile):
-        """
-        Reset all the existing chat state entity data associated with this profile after a parameter modification.
+        """Reset all the existing chat state entity data associated with this profile after a parameter modification.
+
         @param name: parameter name
         @param value: "true" to activate the notifications, or any other value to delete it
         @param category: parameter category
         @param type_: parameter type
         """
         if (category, name) == (PARAM_KEY, PARAM_NAME):
-            self.updateEntityData("@ALL@", True if value == "true" else C.PROF_KEY_NONE, profile)
+            self.updateCache(C.ENTITY_ALL, True if bool("true") else DELETE_VALUE, profile=profile)
             return False
         return True
 
@@ -160,20 +164,20 @@
             return True
 
         from_jid = JID(message.getAttribute("from"))
-        if self.__isMUC(from_jid, profile):
+        if self._isMUC(from_jid, profile):
             from_jid = from_jid.userhostJID()
         else:  # update entity data for one2one chat
-            assert(from_jid.resource)
+            # assert from_jid.resource # FIXME: assert doesn't work on normal message from server (e.g. server announce), because there is no resource
             try:
                 domish.generateElementsNamed(message.elements(), name="body").next()
                 try:
                     domish.generateElementsNamed(message.elements(), name="active").next()
                     # contact enabled Chat State Notifications
-                    self.updateEntityData(from_jid, True, profile)
+                    self.updateCache(from_jid, True, profile=profile)
                 except StopIteration:
                     if message.getAttribute('type') == 'chat':
                         # contact didn't enable Chat State Notifications
-                        self.updateEntityData(from_jid, False, profile)
+                        self.updateCache(from_jid, False, profile=profile)
                         return True
             except StopIteration:
                 pass
@@ -200,27 +204,27 @@
         def treatment(mess_data):
             message = mess_data['xml']
             to_jid = JID(message.getAttribute("to"))
-            if not self.__checkActivation(to_jid, forceEntityData=True, profile=profile):
+            if not self._checkActivation(to_jid, forceEntityData=True, profile=profile):
                 return mess_data
             try:
                 # message with a body always mean active state
                 domish.generateElementsNamed(message.elements(), name="body").next()
                 message.addElement('active', NS_CHAT_STATES)
                 # launch the chat state machine (init the timer)
-                if self.__isMUC(to_jid, profile):
+                if self._isMUC(to_jid, profile):
                     to_jid = to_jid.userhostJID()
                 self.__chatStateActive(to_jid, mess_data["type"], profile)
             except StopIteration:
                 if "chat_state" in mess_data["extra"]:
                     state = mess_data["extra"].pop("chat_state")
-                    assert(state in CHAT_STATES)
+                    assert state in CHAT_STATES
                     message.addElement(state, NS_CHAT_STATES)
             return mess_data
 
         post_xml_treatments.addCallback(treatment)
         return True
 
-    def __isMUC(self, to_jid, profile):
+    def _isMUC(self, to_jid, profile):
         """Tell if that JID is a MUC or not
 
         @param to_jid (JID): full or bare JID to check
@@ -229,13 +233,13 @@
         """
         try:
             type_ = self.host.memory.getEntityDatum(to_jid.userhostJID(), 'type', profile)
-            if type_ == 'chatroom':
+            if type_ == 'chatroom': # FIXME: should not use disco instead ?
                 return True
         except (exceptions.UnknownEntityError, KeyError):
             pass
         return False
 
-    def __checkActivation(self, to_jid, forceEntityData, profile):
+    def _checkActivation(self, to_jid, forceEntityData, profile):
         """
         @param to_jid: the contact's full JID (or bare if you know it's a MUC)
         @param forceEntityData: if set to True, a non-existing
@@ -247,15 +251,16 @@
         if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile):
             return False
         # check if notifications should be sent to this contact
-        if self.__isMUC(to_jid, profile):
+        if self._isMUC(to_jid, profile):
             return True
-        assert(to_jid.resource or not self.host.memory.isContactConnected(to_jid, profile))
+        # FIXME: this assertion crash when we want to send a message to an online bare jid
+        # assert to_jid.resource or not self.host.memory.isEntityAvailable(to_jid, profile) # must either have a resource, or talk to an offline contact
         try:
             return self.host.memory.getEntityDatum(to_jid, ENTITY_KEY, profile)
         except (exceptions.UnknownEntityError, KeyError):
             if forceEntityData:
                 # enable it for the first time
-                self.updateEntityData(to_jid, True, profile)
+                self.updateCache(to_jid, True, profile=profile)
                 return True
         # wait for the first message before sending states
         return False
@@ -305,11 +310,11 @@
         if profile is None:
             raise exceptions.ProfileUnknownError
         to_jid = JID(to_jid_s)
-        if self.__isMUC(to_jid, profile):
+        if self._isMUC(to_jid, profile):
             to_jid = to_jid.userhostJID()
         elif not to_jid.resource:
-            to_jid.resource = self.host.memory.getLastResource(to_jid, profile)
-        if not self.__checkActivation(to_jid, forceEntityData=False, profile=profile):
+            to_jid.resource = self.host.memory.getMainResource(to_jid, profile)
+        if not self._checkActivation(to_jid, forceEntityData=False, profile=profile):
             return
         try:
             self.map[profile][to_jid]._onEvent("composing")
@@ -347,9 +352,9 @@
         notification to the contact (the "active" state is
         automatically sent with each message) and set the timer.
         """
-        assert(state in TRANSITIONS)
+        assert state in TRANSITIONS
         transition = TRANSITIONS[state]
-        assert("next_state" in transition and "delay" in transition)
+        assert "next_state" in transition and "delay" in transition
 
         if state != self.state and state != "active":
             if state != 'gone' or self.mess_type != 'groupchat':
--- a/src/plugins/plugin_xep_0115.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_xep_0115.py	Wed Mar 18 10:52:28 2015 +0100
@@ -107,7 +107,7 @@
             client.caps_sent = False
         if cap_hash not in self.host.memory.disco.hashes:
             self.host.memory.disco.hashes[cap_hash] = disco_infos
-            self.host.memory.updateEntityData(client.jid, C.ENTITY_CAP_HASH, cap_hash, profile)
+            self.host.memory.updateEntityData(client.jid, C.ENTITY_CAP_HASH, cap_hash, profile_key=profile)
 
 
 class XEP_0115_handler(XMPPHandler):
@@ -146,7 +146,7 @@
         if c_ver in self.host.memory.disco.hashes:
             # we already know the hash, we update the jid entity
             log.debug ("hash [%(hash)s] already in cache, updating entity [%(jid)s]" % {'hash': c_ver, 'jid': from_jid.full()})
-            self.host.memory.updateEntityData(from_jid, C.ENTITY_CAP_HASH, c_ver, self.profile)
+            self.host.memory.updateEntityData(from_jid, C.ENTITY_CAP_HASH, c_ver, profile_key=self.profile)
             return
 
         if c_hash != 'sha-1': # unknown hash method
--- a/src/plugins/plugin_xep_0249.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/plugins/plugin_xep_0249.py	Wed Mar 18 10:52:28 2015 +0100
@@ -120,18 +120,18 @@
         #TODO: check parameters validity
         self.invite(jid.JID(target), jid.JID("%s@%s" % (roomId, service)), options, profile_key)
 
-    def _accept(self, room, profile_key=C.PROF_KEY_NONE):
-        """
-        Accept the invitation to join a MUC
-        @param room: room jid as string
+    def _accept(self, room_jid, profile_key=C.PROF_KEY_NONE):
+        """Accept the invitation to join a MUC.
+
+        @param room (jid.JID): JID of the room
         """
         profile = self.host.memory.getProfileName(profile_key)
         if not profile:
             log.error(_("Profile doesn't exists !"))
             return
-        log.info(_('Invitation accepted for room %(room)s [%(profile)s]') % {'room': room, 'profile': profile})
+        log.info(_('Invitation accepted for room %(room)s [%(profile)s]') % {'room': room_jid.userhost(), 'profile': profile})
         _jid, xmlstream = self.host.getJidNStream(profile)
-        d = self.host.plugins["XEP-0045"].join(jid.JID(room), _jid.user, {}, profile)
+        d = self.host.plugins["XEP-0045"].join(room_jid, _jid.user, {}, profile)
         return d
 
     def onInvitation(self, message, profile):
@@ -141,28 +141,29 @@
         @profile: %(doc_profile)s
         """
         try:
-            room = message.firstChildElement()['jid']
-            log.info(_('Invitation received for room %(room)s [%(profile)s]') % {'room': room, 'profile': profile})
+            room_jid_s = message.firstChildElement()['jid']
+            log.info(_('Invitation received for room %(room)s [%(profile)s]') % {'room': room_jid_s, 'profile': profile})
         except:
             log.error(_('Error while parsing invitation'))
             return
-        from_ = message["from"]
-        if room in self.host.plugins["XEP-0045"].clients[profile].joined_rooms:
+        from_jid_s = message["from"]
+        room_jid = jid.JID(room_jid_s)
+        if room_jid in self.host.plugins["XEP-0045"].clients[profile].joined_rooms:
             log.info(_("Invitation silently discarded because user is already in the room."))
             return
         autojoin = self.host.memory.getParamA(AUTOJOIN_NAME, AUTOJOIN_KEY, profile_key=profile)
 
         def accept_cb(conf_id, accepted, data, profile):
-            if conf_id == room and accepted:
-                self._accept(room, profile)
+            if conf_id == room_jid_s and accepted:
+                self._accept(room_jid, profile)
 
         if autojoin == "always":
-            self._accept(room, profile)
+            self._accept(room_jid, profile)
         elif autojoin == "never":
-            self.host.bridge.newAlert(_("An invitation from %(user)s to join the room %(room)s has been declined according to your personal settings.") % {'user': from_, 'room': room}, _("MUC invitation"), "INFO", profile)
+            self.host.bridge.newAlert(_("An invitation from %(user)s to join the room %(room)s has been declined according to your personal settings.") % {'user': from_jid_s, 'room': room_jid_s}, _("MUC invitation"), "INFO", profile)
         else:  # leave the default value here
-            data = {"message": _("You have been invited by %(user)s to join the room %(room)s. Do you accept?") % {'user': from_, 'room': room}, "title": _("MUC invitation")}
-            self.host.askConfirmation(room, "YES/NO", data, accept_cb, profile)
+            data = {"message": _("You have been invited by %(user)s to join the room %(room)s. Do you accept?") % {'user': from_jid_s, 'room': room_jid_s}, "title": _("MUC invitation")}
+            self.host.askConfirmation(room_jid_s, "YES/NO", data, accept_cb, profile)
 
     def cmd_invite(self, mess_data, profile):
         """invite someone in the room
--- a/src/stdui/ui_contact_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/stdui/ui_contact_list.py	Wed Mar 18 10:52:28 2015 +0100
@@ -50,7 +50,7 @@
         @return: list[string]
         """
         client = self.host.getClient(profile)
-        ret = [contact.userhost() for contact in client.roster.getBareJids()]
+        ret = [contact.full() for contact in client.roster.getJids()]
         ret.sort()
         return ret
 
--- a/src/stdui/ui_profile_manager.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/stdui/ui_profile_manager.py	Wed Mar 18 10:52:28 2015 +0100
@@ -48,20 +48,20 @@
         def gotProfileCipher(profile_cipher):
             if self.host.memory.auth_sessions.profileGetUnique(profile):
                 # case 1: profile already authenticated
-                return {'authenticated_profile': profile, 'caller': data['caller']}
+                return {'validated': C.boolConst(True)}
             self.profile_ciphers[profile] = profile_cipher
             if 'profile_password' in data:
                 # case 2: password is provided by the caller
                 return self._verifyPassword(data, profile)
 
-            def check_empty_password(empty_password_result):
-                if 'authenticated_profile' in empty_password_result:
+            def check_empty_password(verify_password_result):
+                validated = C.bool(verify_password_result['validated'])
+                if validated:
                     # case 3: there's no password for this profile
-                    return empty_password_result
+                    return verify_password_result
 
                 # case 4: prompt the user for a password
                 def xmlui_cb(data_, profile):
-                    data_['caller'] = data['caller']
                     return self._verifyPassword(data_, profile)
 
                 callback_id = self.host.registerCallback(xmlui_cb, with_data=True, one_shot=True)
@@ -69,11 +69,10 @@
                 form_ui.addPassword('profile_password', value='')
                 return {'xmlui': form_ui.toXml()}
 
-            check_empty_data = {'profile_password': '', 'caller': data['caller']}
+            check_empty_data = {'profile_password': ''}
             d = self._verifyPassword(check_empty_data, profile)
             return d.addCallback(check_empty_password)
 
-        assert(data['caller'])
         d = self.host.memory.asyncGetStringParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile)
         d.addCallback(gotProfileCipher)
         d.addErrback(self.getParamError)
@@ -82,7 +81,7 @@
     def getParamError(self, failure):
         _dialog = xml_tools.XMLUI('popup', title=D_('Error'))
         _dialog.addText(D_("Can't get profile parameter."))
-        return {'xmlui': _dialog.toXml()}
+        return {'xmlui': _dialog.toXml(), 'validated': C.boolConst(False)}
 
     @defer.inlineCallbacks
     def _verifyPassword(self, data, profile):
@@ -93,7 +92,6 @@
         @return: deferred dict
         """
         assert(profile in self.profile_ciphers)
-        assert(data['caller'])
 
         try:
             profile_password = data[xml_tools.formEscape('profile_password')]
@@ -103,10 +101,10 @@
         if not verified:
             _dialog = xml_tools.XMLUI('popup', title=D_('Connection error'))
             _dialog.addText(D_("The provided profile password doesn't match."))
-            defer.returnValue({'xmlui': _dialog.toXml()})
+            defer.returnValue({'xmlui': _dialog.toXml(), 'validated': C.boolConst(False)})
 
         yield self.host.memory.newAuthSession(profile_password, profile)
-        defer.returnValue({'authenticated_profile': profile, 'caller': data['caller']})
+        defer.returnValue({'validated': C.boolConst(True)})
 
     def _changeXMPPPassword(self, data, profile):
         session_data = self._sessions.profileGetUnique(profile)
--- a/src/test/helpers.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/test/helpers.py	Wed Mar 18 10:52:28 2015 +0100
@@ -312,7 +312,7 @@
     def delWaitingSub(self, contact_jid, profile_key):
         pass
 
-    def updateEntityData(self, entity_jid, key, value, profile_key):
+    def updateEntityData(self, entity_jid, key, value, silent=False, profile_key="@NONE@"):
         self.entities_data.setdefault(entity_jid, {})
         self.entities_data[entity_jid][key] = value
 
--- a/src/tools/misc.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/tools/misc.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,7 +20,6 @@
 """Misc usefull classes"""
 
 from sat.core.i18n import _
-import sys
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
@@ -39,8 +38,13 @@
     """This class manage triggers: code which interact to change the behaviour
     of SàT"""
 
-    MIN_PRIORITY = float('-inf')
-    MAX_PRIORITY = float('+inf')
+    try:  # FIXME: to be removed when a better solution is found
+        MIN_PRIORITY = float('-inf')
+        MAX_PRIORITY = float('+inf')
+    except:  # XXX: Pyjamas will bug if you specify ValueError here
+        # Pyjamas uses the JS Float class
+        MIN_PRIORITY = Number.NEGATIVE_INFINITY
+        MAX_PRIORITY = Number.POSITIVE_INFINITY
 
     def __init__(self):
         self.__triggers = {}
@@ -81,7 +85,7 @@
         if point_name not in self.__triggers:
             return True
 
-        for priority,trigger in self.__triggers[point_name]:
+        for priority, trigger in self.__triggers[point_name]:
             try:
                 if not trigger(*args, **kwargs):
                     return False