changeset 679:a90cc8fc9605

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 16:15:18 +0100
parents 1bffc4c244c3 (current diff) 2e087e093e7f (diff)
children 3b185ccb70b4
files src/browser/libervia_main.py src/browser/public/libervia.css src/browser/sat_browser/base_panels.py src/browser/sat_browser/blog.py src/browser/sat_browser/card_game.py src/browser/sat_browser/chat.py src/browser/sat_browser/contact.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/jid.py src/browser/sat_browser/json.py src/browser/sat_browser/main_panel.py src/browser/sat_browser/panels.py src/browser/sat_browser/radiocol.py src/browser/sat_browser/widget.py src/common/constants.py src/server/blog.py src/server/server.py
diffstat 43 files changed, 6991 insertions(+), 6201 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Feb 05 12:05:32 2015 +0100
+++ b/.hgignore	Wed Mar 18 16:15:18 2015 +0100
@@ -7,7 +7,6 @@
 tags
 twistd.log
 twistd.pid
-output
 sat.egg-info
 *.un~
 dist
@@ -16,3 +15,4 @@
 build/
 Session.vim
 ctags_links/
+html/
--- a/setup.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/setup.py	Wed Mar 18 16:15:18 2015 +0100
@@ -119,7 +119,10 @@
         os.symlink(os.path.dirname(sat.__file__), os.path.join(tmp_dir,"sat")) # FIXME: only work on unixes
         os.symlink(os.path.dirname(sat_frontends.__file__), os.path.join(tmp_dir,"sat_frontends")) # FIXME: only work on unixes
         os.symlink(os.path.dirname(libervia.__file__), os.path.join(tmp_dir,"libervia")) # FIXME: only work on unixes
-        result = subprocess.call(['pyjsbuild', 'libervia_main', '-d', '--no-compile-inplace', '-I', tmp_dir, '-o', self.pyjamas_output_dir])
+        for module in ('libervia_main', 'libervia_test'):
+            result = subprocess.call(['pyjsbuild', module, '-d', '--no-compile-inplace', '-I', tmp_dir, '-o', self.pyjamas_output_dir])
+            if result != 0:
+                continue
         shutil.rmtree(tmp_dir)
         os.chdir(cwd)
         return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/collections.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+class OrderedDict(object):
+    """Naive implementation of OrderedDict which is compatible with pyjamas"""
+
+    def __init__(self, *args, **kwargs):
+        self.__internal_dict = {}
+        self.__keys = [] # this list keep the keys in order
+        if args:
+            if len(args)>1:
+                raise TypeError("OrderedDict expected at most 1 arguments, got {}".format(len(args)))
+            if isinstance(args[0], (dict, OrderedDict)):
+                for key, value in args[0].iteritems():
+                    self[key] = value
+            for key, value in args[0]:
+                self[key] = value
+
+    def __len__(self):
+        return len(self.__keys)
+
+    def __setitem__(self, key, value):
+        if key not in self.__keys:
+            self.__keys.append(key)
+        self.__internal_dict[key] = value
+
+    def __getitem__(self, key):
+        return self.__internal_dict[key]
+
+    def __delitem__(self, key):
+        del self.__internal_dict[key]
+        self.__keys.remove(key)
+
+    def __contains__(self, key):
+        return key in self.__keys
+
+    def clear(self):
+        self.__internal_dict.clear()
+        del self.__keys[:]
+
+    def copy(self):
+        return OrderedDict(self)
+
+    @classmethod
+    def fromkeys(cls, seq, value=None):
+        ret = OrderedDict()
+        for key in seq:
+            ret[key] = value
+        return ret
+
+    def get(self, key, default=None):
+        try:
+            return self.__internal_dict[key]
+        except KeyError:
+            return default
+
+    def has_key(self, key):
+        return key in self.__keys
+
+    def keys(self):
+        return self.__keys[:]
+
+    def iterkeys(self):
+        for key in self.__keys:
+            yield key
+
+    def items(self):
+        ret = []
+        for key in self.__keys:
+            ret.append((key, self.__internal_dict[key]))
+        return ret
+
+    def iteritems(self):
+        for key in self.__keys:
+            yield (key, self.__internal_dict[key])
+
+    def values(self):
+        ret = []
+        for key in self.__keys:
+            ret.append(self.__internal_dict[key])
+        return ret
+
+    def itervalues(self):
+        for key in self.__keys:
+            yield (self.__internal_dict[key])
+
+    def popitem(self, last=True):
+        try:
+            key = self.__keys.pop(-1 if last else 0)
+        except IndexError:
+            raise KeyError('dictionnary is empty')
+        value = self.__internal_dict.pop(key)
+        return((key, value))
+
+    def setdefault(self, key, default=None):
+        try:
+            return self.__internal_dict[key]
+        except KeyError:
+            self[key] = default
+            return default
+
+    def update(self, *args, **kwargs):
+        if len(args) > 1:
+            raise TypeError('udpate expected at most 1 argument, got {}'.format(len(args)))
+        if args:
+            if hasattr(args[0], 'keys'):
+                for k in args[0]:
+                    self[k] = args[0][k]
+            else:
+                for (k, v) in args[0]:
+                    self[k] = v
+        for k, v in kwargs.items():
+            self[k] = v
+
+    def pop(self, *args):
+        if not args:
+            raise TypeError('pop expected at least 1 argument, got 0')
+        try:
+            self.__internal_dict.pop(args[0])
+        except KeyError:
+            if len(args) == 2:
+                return args[1]
+            raise KeyError(args[0])
+        self.__keys.remove(args[0])
+
+    def viewitems(self):
+        raise NotImplementedError
+
+    def viewkeys(self):
+        raise NotImplementedError
+
+    def viewvalues(self):
+        raise NotImplementedError
--- a/src/browser/libervia_main.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/libervia_main.py	Wed Mar 18 16:15:18 2015 +0100
@@ -17,7 +17,6 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import pyjd  # this is dummy in pyjs
 
 ### logging configuration ###
 from sat_browser import logging
@@ -26,8 +25,16 @@
 log = getLogger(__name__)
 ###
 
+from sat.core.i18n import D_
+
+from sat_frontends.quick_frontend.quick_app import QuickApp
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_menus
+
 from sat_frontends.tools.misc import InputHistory
 from sat_frontends.tools import strings
+from sat_frontends.tools import jid
+from sat_frontends.tools import host_listener
 from sat.core.i18n import _
 
 from pyjamas.ui.RootPanel import RootPanel
@@ -35,19 +42,22 @@
 from pyjamas.ui.KeyboardListener import KEY_ESCAPE
 from pyjamas.Timer import Timer
 from pyjamas import Window, DOM
-from pyjamas.JSONService import JSONProxy
 
+from sat_browser import json
 from sat_browser import register
-from sat_browser import contact
-from sat_browser import base_widget
-from sat_browser import panels
+from sat_browser.contact_list import ContactList
+from sat_browser import widget
+from sat_browser import main_panel
+from sat_browser import blog
 from sat_browser import dialog
-from sat_browser import jid
 from sat_browser import xmlui
 from sat_browser import html_tools
 from sat_browser import notification
+from sat_browser import libervia_widget
 
 from sat_browser.constants import Const as C
+import os.path
+
 
 try:
     # FIXME: import plugin dynamically
@@ -55,148 +65,36 @@
 except ImportError:
     pass
 
+
+unicode = str  # FIXME: pyjamas workaround
+
+
 MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
 
 # Set to true to not create a new LiberviaWidget when a similar one
 # already exist (i.e. a chat panel with the same target). Instead
 # the existing widget will be eventually removed from its parent
-# and added to new base_widget.WidgetsPanel, or replaced to the expected
+# and added to new libervia_widget.WidgetsPanel, or replaced to the expected
 # position if the previous and the new parent are the same.
-REUSE_EXISTING_LIBERVIA_WIDGETS = True
-
-
-class LiberviaJsonProxy(JSONProxy):
-    def __init__(self, *args, **kwargs):
-        JSONProxy.__init__(self, *args, **kwargs)
-        self.handler = self
-        self.cb = {}
-        self.eb = {}
-
-    def call(self, method, cb, *args):
-        _id = self.callMethod(method, args)
-        if cb:
-            if isinstance(cb, tuple):
-                if len(cb) != 2:
-                    log.error("tuple syntax for bridge.call is (callback, errback), aborting")
-                    return
-                if cb[0] is not None:
-                    self.cb[_id] = cb[0]
-                self.eb[_id] = cb[1]
-            else:
-                self.cb[_id] = cb
-
-    def onRemoteResponse(self, response, request_info):
-        if request_info.id in self.cb:
-            _cb = self.cb[request_info.id]
-            # if isinstance(_cb, tuple):
-            #     #we have arguments attached to the callback
-            #     #we send them after the answer
-            #     callback, args = _cb
-            #     callback(response, *args)
-            # else:
-            #     #No additional argument, we call directly the callback
-            _cb(response)
-            del self.cb[request_info.id]
-            if request_info.id in self.eb:
-                del self.eb[request_info.id]
-
-    def onRemoteError(self, code, errobj, request_info):
-        """def dump(obj):
-            print "\n\nDUMPING %s\n\n" % obj
-            for i in dir(obj):
-                print "%s: %s" % (i, getattr(obj,i))"""
-        if request_info.id in self.eb:
-            _eb = self.eb[request_info.id]
-            _eb((code, errobj))
-            del self.cb[request_info.id]
-            del self.eb[request_info.id]
-        else:
-            if code != 0:
-                log.error("Internal server error")
-                """for o in code, error, request_info:
-                    dump(o)"""
-            else:
-                if isinstance(errobj['message'], dict):
-                    log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
-                else:
-                    log.error("%s" % errobj['message'])
+# REUSE_EXISTING_LIBERVIA_WIDGETS = True # FIXME
 
 
-class RegisterCall(LiberviaJsonProxy):
-    def __init__(self):
-        LiberviaJsonProxy.__init__(self, "/register_api",
-                        ["isRegistered", "isConnected", "asyncConnect", "registerParams", "getMenus"])
-
-
-class BridgeCall(LiberviaJsonProxy):
-    def __init__(self):
-        LiberviaJsonProxy.__init__(self, "/json_api",
-                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
-                         "getMblogs", "getMassiveMblogs", "getMblogComments", "getProfileJid",
-                         "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined",
-                         "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
-                         "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments",
-                         "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
-                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
-                         "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer",
-                         "syntaxConvert", "getAccountDialogUI", "getLastResource"
-                        ])
-
-
-class BridgeSignals(LiberviaJsonProxy):
-    RETRY_BASE_DELAY = 1000
-
-    def __init__(self, host):
-        self.host = host
-        self.retry_delay = self.RETRY_BASE_DELAY
-        LiberviaJsonProxy.__init__(self, "/json_signal_api",
-                        ["getSignals"])
-
-    def onRemoteResponse(self, response, request_info):
-        self.retry_delay = self.RETRY_BASE_DELAY
-        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)
-
-    def onRemoteError(self, code, errobj, request_info):
-        if errobj['message'] == 'Empty Response':
-            Window.getLocation().reload()  # XXX: reset page in case of session ended.
-                                           # FIXME: Should be done more properly without hard reload
-        LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info)
-        #we now try to reconnect
-        if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0:
-            Window.alert('You are not allowed to connect to server')
-        else:
-            def _timerCb(timer):
-                self.host.bridge_signals.call('getSignals', self.host._getSignalsCB)
-            Timer(notify=_timerCb).schedule(self.retry_delay)
-            self.retry_delay *= 2
-
-
-class SatWebFrontend(InputHistory):
+class SatWebFrontend(InputHistory, QuickApp):
     def onModuleLoad(self):
         log.info("============ onModuleLoad ==============")
-        panels.ChatPanel.registerClass()
-        panels.MicroblogPanel.registerClass()
-        self.whoami = None
-        self._selected_listeners = set()
-        self.bridge = BridgeCall()
-        self.bridge_signals = BridgeSignals(self)
-        self.uni_box = None
-        self.status_panel = HTML('<br />')
-        self.contact_panel = contact.ContactPanel(self)
-        self.panel = panels.MainPanel(self)
-        self.discuss_panel = self.panel.discuss_panel
+        self.bridge_signals = json.BridgeSignals(self)
+        QuickApp.__init__(self, json.BridgeCall)
+        self.uni_box = None # FIXME: to be removed
+        self.panel = main_panel.MainPanel(self)
         self.tab_panel = self.panel.tab_panel
         self.tab_panel.addTabListener(self)
-        self.libervia_widgets = set()  # keep track of all actives LiberviaWidgets
-        self.room_list = []  # list of rooms
-        self.mblog_cache = []  # used to keep our own blog entries in memory, to show them in new mblog panel
-        self.avatars_cache = {}  # keep track of jid's avatar hash (key=jid, value=file)
         self._register_box = None
         RootPanel().add(self.panel)
+
         self.notification = notification.Notification()
         DOM.addEventPreview(self)
         self.importPlugins()
-        self._register = RegisterCall()
+        self._register = json.RegisterCall()
         self._register.call('getMenus', self.gotMenus)
         self._register.call('registerParams', None)
         self._register.call('isRegistered', self._isRegisteredCB)
@@ -205,35 +103,65 @@
         self.cached_params = {}
         self.next_rsm_index = 0
 
+        #FIXME: microblog cache should be managed directly in blog module
+        self.mblog_cache = []  # used to keep our own blog entries in memory, to show them in new mblog panel
+
+        # self._selected_listeners = set() # FIXME: to be done with new listeners mechanism
+
+    @property
+    def whoami(self):
+        # XXX: works because Libervia is mono-profile
+        #      if one day Libervia manage several profiles at once, this must be deleted
+        return self.profiles[C.PROF_KEY_NONE].whoami
+
+    @property
+    def contact_list(self):
+        return self.contact_lists[C.PROF_KEY_NONE]
+
+    @property
+    def visible_widgets(self):
+        widgets_panel = self.tab_panel.getCurrentPanel()
+        return [wid for wid in widgets_panel.widgets if isinstance(wid, quick_widgets.QuickWidget)]
+
+    @property
+    def base_location(self):
+        """Return absolute base url of this Libervia instance"""
+        url = Window.getLocation().getHref()
+        if url.endswith(C.LIBERVIA_MAIN_PAGE):
+            url = url[:-len(C.LIBERVIA_MAIN_PAGE)]
+        if url.endswith("/"):
+            url = url[:-1]
+        return url
+
+    def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
+        if handler is None:
+            callback = getattr(self, "{}{}".format(functionName, "Handler"))
+        else:
+            callback = handler
+
+        self.bridge_signals.register(functionName, callback, with_profile=with_profile)
+
     def importPlugins(self):
         self.plugins = {}
-        inhibited_menus = []
-        # FIXME: plugins import should be dynamic and generic like in sat
         try:
             self.plugins['otr'] = plugin_sec_otr.OTR(self)
         except TypeError:  # plugin_sec_otr has not been imported
-            inhibited_menus.append('OTR')
+            pass
 
-        class DummyPlugin(object):
-            def inhibitMenus(self):
-                return inhibited_menus
-
-        self.plugins['dummy_plugin'] = DummyPlugin()
-
-    def addSelectedListener(self, callback):
-        self._selected_listeners.add(callback)
+    # def addSelectedListener(self, callback):
+    #     self._selected_listeners.add(callback)
 
     def getSelected(self):
         wid = self.tab_panel.getCurrentPanel()
-        if not isinstance(wid, base_widget.WidgetsPanel):
-            log.error("Tab widget is not a base_widget.WidgetsPanel, can't get selected widget")
+        if not isinstance(wid, libervia_widget.WidgetsPanel):
+            log.error("Tab widget is not a WidgetsPanel, can't get selected widget")
             return None
         return wid.selected
 
     def setSelected(self, widget):
         """Define the selected widget"""
         widgets_panel = self.tab_panel.getCurrentPanel()
-        if not isinstance(widgets_panel, base_widget.WidgetsPanel):
+        if not isinstance(widgets_panel, libervia_widget.WidgetsPanel):
             return
 
         selected = widgets_panel.selected
@@ -244,13 +172,16 @@
         if selected:
             selected.removeStyleName('selected_widget')
 
+        # FIXME: check that widget is in the current WidgetsPanel
         widgets_panel.selected = widget
+        self.selected_widget = widget
 
         if widget:
             widgets_panel.selected.addStyleName('selected_widget')
 
-        for callback in self._selected_listeners:
-            callback(widget)
+        # FIXME:
+        # for callback in self._selected_listeners:
+        #     callback(widget)
 
     def resize(self):
         """Resize elements"""
@@ -260,9 +191,11 @@
         return True
 
     def onTabSelected(self, sender, tab_index):
-        selected = self.getSelected()
-        for callback in self._selected_listeners:
-            callback(selected)
+        pass
+        # selected = self.getSelected()
+        # FIXME:
+        # for callback in self._selected_listeners:
+        #     callback(selected)
 
     def onEventPreview(self, event):
         if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
@@ -270,26 +203,24 @@
             event.preventDefault()
         return True
 
-    def getAvatar(self, jid_str):
+    # FIXME: must not call _entityDataUpdatedCb by itself
+    #        should not get VCard, backend plugin must be fixed too
+    def getAvatarURL(self, jid_):
         """Return avatar of a jid if in cache, else ask for it.
 
-        @param jid_str (str): JID of the contact
-        @return: the URL to the avatar (str)
+        @param jid_ (jid.JID): JID of the contact
+        @return: the URL to the avatar (unicode)
         """
-        def dataReceived(result):
-            if 'avatar' in result:
-                self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar'])
-            else:
-                self.bridge.call("getCard", None, jid_str)
-
-        def avatarError(error_data):
-            # The jid is maybe not in our roster, we ask for the VCard
-            self.bridge.call("getCard", None, jid_str)
-
-        if jid_str not in self.avatars_cache:
-            self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar'])
-            self.avatars_cache[jid_str] = C.DEFAULT_AVATAR
-        return self.avatars_cache[jid_str]
+        assert isinstance(jid_, jid.JID)
+        contact_list = self.contact_list  # pyjamas issue: need a temporary variable to call a property's method
+        avatar_hash = contact_list.getCache(jid_, 'avatar')
+        if avatar_hash is None:
+            # we have no value for avatar_hash, so we request the vcard
+            self.bridge.getCard(unicode(jid_), profile=C.PROF_KEY_NONE)
+        if not avatar_hash:
+            return C.DEFAULT_AVATAR_URL
+        ret = os.path.join(C.AVATARS_DIR, avatar_hash)
+        return ret
 
     def registerWidget(self, wid):
         log.debug("Registering %s" % wid.getDebugName())
@@ -304,64 +235,65 @@
     def refresh(self):
         """Refresh the general display."""
         self.panel.refresh()
-        if self.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true':
-            self.uni_box = self.panel.unibox_panel.unibox
-        else:
-            self.uni_box = None
         for lib_wid in self.libervia_widgets:
             lib_wid.refresh()
         self.resize()
 
-    def addTab(self, label, wid, select=True):
-        """Create a new tab and eventually add a widget in
-        @param label: label of the tab
-        @param wid: LiberviaWidget to add
-        @param select: True to select the added tab
-        """
-        widgets_panel = base_widget.WidgetsPanel(self)
-        self.tab_panel.add(widgets_panel, label)
-        widgets_panel.addWidget(wid)
-        if select:
-            self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1)
-        return widgets_panel
-
     def addWidget(self, wid, tab_index=None):
         """ Add a widget at the bottom of the current or specified tab
+
         @param wid: LiberviaWidget to add
-        @param tab_index: index of the tab to add the widget to"""
+        @param tab_index: index of the tab to add the widget to
+        """
         if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount():
             panel = self.tab_panel.getCurrentPanel()
         else:
-            panel = self.tab_panel.tabBar.getTabWidget(tab_index)
+            panel = self.tab_panel.deck.getWidget(tab_index)
         panel.addWidget(wid)
 
     def displayNotification(self, title, body):
         self.notification.notify(title, body)
 
-    def gotMenus(self, menus):
+    def gotMenus(self, backend_menus):
         """Put the menus data in cache and build the main menu bar
 
-        @param menus (list[tuple]): menu data
+        @param backend_menus (list[tuple]): menu data from backend
         """
-        def process(menus, inhibited=None):
-            for raw_menu in menus:
-                id_, type_, path, path_i18n = raw_menu
-                if inhibited and path[0] in inhibited:
-                    continue
-                menus_data = self.menus.setdefault(type_, [])
-                menus_data.append((id_, path, path_i18n))
+        main_menu = self.panel.menu # most of global menu callbacks are in main_menu
+
+        # Categories (with icons)
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"General")], extra={'icon': 'home'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Contacts")], extra={'icon': 'social'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Groups")], extra={'icon': 'social'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Games")], extra={'icon': 'games'})
+
+        # menus to have before backend menus
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Discussion")), callback=main_menu.onJoinRoom)
+
+        # menus added by the backend/plugins (include other types than C.MENU_GLOBAL)
+        self.menus.addMenus(backend_menus, top_extra={'icon': 'plugins'})
+
+        # menus to have under backend menus
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Contacts"), D_(u"Manage groups")), callback=main_menu.onManageContactGroups)
 
-        self.menus = {}
-        inhibited = set()
-        extras = []
-        for plugin in self.plugins.values():
-            if hasattr(plugin, "inhibitMenus"):
-                inhibited.update(plugin.inhibitMenus())
-            if hasattr(plugin, "extraMenus"):
-                extras.extend(plugin.extraMenus())
-        process(menus, inhibited)
-        process(extras)
-        self.panel.menu.createMenus()
+        # separator and right hand menus
+        self.menus.addMenuItem(C.MENU_GLOBAL, [], quick_menus.MenuSeparator())
+
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Social contract")), top_extra={'icon': 'help'}, callback=main_menu.onSocialContract)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("About")), callback=main_menu.onAbout)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Account")), top_extra={'icon': 'settings'}, callback=main_menu.onAccount)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Parameters")), callback=main_menu.onParameters)
+        # XXX: temporary, will change when a full profile will be managed in SàT
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Upload avatar")), callback=main_menu.onAvatarUpload)
+
+        # we call listener to have menu added by local classes/plugins
+        self.callListeners('gotMenus')  # FIXME: to be done another way or moved to quick_app
+
+        # and finally the menus which must appear at the bottom
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Disconnect")), callback=main_menu.onDisconnect)
+
+        # we can now display all the menus
+        main_menu.update(C.MENU_GLOBAL)
 
     def _isRegisteredCB(self, result):
         registered, warning = result
@@ -386,17 +318,19 @@
             self._register_box.hide()
             del self._register_box  # don't work if self._register_box is None
 
-        # display the real presence status panel
-        self.panel.header.remove(self.status_panel)
-        self.status_panel = panels.PresenceStatusPanel(self)
-        self.panel.header.add(self.status_panel)
+        # display the presence status panel and tab bar
+        self.presence_status_panel = main_panel.PresenceStatusPanel(self)
+        self.panel.addPresenceStatusPanel(self.presence_status_panel)
+        self.panel.tab_panel.getTabBar().setVisible(True)
+
+        self.bridge_signals.call('getSignals', self.bridge_signals.signalHandler)
 
         #it's time to fill the page
-        self.bridge.call('getContacts', self._getContactsCB)
-        self.bridge.call('getParamsUI', self._getParamsUICB)
-        self.bridge_signals.call('getSignals', self._getSignalsCB)
-        #We want to know our own jid
-        self.bridge.call('getProfileJid', self._getProfileJidCB)
+        # self.bridge.call('getContacts', self._getContactsCB)
+        # self.bridge.call('getParamsUI', self._getParamsUICB)
+        # self.bridge_signals.call('getSignals', self._getSignalsCB)
+        # #We want to know our own jid
+        # self.bridge.call('getProfileJid', self._getProfileJidCB)
 
         def domain_cb(value):
             self._defaultDomain = value
@@ -405,18 +339,72 @@
         def domain_eb(value):
             self._defaultDomain = "libervia.org"
 
-        self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb))
-        self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
+        self.bridge.getNewAccountDomain(callback=domain_cb, errback=domain_eb)
+        self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg.
+        # self.discuss_panel.addWidget(panel.MicroblogPanel(self, []))
+
+        # # get cached params and refresh the display
+        # def param_cb(cat, name, count):
+        #     count[0] += 1
+        #     refresh = count[0] == len(C.CACHED_PARAMS)
+        #     return lambda value: self._paramUpdate(name, value, cat, refresh)
+
+        # count = [0]  # used to do something similar to DeferredList
+        # for cat, name in C.CACHED_PARAMS:
+        #     self.bridge.call('asyncGetParamA', param_cb(cat, name, count), name, cat)
+
+    def profilePlugged(self, dummy):  # FIXME: to be called as a "profilePlugged" listener?
+        QuickApp.profilePlugged(self, dummy)
+
+        microblog_widget = self.displayWidget(blog.MicroblogPanel, ())
+        self.setSelected(microblog_widget)
+
+        # we fill the panels already here
+        for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+            if wid.accept_all():
+                self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
+            else:
+                self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
+
+        #we ask for our own microblogs:
+        self.loadOurMainEntries()
 
-        # get cached params and refresh the display
-        def param_cb(cat, name, count):
-            count[0] += 1
-            refresh = count[0] == len(C.CACHED_PARAMS)
-            return lambda value: self._paramUpdate(name, value, cat, refresh)
+    def addContactList(self, dummy):
+        contact_list = ContactList(self)
+        self.panel.addContactList(contact_list)
+
+        # FIXME: the contact list height has to be set manually the first time
+        self.resize()
+
+        return contact_list
+
+    def newWidget(self, wid):
+        log.debug("newWidget: {}".format(wid))
+        self.addWidget(wid)
 
-        count = [0]  # used to do something similar to DeferredList
-        for cat, name in C.CACHED_PARAMS:
-            self.bridge.call('asyncGetParamA', param_cb(cat, name, count), name, cat)
+    def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile=C.PROF_KEY_NONE):
+        if type_ == C.MESS_TYPE_HEADLINE:
+            from_jid = jid.JID(from_jid_s)
+            if from_jid.domain == self._defaultDomain:
+                # we display announcement from the server in a dialog for better visibility
+                try:
+                    title = extra['subject']
+                except KeyError:
+                    title = _('Announcement from %s') % from_jid
+                msg = strings.addURLToText(html_tools.XHTML2Text(msg))
+                dialog.InfoDialog(title, msg).show()
+                return
+        QuickApp.newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile)
+
+    def disconnectedHandler(self, profile):
+        QuickApp.disconnectedHandler(self, profile)
+        Window.getLocation().reload()
+
+    def setStatusOnline(self, online=True, show='', statuses={}, profile=C.PROF_KEY_NONE):
+        self.presence_status_panel.setPresence(show)
+        if statuses:
+            # FIXME: retrieve user language status or fallback to 'default'
+            self.presence_status_panel.setStatus(statuses.values()[0])
 
     def _tryAutoConnect(self, skip_validation=False):
         """This method retrieve the eventual URL parameters to auto-connect the user.
@@ -447,7 +435,8 @@
         elif "public_blog" in data:
             # TODO: use the bare instead of node when all blogs can be retrieved
             node = jid.JID(data['public_blog']).node
-            self.addTab("%s's blog" % node, panels.WebPanel(self, "/blog/%s" % node))
+            # FIXME: "/blog/{}" won't work with unicode nodes
+            self.displayWidget(widget.WebWidget, "/blog/{}".format(node), show_url=False, new_tab=_(u"{}'s blog").format(unicode(node)))
         else:
             dialog.InfoDialog("Error",
                               "Unmanaged action result", Width="400px").center()
@@ -455,9 +444,9 @@
     def _actionEb(self, err_data):
         err_code, err_obj = err_data
         dialog.InfoDialog("Error",
-                          str(err_obj), Width="400px").center()
+                          unicode(err_obj), Width="400px").center()
 
-    def launchAction(self, callback_id, data):
+    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
@@ -465,78 +454,13 @@
         """
         if data is None:
             data = {}
-        self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data)
+        self.bridge.launchAction(callback_id, data, profile=profile, callback=self._actionCb, errback=self._actionEb)
 
     def _getContactsCB(self, contacts_data):
         for contact_ in contacts_data:
             jid, attributes, groups = contact_
             self._newContactCb(jid, attributes, groups)
 
-    def _getSignalsCB(self, signal_data):
-        self.bridge_signals.call('getSignals', self._getSignalsCB)
-        if len(signal_data) == 1:
-            signal_data.append([])
-        log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1]))
-        name, args = signal_data
-        if name == 'personalEvent':
-            self._personalEventCb(*args)
-        elif name == 'newMessage':
-            self._newMessageCb(*args)
-        elif name == 'presenceUpdate':
-            self._presenceUpdateCb(*args)
-        elif name == 'paramUpdate':
-            self._paramUpdate(*args)
-        elif name == 'roomJoined':
-            self._roomJoinedCb(*args)
-        elif name == 'roomLeft':
-            self._roomLeftCb(*args)
-        elif name == 'roomUserJoined':
-            self._roomUserJoinedCb(*args)
-        elif name == 'roomUserLeft':
-            self._roomUserLeftCb(*args)
-        elif name == 'roomUserChangedNick':
-            self._roomUserChangedNickCb(*args)
-        elif name == 'askConfirmation':
-            self._askConfirmation(*args)
-        elif name == 'newAlert':
-            self._newAlert(*args)
-        elif name == 'tarotGamePlayers':
-            self._tarotGameStartedCb(True, *args)
-        elif name == 'tarotGameStarted':
-            self._tarotGameStartedCb(False, *args)
-        elif name == 'tarotGameNew' or \
-             name == 'tarotGameChooseContrat' or \
-             name == 'tarotGameShowCards' or \
-             name == 'tarotGameInvalidCards' or \
-             name == 'tarotGameCardsPlayed' or \
-             name == 'tarotGameYourTurn' or \
-             name == 'tarotGameScore':
-            self._tarotGameGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolPlayers':
-            self._radioColStartedCb(True, *args)
-        elif name == 'radiocolStarted':
-            self._radioColStartedCb(False, *args)
-        elif name == 'radiocolPreload':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolPlay':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolNoUpload':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolUploadOk':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolSongRejected':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'subscribe':
-            self._subscribeCb(*args)
-        elif name == 'contactDeleted':
-            self._contactDeletedCb(*args)
-        elif name == 'newContact':
-            self._newContactCb(*args)
-        elif name == 'entityDataUpdated':
-            self._entityDataUpdatedCb(*args)
-        elif name == 'chatStateReceived':
-            self._chatStateReceivedCb(*args)
-
     def _getParamsUICB(self, xml_ui):
         """Hide the parameters item if there's nothing to display"""
         if not xml_ui:
@@ -558,48 +482,52 @@
                     _groups = set(mblog['groups'].split() if mblog['groups'] else [])
                 else:
                     _groups = None
-                mblog_entry = panels.MicroblogItem(mblog)
+                mblog_entry = blog.MicroblogItem(mblog)
                 cache.append((_groups, mblog_entry))
 
         self.mblog_cache.extend(cache)
         if len(self.mblog_cache) > MAX_MBLOG_CACHE:
             del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
 
-        widget_list = [mblog_panel] if mblog_panel else self.libervia_widgets
-        for lib_wid in widget_list:
-            if isinstance(lib_wid, panels.MicroblogPanel):
-                self.fillMicroblogPanel(lib_wid, cache)
+        widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel)
+
+        for wid in widget_list:
+            self.fillMicroblogPanel(wid, cache)
+
+        # FIXME
 
         if self.initialised:
             return
         self.initialised = True  # initialisation phase is finished here
         for event_data in self.init_cache:  # so we have to send all the cached events
-            self._personalEventCb(*event_data)
+            self.personalEventHandler(*event_data)
         del self.init_cache
 
     def _getProfileJidCB(self, jid_s):
-        self.whoami = jid.JID(jid_s)
-        #we can now ask our status
-        self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb)
-        #the rooms where we are
-        self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb)
-        #and if there is any subscription request waiting for us
-        self.bridge.call('getWaitingSub', self._getWaitingSubCb)
-        #we fill the panels already here
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.MicroblogPanel):
-                if lib_wid.accept_all():
-                    self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'ALL', [])
-                else:
-                    self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups)
+        # FIXME
+        raise Exception("should not be here !")
+        # self.whoami = jid.JID(jid_s)
+        # #we can now ask our status
+        # self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb)
+        # #the rooms where we are
+        # self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb)
+        # #and if there is any subscription request waiting for us
+        # self.bridge.call('getWaitingSub', self._getWaitingSubCb)
+        # #we fill the panels already here
+        # for lib_wid in self.libervia_widgets:
+        #     if isinstance(lib_wid, panel.MicroblogPanel):
+        #         if lib_wid.accept_all():
+        #             self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10)
+        #         else:
+        #             self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10)
 
-        #we ask for our own microblogs:
-        self.loadOurMainEntries()
+        # #we ask for our own microblogs:
+        # self.loadOurMainEntries()
 
-        # initialize plugins which waited for the connection to be done
-        for plugin in self.plugins.values():
-            if hasattr(plugin, 'profileConnected'):
-                plugin.profileConnected()
+        # # initialize plugins which waited for the connection to be done
+        # for plugin in self.plugins.values():
+        #     if hasattr(plugin, 'profileConnected'):
+        #         plugin.profileConnected()
 
     def loadOurMainEntries(self, index=0, mblog_panel=None):
         """Load a page of our own blogs from the cache or ask them to the
@@ -610,7 +538,7 @@
         """
         delta = index - self.next_rsm_index
         if delta < 0:
-            assert(mblog_panel is not None)
+            assert mblog_panel is not None
             self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS])
             return
 
@@ -618,12 +546,13 @@
             self._ownBlogsFills(result, mblog_panel)
 
         rsm = {'max': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
-        self.bridge.call('getMassiveMblogs', cb, 'JID', [self.whoami.bare], rsm)
+        self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE)
         self.next_rsm_index = index + C.RSM_MAX_ITEMS
 
     ## Signals callbacks ##
 
-    def _personalEventCb(self, sender, event_type, data):
+    def personalEventHandler(self, sender, event_type, data):
+        # FIXME: move some code from here to QuickApp
         if not self.initialised:
             self.init_cache.append((sender, event_type, data))
             return
@@ -636,11 +565,10 @@
                 _groups = set(data['groups'].split() if data['groups'] else [])
             else:
                 _groups = None
-            mblog_entry = panels.MicroblogItem(data)
+            mblog_entry = blog.MicroblogItem(data)
 
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
-                    self.addBlogEntry(lib_wid, sender, _groups, mblog_entry)
+            for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+                wid.addEntryIfAccepted(sender, _groups, mblog_entry)
 
             if sender == self.whoami.bare:
                 found = False
@@ -657,9 +585,8 @@
                     if len(self.mblog_cache) > MAX_MBLOG_CACHE:
                         del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
         elif event_type == 'MICROBLOG_DELETE':
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
-                    lib_wid.removeEntry(data['type'], data['id'])
+            for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+                wid.removeEntry(data['type'], data['id'])
             log.debug("%s %s %s" % (self.whoami.bare, sender, data['type']))
 
             if sender == self.whoami.bare and data['type'] == 'main_item':
@@ -669,235 +596,276 @@
                         self.mblog_cache.remove(entry)
                         break
 
-    def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry):
-        """Check if an entry can go in MicroblogPanel and add to it
-        @param mblog_panel: MicroblogPanel instance
-        @param sender: jid of the entry sender
-        @param _groups: groups which can receive this entry
-        @param mblog_entry: panels.MicroblogItem instance"""
-        if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \
-           or (_groups and _groups.intersection(mblog_panel.accepted_groups)):
-            mblog_panel.addEntry(mblog_entry)
-
     def fillMicroblogPanel(self, mblog_panel, mblogs):
         """Fill a microblog panel with entries in cache
+
         @param mblog_panel: MicroblogPanel instance
         """
         #XXX: only our own entries are cached
         for cache_entry in mblogs:
             _groups, mblog_entry = cache_entry
-            self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry)
+            mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry)
 
     def getEntityMBlog(self, entity):
         log.info("geting mblog for entity [%s]" % (entity,))
         for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.MicroblogPanel):
+            if isinstance(lib_wid, blog.MicroblogPanel):
                 if lib_wid.isJidAccepted(entity):
-                    self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [entity])
+                    self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)])
+
+    # def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True):
+    #     """Get the corresponding panel if it exists.
+    #     @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...)
+    #     @param entity (dict): dictionnary to define the entity.
+    #     @param ignoreOtherTabs (bool): if True, the widgets that are not
+    #     contained by the currently selected tab will be ignored
+    #     @return: the existing widget that has been found or None."""
+    #     selected_tab = self.tab_panel.getCurrentPanel()
+    #     for lib_wid in self.libervia_widgets:
+    #         parent = lib_wid.getWidgetsPanel(expect=False)
+    #         if parent is None or (ignoreOtherTabs and parent != selected_tab):
+    #             # do not return a widget that is not in the currently selected tab
+    #             continue
+    #         if isinstance(lib_wid, class_):
+    #             try:
+    #                 if lib_wid.matchEntity(*(entity.values())):  # XXX: passing **entity bugs!
+    #                     log.debug("existing widget found: %s" % lib_wid.getDebugName())
+    #                     return lib_wid
+    #             except AttributeError as e:
+    #                 e.stack_list()
+    #                 return None
+    #     return None
 
-    def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True):
-        """Get the corresponding panel if it exists.
-        @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...)
-        @param entity (dict): dictionnary to define the entity.
-        @param ignoreOtherTabs (bool): if True, the widgets that are not
-        contained by the currently selected tab will be ignored
-        @return: the existing widget that has been found or None."""
-        selected_tab = self.tab_panel.getCurrentPanel()
-        for lib_wid in self.libervia_widgets:
-            parent = lib_wid.getWidgetsPanel(expect=False)
-            if parent is None or (ignoreOtherTabs and parent != selected_tab):
-                # do not return a widget that is not in the currently selected tab
-                continue
-            if isinstance(lib_wid, class_):
-                try:
-                    if lib_wid.matchEntity(*(entity.values())):  # XXX: passing **entity bugs!
-                        log.debug("existing widget found: %s" % lib_wid.getDebugName())
-                        return lib_wid
-                except AttributeError as e:
-                    e.stack_list()
-                    return None
-        return None
+    def displayWidget(self, class_, target, dropped=False, new_tab=None, *args, **kwargs):
+        """Get or create a LiberviaWidget and select it. When the user dropped
+        something, a new widget is always created, otherwise we look for an
+        existing widget and re-use it if it's in the current tab.
+
+        @arg class_(class): see quick_widgets.getOrCreateWidget
+        @arg target: see quick_widgets.getOrCreateWidget
+        @arg dropped(bool): if True, assume the widget has been dropped
+        @arg new_tab(unicode): if not None, it holds the name of a new tab to
+            open for the widget. If None, use the default behavior.
+        @param args(list): optional args to create a new instance of class_
+        @param kwargs(list): optional kwargs to create a new instance of class_
+        @return: the widget
+        """
+        kwargs['profile'] = C.PROF_KEY_NONE
+
+        if dropped:
+            kwargs['on_new_widget'] = None
+            kwargs['on_existing_widget'] = C.WIDGET_RECREATE
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+            self.setSelected(wid)
+            return wid
+
+        if new_tab:
+            kwargs['on_new_widget'] = None
+            kwargs['on_existing_widget'] = C.WIDGET_RECREATE
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+            self.tab_panel.addWidgetsTab(new_tab)
+            self.addWidget(wid, tab_index=self.tab_panel.getWidgetCount() - 1)
+            return wid
 
-    def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None):
-        """Get the matching LiberviaWidget if it exists, or create a new one.
-        @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...)
-        @param entity (dict): dictionnary to define the entity.
-        @param select (bool): if True, select the widget that has been found or created
-        @param new_tab (str): if not None, a widget which is created is created in
-        a new tab. In that case new_tab is a unicode to label that new tab.
-        If new_tab is not None and a widget is found, no tab is created.
-        @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS
-         is set to False or if the widget has not been found, the existing
-         widget that has been found otherwise."""
-        lib_wid = None
-        tab = None
-        if REUSE_EXISTING_LIBERVIA_WIDGETS:
-            lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None)
-        if lib_wid is None:  # create a new widget
-            lib_wid = class_.createPanel(self, *(entity.values()))  # XXX: passing **entity bugs!
-            if new_tab is None:
-                self.addWidget(lib_wid)
-            else:
-                tab = self.addTab(new_tab, lib_wid, False)
-        else:  # reuse existing widget
-            tab = lib_wid.getWidgetsPanel(expect=False)
-            if new_tab is None:
-                if tab is not None:
-                    tab.removeWidget(lib_wid)
-                self.addWidget(lib_wid)
-        if select:
-            if new_tab is not None:
-                self.tab_panel.selectTab(tab)
-            # must be done after the widget is added,
-            # for example to scroll to the bottom
-            self.setSelected(lib_wid)
-            lib_wid.refresh()
-        return lib_wid
+        kwargs['on_existing_widget'] = C.WIDGET_RAISE
+        try:
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+        except quick_widgets.WidgetAlreadyExistsError:
+            kwargs['on_existing_widget'] = C.WIDGET_KEEP
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+            widgets_panel = wid.getParent(libervia_widget.WidgetsPanel, expect=False)
+            if widgets_panel is None:
+                # The widget exists but is hidden
+                self.addWidget(wid)
+            elif widgets_panel != self.tab_panel.getCurrentPanel():
+                # the widget is on an other tab, so we add a new one here
+                kwargs['on_existing_widget'] = C.WIDGET_RECREATE
+                wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+                self.addWidget(wid)
+        self.setSelected(wid)
+        return wid
+
 
-    def getRoomWidget(self, target):
-        """Get the MUC widget for the given target.
-
-        @param target (jid.JID): BARE jid of the MUC
-        @return: panels.ChatPanel instance or None
-        """
-        entity = {'item': target, 'type_': 'group'}
-        if target.full() in self.room_list or target in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
-            return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False)
-        return None
-
-    def getOrCreateRoomWidget(self, target):
-        """Get the MUC widget for the given target, create it if necessary.
+    # def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None):
+    #     """Get the matching LiberviaWidget if it exists, or create a new one.
+    #     @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...)
+    #     @param entity (dict): dictionnary to define the entity.
+    #     @param select (bool): if True, select the widget that has been found or created
+    #     @param new_tab (unicode): if not None, a widget which is created is created in
+    #     a new tab. In that case new_tab is a unicode to label that new tab.
+    #     If new_tab is not None and a widget is found, no tab is created.
+    #     @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS
+    #      is set to False or if the widget has not been found, the existing
+    #      widget that has been found otherwise."""
+    #     lib_wid = None
+    #     tab = None
+    #     if REUSE_EXISTING_LIBERVIA_WIDGETS:
+    #         lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None)
+    #     if lib_wid is None:  # create a new widget
+    #         lib_wid = class_.createPanel(self, *(entity.values()))  # XXX: passing **entity bugs!
+    #         if new_tab is None:
+    #             self.addWidget(lib_wid)
+    #         else:
+    #             tab = self.addTab(new_tab, lib_wid, False)
+    #     else:  # reuse existing widget
+    #         tab = lib_wid.getWidgetsPanel(expect=False)
+    #         if new_tab is None:
+    #             if tab is not None:
+    #                 tab.removeWidget(lib_wid)
+    #             self.addWidget(lib_wid)
+    #     if select:
+    #         if new_tab is not None:
+    #             self.tab_panel.selectTab(tab)
+    #         # must be done after the widget is added,
+    #         # for example to scroll to the bottom
+    #         self.setSelected(lib_wid)
+    #         lib_wid.refresh()
+    #     return lib_wid
 
-        @param target (jid.JID): BARE jid of the MUC
-        @return: panels.ChatPanel instance
-        """
-        lib_wid = self.getRoomWidget(target)
-        if lib_wid:
-            return lib_wid
-
-        # XXX: target.node.startwith(...) raises an error "startswith is not a function"
-        # This happens when node a is property defined in the JID class
-        # FIXME: pyjamas doesn't handle the properties well
-        node = target.node
+    # def getRoomWidget(self, target):
+    #     """Get the MUC widget for the given target.
 
-        # XXX: it's not really beautiful, but it works :)
-        if node.startswith('sat_tarot_'):
-            tab_name = "Tarot"
-        elif node.startswith('sat_radiocol_'):
-            tab_name = "Radio collective"
-        else:
-            tab_name = target.node
+    #     @param target (jid.JID): BARE jid of the MUC
+    #     @return: panel.ChatPanel instance or None
+    #     """
+    #     entity = {'item': target, 'type_': 'group'}
+    #     if target.full() in self.room_list or target in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
+    #         return self.getLiberviaWidget(panel.ChatPanel, entity, ignoreOtherTabs=False)
+    #     return None
 
-        self.room_list.append(target)
-        entity = {'item': target, 'type_': 'group'}
-        return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name)
+    # def getOrCreateRoomWidget(self, target):
+    #     """Get the MUC widget for the given target, create it if necessary.
 
-    def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra):
-        from_jid = jid.JID(from_jid_s)
-        to_jid = jid.JID(to_jid_s)
-        for plugin in self.plugins.values():
-            if hasattr(plugin, 'messageReceivedTrigger'):
-                if not plugin.messageReceivedTrigger(from_jid, msg, msg_type, to_jid, extra):
-                    return  # plugin returned False to interrupt the process
-        self.newMessageCb(from_jid, msg, msg_type, to_jid, extra)
+    #     @param target (jid.JID): BARE jid of the MUC
+    #     @return: panel.ChatPanel instance
+    #     """
+    #     lib_wid = self.getRoomWidget(target)
+    #     if lib_wid:
+    #         return lib_wid
+
+    #     # XXX: target.node.startwith(...) raises an error "startswith is not a function"
+    #     # This happens when node a is property defined in the JID class
+    #     # FIXME: pyjamas doesn't handle the properties well
+    #     node = target.node
+
+    #     # XXX: it's not really beautiful, but it works :)
+    #     if node.startswith('sat_tarot_'):
+    #         tab_name = "Tarot"
+    #     elif node.startswith('sat_radiocol_'):
+    #         tab_name = "Radio collective"
+    #     else:
+    #         tab_name = target.node
+
+    #     self.room_list.append(target)
+    #     entity = {'item': target, 'type_': 'group'}
+    #     return self.getOrCreateLiberviaWidget(panel.ChatPanel, entity, new_tab=tab_name)
 
-    def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
-        other = to_jid if from_jid.bare == self.whoami.bare else from_jid
-        lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False)
-        self.displayNotification(from_jid, msg)
-        if msg_type == 'headline' and from_jid.full() == self._defaultDomain:
-            try:
-                assert extra['subject']  # subject is defined and not empty
-                title = extra['subject']
-            except (KeyError, AssertionError):
-                title = _('Announcement from %s') % from_jid.full()
-            msg = strings.addURLToText(html_tools.XHTML2Text(msg))
-            dialog.InfoDialog(title, msg).show()
-            return
-        if lib_wid is not None:
-            if msg_type == C.MESS_TYPE_INFO:
-                lib_wid.printInfo(msg)
-            else:
-                lib_wid.printMessage(from_jid, msg, extra)
-            if 'header_info' in extra:
-                lib_wid.setHeaderInfo(extra['header_info'])
-        else:
-            # FIXME: "info" message and header info will be lost here
-            if not self.contact_panel.isContactInRoster(other.bare):
-                self.contact_panel.updateContact(other.bare, {}, [C.GROUP_NOT_IN_ROSTER])
-            # The message has not been shown, we must indicate it
-            self.contact_panel.setContactMessageWaiting(other.bare, True)
+    # def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra):
+    #     from_jid = jid.JID(from_jid_s)
+    #     to_jid = jid.JID(to_jid_s)
+    #     for plugin in self.plugins.values():
+    #         if hasattr(plugin, 'messageReceivedTrigger'):
+    #             if not plugin.messageReceivedTrigger(from_jid, msg, msg_type, to_jid, extra):
+    #                 return  # plugin returned False to interrupt the process
+    #     self.newMessageCb(from_jid, msg, msg_type, to_jid, extra)
+
+    # def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
+    #     other = to_jid if from_jid.bare == self.whoami.bare else from_jid
+    #     lib_wid = self.getLiberviaWidget(panel.ChatPanel, {'item': other}, ignoreOtherTabs=False)
+    #     self.displayNotification(from_jid, msg)
+    #     if msg_type == 'headline' and from_jid.full() == self._defaultDomain:
+    #         try:
+    #             assert extra['subject']  # subject is defined and not empty
+    #             title = extra['subject']
+    #         except (KeyError, AssertionError):
+    #             title = _('Announcement from %s') % from_jid.full()
+    #         msg = strings.addURLToText(html_tools.XHTML2Text(msg))
+    #         dialog.InfoDialog(title, msg).show()
+    #         return
+    #     if lib_wid is not None:
+    #         if msg_type == C.MESS_TYPE_INFO:
+    #             lib_wid.printInfo(msg)
+    #         else:
+    #             lib_wid.printMessage(from_jid, msg, extra)
+    #         if 'header_info' in extra:
+    #             lib_wid.setHeaderInfo(extra['header_info'])
+    #     else:
+    #         # FIXME: "info" message and header info will be lost here
+    #         if not self.contact_panel.isContactInRoster(other.bare):
+    #             self.contact_panel.updateContact(other.bare, {}, [C.GROUP_NOT_IN_ROSTER])
+    #         # The message has not been shown, we must indicate it
+    #         self.contact_panel.setContactMessageWaiting(other.bare, True)
 
-    def _presenceUpdateCb(self, entity, show, priority, statuses):
-        entity_jid = jid.JID(entity)
-        if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
-            assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
-            self.status_panel.setPresence(show)  # pylint: disable=E1103
-            if statuses:
-                self.status_panel.setStatus(statuses.values()[0])  # pylint: disable=E1103
-        else:
-            bare_jid = entity_jid.bareJID()
-            if bare_jid.full() in self.room_list or bare_jid in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
-                wid = self.getRoomWidget(bare_jid)
-            else:
-                wid = self.contact_panel
-                if show == 'unavailable':  # XXX: save some resources as for now we only need 'unavailable'
-                    for plugin in self.plugins.values():
-                        if hasattr(plugin, 'presenceReceivedTrigger'):
-                            plugin.presenceReceivedTrigger(entity_jid, show, priority, statuses)
-            if wid:
-                wid.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
+    # def _presenceUpdateCb(self, entity, show, priority, statuses):
+    #     entity_jid = jid.JID(entity)
+    #     if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
+    #         assert(isinstance(self.status_panel, main_panel.PresenceStatusPanel))
+    #         self.status_panel.setPresence(show)  # pylint: disable=E1103
+    #         if statuses:
+    #             self.status_panel.setStatus(statuses.values()[0])  # pylint: disable=E1103
+    #     else:
+    #         bare_jid = entity_jid.bareJID()
+    #         if bare_jid.full() in self.room_list or bare_jid in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
+    #             wid = self.getRoomWidget(bare_jid)
+    #         else:
+    #             wid = self.contact_panel
+    #             if show == 'unavailable':  # XXX: save some resources as for now we only need 'unavailable'
+    #                 for plugin in self.plugins.values():
+    #                     if hasattr(plugin, 'presenceReceivedTrigger'):
+    #                         plugin.presenceReceivedTrigger(entity_jid, show, priority, statuses)
+    #         if wid:
+    #             wid.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
 
-    def _roomJoinedCb(self, room_jid_s, room_nicks, user_nick):
-        chat_panel = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
-        chat_panel.setUserNick(user_nick)
-        chat_panel.setPresents(room_nicks)
-        chat_panel.refresh()
+    # def _roomJoinedCb(self, room_jid_s, room_nicks, user_nick):
+    #     chat_panel = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
+    #     chat_panel.setUserNick(user_nick)
+    #     chat_panel.setPresents(room_nicks)
+    #     chat_panel.refresh()
 
-    def _roomLeftCb(self, room_jid_s, room_nicks, user_nick):
-        try:
-            del self.room_list[room_jid_s]
-        except KeyError:
-            try:  # as JID is a string-based class,  we don't know what will please Pyjamas...
-                del self.room_list[jid.JID(room_jid_s)]
-            except KeyError:
-                pass
+    # def _roomLeftCb(self, room_jid_s, room_nicks, user_nick):
+    #     try:
+    #         del self.room_list[room_jid_s]
+    #     except KeyError:
+    #         try:  # as JID is a string-based class,  we don't know what will please Pyjamas...
+    #             del self.room_list[jid.JID(room_jid_s)]
+    #         except KeyError:
+    #             pass
 
-    def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
-        lib_wid = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.userJoined(user_nick, user_data)
+    # def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
+    #     lib_wid = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.userJoined(user_nick, user_data)
 
-    def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.userLeft(user_nick, user_data)
+    # def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.userLeft(user_nick, user_data)
 
-    def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick):
-        """Called when an user joined a MUC room"""
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.changeUserNick(old_nick, new_nick)
+    # def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick):
+    #     """Called when an user joined a MUC room"""
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.changeUserNick(old_nick, new_nick)
 
-    def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.startGame("Tarot", waiting, referee, players)
+    # def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.startGame("Tarot", waiting, referee, players)
 
-    def _tarotGameGenericCb(self, event_name, room_jid_s, args):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            getattr(lib_wid.getGame("Tarot"), event_name)(*args)
+    # def _tarotGameGenericCb(self, event_name, room_jid_s, args):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         getattr(lib_wid.getGame("Tarot"), event_name)(*args)
 
-    def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.startGame("RadioCol", waiting, referee, players, queue_data)
+    # def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.startGame("RadioCol", waiting, referee, players, queue_data)
 
-    def _radioColGenericCb(self, event_name, room_jid_s, args):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            getattr(lib_wid.getGame("RadioCol"), event_name)(*args)
+    # def _radioColGenericCb(self, event_name, room_jid_s, args):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         getattr(lib_wid.getGame("RadioCol"), event_name)(*args)
 
     def _getPresenceStatusesCb(self, presence_data):
         for entity in presence_data:
@@ -929,7 +897,8 @@
             msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_tools.html_sanitize(entity))
 
             def ok_cb(ignore):
-                self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups())
+                self.bridge.call('subscription', None, "subscribed", entity)
+                self.bridge.updateContact(entity, '', _dialog.getSelectedGroups())
 
             def cancel_cb(ignore):
                 self.bridge.call('subscription', None, "unsubscribed", entity, '', '')
@@ -945,31 +914,32 @@
         self.contact_panel.updateContact(contact_jid, attributes, groups)
 
     def _entityDataUpdatedCb(self, entity_jid_s, key, value):
+        raise Exception # FIXME should not be here
         if key == "avatar":
             avatar = '/' + C.AVATARS_DIR + value
             self.avatars_cache[entity_jid_s] = avatar
             self.contact_panel.updateAvatar(entity_jid_s, avatar)
 
             for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
+                if isinstance(lib_wid, blog.MicroblogPanel):
                     if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare):
                         lib_wid.updateValue('avatar', entity_jid_s, avatar)
 
-    def _chatStateReceivedCb(self, from_jid_s, state):
-        """Callback 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)
-        """
-        if from_jid_s == '@ALL@':
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.ChatPanel):
-                    lib_wid.setState(state, nick=C.ALL_OCCUPANTS)
-            return
-        from_jid = jid.JID(from_jid_s)
-        lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False)
-        lib_wid.setState(state, nick=from_jid.resource)
+    # def _chatStateReceivedCb(self, from_jid_s, state):
+    #     """Callback when a new chat state is received.
+    #     @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
+    #     @param state (unicode): new state
+    #     """
+    #     if from_jid_s == '@ALL@':
+    #         for lib_wid in self.libervia_widgets:
+    #             if isinstance(lib_wid, panel.ChatPanel):
+    #                 lib_wid.setState(state, nick=C.ALL_OCCUPANTS)
+    #         return
+    #     from_jid = jid.JID(from_jid_s)
+    #     lib_wid = self.getLiberviaWidget(panel.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False)
+    #     lib_wid.setState(state, nick=from_jid.resource)
 
-    def _askConfirmation(self, confirmation_id, confirmation_type, data):
+    def askConfirmationHandler(self, confirmation_id, confirmation_type, data):
         answer_data = {}
 
         def confirm_cb(result):
@@ -996,51 +966,53 @@
     def getCachedParam(self, category, name):
         """Return a parameter cached value (e.g for refreshing the UI)
 
-        @param category (str): the parameter category
-        @pram name (str): the parameter name
+        @param category (unicode): the parameter category
+        @pram name (unicode): the parameter name
         """
         return self.cached_params[(category, name)] if (category, name) in self.cached_params else None
 
     def sendError(self, errorData):
         dialog.InfoDialog("Error while sending message",
                           "Your message can't be sent", Width="400px").center()
-        log.error("sendError: %s" % str(errorData))
+        log.error("sendError: %s" % unicode(errorData))
 
-    def send(self, targets, text, extra={}):
-        """Send a message to any target type.
-        @param targets: list of tuples (type, entities, addr) with:
-        - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat")
-        - entities could be a JID, a list groups, a node hash... depending the target
-        - addr in ("To", "Cc", "Bcc") - ignore case
-        @param text: the message content
-        @param extra: options
-        """
-        # FIXME: too many magic strings, we should use constants instead
-        addresses = []
-        for target in targets:
-            type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower()
-            if type_ in ("PUBLIC", "GROUP"):
-                self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra)
-            elif type_ == "COMMENT":
-                self.bridge.call("sendMblogComment", None, entities, text, extra)
-            elif type_ == "STATUS":
-                assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
-                self.bridge.call('setStatus', None, self.status_panel.presence, text)  # pylint: disable=E1103
-            elif type_ in ("groupchat", "chat"):
-                addresses.append((addr, entities))
-            else:
-                log.error("Unknown target type")
-        if addresses:
-            if len(addresses) == 1 and addresses[0][0] == 'to':
-                to_jid_s = addresses[0][1]
-                for plugin in self.plugins.values():
-                    if hasattr(plugin, 'sendMessageTrigger'):
-                        if not plugin.sendMessageTrigger(jid.JID(to_jid_s), text, type_, extra):
-                            return  # plugin returned False to interrupt the process
-                self.bridge.call('sendMessage', (None, self.sendError), to_jid_s, text, '', type_, extra)
-            else:
-                extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])})
-                self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra)
+    # FIXME: this method is fat too complicated and depend of widget type
+    #        must be refactored and moved to each widget instead
+    # def send(self, targets, text, extra={}):
+    #     """Send a message to any target type.
+    #     @param targets: list of tuples (type, entities, addr) with:
+    #     - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat")
+    #     - entities could be a JID, a list groups, a node hash... depending the target
+    #     - addr in ("To", "Cc", "Bcc") - ignore case
+    #     @param text: the message content
+    #     @param extra: options
+    #     """
+    #     # FIXME: too many magic strings, we should use constants instead
+    #     addresses = []
+    #     for target in targets:
+    #         type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower()
+    #         if type_ in ("PUBLIC", "GROUP"):
+    #             self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra)
+    #         elif type_ == "COMMENT":
+    #             self.bridge.call("sendMblogComment", None, entities, text, extra)
+    #         elif type_ == "STATUS":
+    #             assert(isinstance(self.status_panel, main_panel.PresenceStatusPanel))
+    #             self.bridge.call('setStatus', None, self.status_panel.presence, text)  # pylint: disable=E1103
+    #         elif type_ in ("groupchat", "chat"):
+    #             addresses.append((addr, entities))
+    #         else:
+    #             log.error("Unknown target type")
+    #     if addresses:
+    #         if len(addresses) == 1 and addresses[0][0] == 'to':
+    #             to_jid_s = addresses[0][1]
+    #             for plugin in self.plugins.values():
+    #                 if hasattr(plugin, 'sendMessageTrigger'):
+    #                     if not plugin.sendMessageTrigger(jid.JID(to_jid_s), text, type_, extra):
+    #                         return  # plugin returned False to interrupt the process
+    #             self.bridge.call('sendMessage', (None, self.sendError), to_jid_s, text, '', type_, extra)
+    #         else:
+    #             extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])})
+    #             self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra)
 
     def showWarning(self, type_=None, msg=None):
         """Display a popup information message, e.g. to notify the recipient of a message being composed.
@@ -1049,11 +1021,25 @@
         @msg: message to be displayed
         """
         if not hasattr(self, "warning_popup"):
-            self.warning_popup = panels.WarningPopup()
+            self.warning_popup = main_panel.WarningPopup()
         self.warning_popup.showWarning(type_, msg)
 
+    def showDialog(self, message, title="", type_="info", answer_cb=None, answer_data=None):
+        if type_ == 'info':
+            popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb)
+        elif type_ == 'error':
+            popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb)
+        elif type_ == 'yes/no':
+            popup = dialog.ConfirmDialog(lambda answer: answer_cb(answer, answer_data),
+                                         text=unicode(message), title=unicode(title))
+            popup.cancel_button.setText(_("No"))
+        else:
+            popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb)
+            log.error(_('unmanaged dialog type: %s'), type_)
+        popup.show()
+
 
 if __name__ == '__main__':
     app = SatWebFrontend()
     app.onModuleLoad()
-    pyjd.run()
+    host_listener.callListeners(app)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/libervia_test.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+
+# Just visit <root_url>/test. If you don't get any AssertError pop-up,
+# everything is fine. #TODO: nicely display the results in HTML output.
+
+
+### logging configuration ###
+from sat_browser import logging
+logging.configure()
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from sat_frontends.tools import jid
+from sat_browser import contact_list
+
+
+def test_JID():
+    """Check that the JID class reproduces the Twisted behavior"""
+    j1 = jid.JID("t1@test.org")
+    j1b = jid.JID("t1@test.org")
+    t1 = "t1@test.org"
+
+    assert j1 == j1b
+    assert j1 != t1
+    assert t1 != j1
+    assert hash(j1) == hash(j1b)
+    assert hash(j1) != hash(t1)
+
+
+def test_JIDIterable():
+    """Check that our iterables reproduce the Twisted behavior"""
+
+    j1 = jid.JID("t1@test.org")
+    j1b = jid.JID("t1@test.org")
+    j2 = jid.JID("t2@test.org")
+    t1 = "t1@test.org"
+    t2 = "t2@test.org"
+    jid_set = set([j1, t2])
+    jid_list = contact_list.JIDList([j1, t2])
+    jid_dict = {j1: "dummy 1", t2: "dummy 2"}
+    for iterable in (jid_set, jid_list, jid_dict):
+        log.info("Testing %s" % type(iterable))
+        assert j1 in iterable
+        assert j1b in iterable
+        assert j2 not in iterable
+        assert t1 not in iterable
+        assert t2 in iterable
+
+    # Check that the extra JIDList class is still needed
+    log.info("Testing Pyjamas native list")
+    jid_native_list = ([j1, t2])
+    assert j1 in jid_native_list
+    assert j1b not in jid_native_list  # this is NOT Twisted's behavior
+    assert j2 in jid_native_list  # this is NOT Twisted's behavior
+    assert t1 in jid_native_list  # this is NOT Twisted's behavior
+    assert t2 in jid_native_list
+
+test_JID()
+test_JIDIterable()
--- a/src/browser/public/libervia.css	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/public/libervia.css	Wed Mar 18 16:15:18 2015 +0100
@@ -137,10 +137,13 @@
 .header {
     background-color: #eee;
     border-bottom: 1px solid #ddd;
+    width: 100%;
+    height: 64px;
 }
 
-.menuContainer {
-    margin: 0 32px 0 20px;   
+.mainPanel {
+    width: 100%;
+    height: 100%;
 }
 
 .mainMenuBar {
@@ -148,7 +151,6 @@
     background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
     background: -webkit-linear-gradient(top, #444444, #222222);
     background: linear-gradient(to bottom, #444444, #222222);
-    width: 100%;
     height: 28px;
     padding: 5px 5px 0 5px;    
     border: 1px solid #ddd;
@@ -157,6 +159,10 @@
     -webkit-box-shadow: 0px 1px 4px #000;
     box-shadow: 0px 1px 4px #000;
     display: inline-block;
+    position: absolute;
+    left: 20px;
+    right: 20px;
+    width: auto;
 }
 
 .mainMenuBar .gwt-MenuItem {
@@ -342,7 +348,6 @@
     text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
     padding: 3px 5px 3px 5px;
     margin: 10px 5px 10px 5px;
-    color: #fff;
     font-weight: bold;
     font-size: 1em;
     border: none; 
@@ -350,7 +355,12 @@
     transition: color 0.2s linear;
 }
 
-.gwt-DialogBox .gwt-button:hover {
+.gwt-DialogBox .gwt-button:enabled {
+    cursor: pointer;
+    color: #fff;
+}
+
+.gwt-DialogBox .gwt-button:enabled:hover {
     background-color: #cf2828;
     background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
     background: -webkit-linear-gradient(top, #cf2828, #981a1a);
@@ -420,16 +430,9 @@
 }
 /* Contact List */
 
-div.contactPanel {
+div.contactList {
     width: 100%;
-   /* We want the contact panel to not use all the available height when displayed
-      in the unibox panel (grey part), because the dialogs panels (white part) should
-      still be visible. The setting max-height: fit-content would be appropriate here
-      but it doesn't work with firefox 24.0. TODO: check if the current setting works
-      with other browsers... the panel should of course not be displayed on 100px
-      but exactly fit the contacts box.
-     */
-    max-height: 100px;
+    margin-top: 9px;
 }
 
 .contactTitle {
@@ -447,13 +450,14 @@
     border: 0;
     padding: 0;
     font-size: large;
+    margin-top: 9px;
 }
 
-.groupList {
+.groupPanel {
     width: 100%;    
 }
 
-.groupList tr:first-child td {
+.groupPanel tr:first-child td {
     padding-top: 10px;
 }
 
@@ -495,7 +499,7 @@
     background: #EDEDED;
 }
 
-.contactBox img {
+.contactBox img, .muc_contact img {
     width: 32px;
     height: 32px;
     border-radius: 5px;
@@ -539,6 +543,14 @@
     background-color: rgb(175, 175, 175);
 }
 
+/* Contacts in MUC */
+
+.muc_contact {
+    border-radius: 5px;
+    background: #EDEDED;
+    margin: 2px;
+}
+
 /* START - contact presence status */
 .contactLabel-connected {
     color: #3c7e0c;
@@ -610,10 +622,9 @@
     height:45px;
 }
 
-.statusPanel {
+.presenceStatusPanel {
     margin: auto;
     text-align: center;
-    width: 100%;
     padding: 5px 0px;
     text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
     font-size: 1.2em;
@@ -813,6 +824,12 @@
 
 /* BorderWidgets */
 
+.borderWidgetOnDrag {
+    background-color: lightgray;
+    border: 1px dashed #000;
+    border-radius: 1em;
+}
+
 .bottomBorderWidget {
     height: 10px !important;
 }
@@ -821,6 +838,14 @@
     width: 10px !important;
 }
 
+.leftBorderWidget {
+    float: right;
+}
+
+.rightBorderWidget {
+    float: left;
+}
+
 /* Microblog */
 
 .microblogPanel {
@@ -1040,10 +1065,9 @@
     white-space: nowrap;
 }
 
-.occupantsList {
+.occupantsPanelCell {
     border-right: 2px dotted #ddd;
-    margin-left: 5px;
-    margin-right: 10px;
+    padding-left: 5px;
     height: 100%;
 }
 
@@ -1148,9 +1172,6 @@
 
 /* Tab panel */
 
-.liberviaTabPanel {
-}
-
 .gwt-TabPanel {
 }
 
@@ -1164,13 +1185,6 @@
     border-bottom: 3px solid #a01c1c;  
 }
 
-.mainTabPanel .gwt-TabBar {
-    z-index: 10;
-    position: fixed;
-    bottom: 0;
-    left: 0;
-}
-
 .gwt-TabBar .gwt-TabBarFirst {
     height: 100%;
 }
@@ -1178,12 +1192,28 @@
 .gwt-TabBar .gwt-TabBarRest {
 }
 
-.liberviaTabPanel .gwt-TabBar {;
+.mainPanel .gwt-TabBar {
+    z-index: 10;
+}
+
+.mainPanel .gwt-TabBar-oneTab {
+    position: fixed;
+    left: 0px;
+    bottom: 0px;
+    border: none;
 }
 
-.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem {
-    cursor: pointer;
-    margin-right: 5px;
+.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper {
+    display: none;
+}
+
+.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper:nth-child(3) {
+    display: block;
+}
+
+.liberviaTabPanel {
+    width: 100%;
+    height: 100%;
 }
 
 .liberviaTabPanel .gwt-TabBarItem div {
@@ -1201,6 +1231,8 @@
     padding: 4px 15px 4px 15px;
     border-radius: 1em 1em 0 0;
     text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+    cursor: pointer;
+    margin-right: 5px;
 }
 
 .liberviaTabPanel .gwt-TabBarItem-selected {
@@ -1214,6 +1246,7 @@
     padding: 4px 15px 4px 15px;
     border-radius: 1em 1em 0 0;
     text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
+    cursor: default;
 }
 
 .liberviaTabPanel div.gwt-TabBarItem:hover {
@@ -1229,10 +1262,6 @@
     text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); 
 }
 
-.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem-selected {
-    cursor: default;
-}
-
 .globalLeftArea {
     margin-top: 9px;
 }
@@ -1357,23 +1386,23 @@
 
 /* Recipients panel */
 
-.recipientButtonCell {
+.itemButtonCell {
     width:55px;
 }
 
-.recipientTypeMenu {
+.itemKeyMenu {
 }
 
-.recipientTypeItem {
+.itemKey {
     cursor: pointer;
     border-radius: 5px;
     width: 50px;
 }
 
-.recipientPanel {
+.itemPanel {
 }
 
-.recipientTextBox {
+.itemTextBox {
     cursor: pointer;
     width: auto;
     border-radius: 5px 5px 5px 5px;
@@ -1385,32 +1414,26 @@
     font-size: 1em;
 }
 
-.recipientTextBox-invalid {
+.itemTextBox-invalid {
     -webkit-box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6);
     box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6);
     border: 1px solid rgb(255, 0, 0);
 }
 
-.recipientRemoveButton {
+.itemRemoveButton {
     margin: 0px 10px 0px 0px;
     padding: 0px;
     border: 1px dashed red;
     border-radius: 5px 5px 5px 5px;
 }
 
-.recipientRemoveIcon {
+.itemRemoveIcon {
     color: red;
     width:15px;
     height:15px;
     vertical-align: baseline;
 }
 
-.dragover-recipientPanel {
-    border-radius: 5px;
-    background: none repeat scroll 0% 0% rgb(135, 179, 255);
-    border: 1px dashed rgb(35,79,255);
-}
-
 .recipientSpacer {
     height: 15px;
 }
@@ -1448,6 +1471,12 @@
     vertical-align:middle;
 }
 
+.contactGroupPanel.dragover {
+    border-radius: 5px !important;
+    background: none repeat scroll 0% 0% rgb(135, 179, 255) !important;
+    border: 1px dashed rgb(35,79,255) !important;
+}
+
 .toggleAssignedContacts {
     white-space: nowrap;
 }
--- a/src/browser/sat_browser/base_menu.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/base_menu.py	Wed Mar 18 16:15:18 2015 +0100
@@ -24,201 +24,45 @@
 by base_widget.py, and the import sequence caused a JS runtime error."""
 
 
-import pyjd  # this is dummy in pyjs
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat.core import exceptions
 from pyjamas.ui.MenuBar import MenuBar
-from pyjamas.ui.UIObject import UIObject
 from pyjamas.ui.MenuItem import MenuItem
 from pyjamas import Window
-
-import re
+from sat_frontends.quick_frontend import quick_menus
+from sat_browser import html_tools
 
 
-class MenuCmd:
-    """Return an object with an "execute" method that can be set to a menu item callback"""
-
-    def __init__(self, object_, handler=None, data=None):
-        """
-        @param object_ (object): a callable or a class instance
-        @param handler (str): method name if object_ is a class instance
-        @param data (dict): data to pass as the callback argument
-        """
-        if handler is None:
-            assert(callable(object_))
-            self.callback = object_
-        else:
-            self.callback = getattr(object_, handler)
-        self.data = data
-
-    def execute(self):
-        self.callback(self.data) if self.data else self.callback()
-
-
-class PluginMenuCmd:
-    """Like MenuCmd, but instead of executing a method, it will command the bridge to launch an action"""
-
-    def __init__(self, host, action_id, menu_data=None):
-        self.host = host
-        self.action_id = action_id
-        self.menu_data = menu_data
-
-    def execute(self):
-        self.host.launchAction(self.action_id, self.menu_data)
+unicode = str  # FIXME: pyjamas workaround
 
 
-class MenuNode(object):
-    """MenuNode is a basic data structure to build a menu hierarchy.
-    When Pyjamas MenuBar and MenuItem defines UI elements, MenuNode
-    stores the logical relation between them."""
-
-    """This class has been introduced to deal with "flattened menus", when you
-    want the items of a sub-menu to be displayed in the parent menu. It was
-    needed to break the naive relation of "one MenuBar = one category"."""
+class MenuCmd(object):
+    """Return an object with an "execute" method that can be set to a menu item callback"""
 
-    def __init__(self, name=None, item=None, menu=None, flat_level=0):
-        """
-        @param name (str): node name
-        @param item (MenuItem): associated menu item
-        @param menu (GenericMenuBar): associated menu bar
-        @param flat_level (int): sub-menus until that level see their items
-        displayed in the parent menu bar, instead of in a callback popup.
-        """
-        self.name = name
-        self.item = item or None  # associated menu item
-        self.menu = menu or None  # associated menu bar (sub-menu)
-        self.flat_level = max(flat_level, -1)
-        self.children = []
-
-    def _getOrCreateCategory(self, path, path_i18n=None, types=None, create=False, sub_menu=None):
-        """Return the requested category. If create is True, path_i18n and
-        types are specified, recursively create the category and its parent.
-
-        @param path (list[str]): path to the category
-        @param path_i18n (list[str]): internationalized path to the category
-        @param types (list[str]): types of the category and its parents
-        @param create (bool): if True, create the category
-        @param sub_menu (GenericMenuBar): instance to popup as the category
-        sub-menu, if it is created. Otherwise keep the previous sub-menu.
-        @return: MenuNode or None
+    def __init__(self, menu_item, caller=None):
         """
-        assert(len(path) > 0 and len(path) == len(path_i18n) == len(types))
-        if len(path) > 1:
-            cat = self._getOrCreateCategory(path[:1], path_i18n[:1], types[:1], create)
-            return cat._getOrCreateCategory(path[1:], path_i18n[1:], types[1:], create, sub_menu) if cat else None
-        cats = [child for child in self.children if child.menu and child.name == path[0]]
-        if len(cats) == 1:
-            return cats[0]
-        assert(cats == [])  # there should not be more than 1 category with the same name
-        if create:
-            html = self.menu.getCategoryHTML(path_i18n[0], types[0])
-            sub_menu = sub_menu if sub_menu else GenericMenuBar(self.menu.host, vertical=True)
-            return self.addItem(html, True, sub_menu, name=path[0])
-        return None
-
-    def getCategories(self, target_path=None):
-        """Return all the categories of the current node, or those of the
-        sub-category which is specified by target_path.
-
-        @param target_path (list[str]): path to the target node
-        @return: list[MenuNode]
+        @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem
+        @param caller: menu caller
         """
-        assert(self.menu)  # this method applies to category nodes
-        if target_path:
-            assert(isinstance(target_path, list))
-            cat = self._getOrCreateCategory(target_path[:-1])
-            return cat.getCategories(target_path[-1:]) if cat else None
-        return [child for child in self.children if child.menu]
-
-    def addMenuItem(self, path, path_i18n, types, callback=None, asHTML=False):
-        """Recursively add a new node, which could be a category or a leaf node.
-
-        @param path (list[str], str): path to the item
-        @param path_i18n (list[str], str): internationalized path to the item
-        @param types (list[str], str): types of the item and its parents
-        @param callback (MenuCmd, PluginMenuCmd or GenericMenuBar): instance to
-        execute as a leaf's callback or to popup as a category sub-menu
-        @param asHTML (boolean): True to display the UI item as HTML
-        """
-        log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, types, callback))
+        self.item = menu_item
+        self._caller = caller
 
-        leaf_node = hasattr(callback, "execute")
-        category = isinstance(callback, GenericMenuBar)
-        assert(not leaf_node or not category)
-
-        path = [path] if isinstance(path, str) else path
-        path_i18n = [path_i18n] if isinstance(path_i18n, str) else path_i18n
-        types = [types for dummy in range(len(path_i18n))] if isinstance(types, str) else types
-
-        if category:
-            return self._getOrCreateCategory(path, path_i18n, types, True, callback)
-
-        if len(path) == len(path_i18n) - 1:
-            path.append(None)  # dummy name for a leaf node
-
-        parent = self._getOrCreateCategory(path[:-1], path_i18n[:-1], types[:-1], True)
-        return parent.addItem(path_i18n[-1], asHTML=asHTML, popup=callback)
+    def execute(self):
+        self.item.call(self._caller)
 
-    def addCategory(self, path, path_i18n, types, menu_bar=None):
-        """Recursively add a new category.
 
-        @param path (list[str], str): path to the category
-        @param path_i18n (list[str], str): internationalized path to the category
-        @param types (list[str], str): types of the category and its parents
-        @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu.
-        """
-        if menu_bar:
-            assert(isinstance(menu_bar, GenericMenuBar))
-        else:
-            menu_bar = GenericMenuBar(self.menu.host, vertical=True)
-        return self.addMenuItem(path, path_i18n, types, menu_bar)
-
-    def addItem(self, item, asHTML=None, popup=None, name=None):
-        """Add a single child to the current node.
+class SimpleCmd(object):
+    """Return an object with an "executre" method that launch a callback"""
 
-        @param item: see MenuBar.addItem
-        @param asHTML: see MenuBar.addItem
-        @param popup: see MenuBar.addItem
-        @param name (str): the item node's name
+    def __init__(self, callback):
         """
-        if item is None:  # empty string is allowed to set a separator
-            return None
-        item = MenuBar.addItem(self.menu, item, asHTML, popup)
-        node_menu = item.getSubMenu()  # node eventually uses it's own menu
-
-        # XXX: all the dealing with flattened menus is done here
-        if self.flat_level > 0:
-            item.setSubMenu(None)  # eventually remove any sub-menu callback
-            if item.getCommand():
-                node_menu = None  # node isn't a category, it needs no menu
-            else:
-                node_menu = self.menu  # node uses the menu of its parent
-                item.setStyleName(self.menu.styles["flattened-category"])
+        @param callback: method to call when menu is selected
+        """
+        self.callback = callback
 
-        node = MenuNode(name=name, item=item, menu=node_menu, flat_level=self.flat_level - 1)
-        self.children.append(node)
-        return node
-
-    def addCachedMenus(self, type_, menu_data=None):
-        """Add cached menus to instance.
-
-        @param type_: menu type like in sat.core.sat_main.importMenu
-        @param menu_data: data to send with these menus
-        """
-        menus = self.menu.host.menus.get(type_, [])
-        for action_id, path, path_i18n in menus:
-            if len(path) != len(path_i18n):
-                log.error("inconsistency between menu paths")
-                continue
-            if isinstance(action_id, str):
-                callback = PluginMenuCmd(self.menu.host, action_id, menu_data)
-            elif callable(action_id):
-                callback = MenuCmd(action_id, data=menu_data)
-            else:
-                raise exceptions.InternalError
-            self.addMenuItem(path, path_i18n, 'plugins', callback)
+    def execute(self):
+        self.callback()
 
 
 class GenericMenuBar(MenuBar):
@@ -236,45 +80,63 @@
         """
         MenuBar.__init__(self, vertical, **kwargs)
         self.host = host
-        self.styles = {'separator': 'menuSeparator', 'flattened-category': 'menuFlattenedCategory'}
+        self.styles = {}
         if styles:
             self.styles.update(styles)
-        if 'menu_bar' in self.styles:
+        try:
             self.setStyleName(self.styles['menu_bar'])
-        self.node = MenuNode(menu=self, flat_level=flat_level)
+        except KeyError:
+            pass
+        self.menus_container = None
+        self.flat_level = flat_level
+
+    def update(self, type_, caller=None):
+        """Method to call when menus have changed
+
+        @param type_: menu type like in sat.core.sat_main.importMenu
+        @param caller: instance linked to the menus
+        """
+        self.menus_container = self.host.menus.getMainContainer(type_)
+        self._caller=caller
+        self.createMenus()
 
     @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
+    def getCategoryHTML(cls, category):
         """Build the html to be used for displaying a category item.
 
         Inheriting classes may overwrite this method.
-        @param menu_name_i18n (str): internationalized category name
-        @param type_ (str): category type
-        @return: str
+        @param category(quick_menus.MenuCategory): category to add
+        @return(unicode): HTML to display
         """
-        return menu_name_i18n
+        return html_tools.html_sanitize(category.name)
+
+    def _buildMenus(self, container, flat_level, caller=None):
+        """Recursively build menus of the container
 
-    def setStyleName(self, style):
-        # XXX: pyjamas set the style to object string representation!
-        # FIXME: fix the bug upstream
-        menu_style = ['gwt-MenuBar']
-        menu_style.append(menu_style[0] + '-' + ('vertical' if self.vertical else 'horizontal'))
-        for classname in style.split(' '):
-            if classname not in menu_style:
-                menu_style.append(classname)
-        UIObject.setStyleName(self, ' '.join(menu_style))
+        @param container: a quick_menus.MenuContainer instance
+        @param caller: instance linked to the menus
+        """
+        for child in container.getActiveMenus():
+            if isinstance(child, quick_menus.MenuContainer):
+                item = self.addCategory(child, flat=bool(flat_level))
+                submenu = item.getSubMenu()
+                if submenu is None:
+                    submenu = self
+                submenu._buildMenus(child, flat_level-1 if flat_level else 0, caller)
+            elif isinstance(child, quick_menus.MenuSeparator):
+                item = MenuItem(text='', asHTML=None, StyleName="menuSeparator")
+                self.addItem(item)
+            elif isinstance(child, quick_menus.MenuItem):
+                self.addItem(child.name, False, MenuCmd(child, caller) if child.CALLABLE else None)
+            else:
+                log.error(u"Unknown child type: {}".format(child))
 
-    def addStyleName(self, style):
-        # XXX: same kind of problem then with setStyleName
-        # FIXME: fix the bug upstream
-        if not re.search('(^| )%s( |$)' % style, self.getStyleName()):
-            UIObject.setStyleName(self, self.getStyleName() + ' ' + style)
-
-    def removeStyleName(self, style):
-        # XXX: same kind of problem then with setStyleName
-        # FIXME: fix the bug upstream
-        style = re.sub('(^| )%s( |$)' % style, ' ', self.getStyleName()).strip()
-        UIObject.setStyleName(self, style)
+    def createMenus(self):
+        self.clearItems()
+        if self.menus_container is None:
+            log.debug("Menu is empty")
+            return
+        self._buildMenus(self.menus_container, self.flat_level, self._caller)
 
     def doItemAction(self, item, fireCommand):
         """Overwrites the default behavior for the popup menu to fit in the screen"""
@@ -294,29 +156,28 @@
         if item.getAbsoluteLeft() > max_left:
             self.popup.setPopupPosition(new_left, top)
             # eventually smooth the popup edges to fit the menu own style
-            if 'moved_popup' in self.styles:
+            try:
                 self.popup.addStyleName(self.styles['moved_popup'])
-
-    def getCategories(self, parent_path=None):
-        """Return all the categories items.
+            except KeyError:
+                pass
 
-        @return: list[CategoryItem]
+    def addCategory(self, category, menu_bar=None, flat=False):
+        """Add a new category.
+
+        @param menu_container(quick_menus.MenuCategory): Category to add
+        @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu.
         """
-        return [cat.item for cat in self.node.getCategories(parent_path)]
-
-    def addMenuItem(self, path, path_i18n, types, menu_cmd, asHTML=False):
-        return self.node.addMenuItem(path, path_i18n, types, menu_cmd, asHTML).item
+        html = self.getCategoryHTML(category)
 
-    def addCategory(self, path, path_i18n, types, menu_bar):
-        return self.node.addCategory(path, path_i18n, types, menu_bar).item
-
-    def addItem(self, item, asHTML=None, popup=None):
-        return self.node.addItem(item, asHTML, popup).item
+        if menu_bar is not None:
+            assert not flat # can't have a menu_bar and be flat at the same time
+            sub_menu = menu_bar
+        elif not flat:
+            sub_menu = GenericMenuBar(self.host, vertical=True)
+        else:
+            sub_menu = None
 
-    def addCachedMenus(self, type_, menu_data=None):
-        self.node.addCachedMenus(type_, menu_data)
-
-    def addSeparator(self):
-        """Add a separator between the categories"""
-        item = MenuItem(text='', asHTML=None, StyleName=self.styles['separator'])
-        return self.addItem(item)
+        item = self.addItem(html, True, sub_menu)
+        if flat:
+            item.setStyleName("menuFlattenedCategory")
+        return item
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/base_panel.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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 pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.StackPanel import StackPanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
+from pyjamas import DOM
+
+
+### Menus ###
+
+
+class PopupMenuPanel(PopupPanel):
+    """This implementation of a popup menu (context menu) allow you to assign
+    two special methods which are common to all the items, in order to hide
+    certain items and also easily define their callbacks. The menu can be
+    bound to any of the mouse button (left, middle, right).
+    """
+    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
+        """
+        @param entries: a dict of dicts, where each sub-dict is representing
+        one menu item: the sub-dict key can be used as the item text and
+        description, but optional "title" and "desc" entries would be used
+        if they exists. The sub-dicts may be extended later to do
+        more complicated stuff or overwrite the common methods.
+        @param hide: function  with 2 args: widget, key as string and
+        returns True if that item should be hidden from the context menu.
+        @param callback: function with 2 args: sender, key as string
+        @param vertical: True or False, to set the direction
+        @param item_style: alternative CSS class for the menu items
+        @param menu_style: supplementary CSS class for the sender widget
+        """
+        PopupPanel.__init__(self, autoHide=True, **kwargs)
+        self._entries = entries
+        self._hide = hide
+        self._callback = callback
+        self.vertical = vertical
+        self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._senders = {}
+
+    def _show(self, sender):
+        """Popup the menu relative to this sender's position.
+        @param sender: the widget that has been clicked
+        """
+        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
+        menu.setStyleName(self.style["menu"])
+
+        def button_cb(item):
+            """You can not put that method in the loop and rely
+            on _key, because it is overwritten by each step.
+            You can rely on item.key instead, which is copied
+            from _key after the item creation.
+            @param item: the menu item that has been clicked
+            """
+            if self._callback is not None:
+                self._callback(sender=sender, key=item.key)
+            self.hide(autoClosed=True)
+
+        for _key in self._entries.keys():
+            entry = self._entries[_key]
+            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
+                continue
+            title = entry["title"] if "title" in entry.keys() else _key
+            item = Button(title, button_cb)
+            item.key = _key
+            item.setStyleName(self.style["item"])
+            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
+            menu.add(item)
+        if len(menu.getChildren()) == 0:
+            return
+        self.add(menu)
+        if self.vertical is True:
+            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
+            y = sender.getAbsoluteTop()
+        else:
+            x = sender.getAbsoluteLeft()
+            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
+        self.setPopupPosition(x, y)
+        self.show()
+        if self.style["selected"]:
+            sender.addStyleDependentName(self.style["selected"])
+
+        def _onHide(popup):
+            if self.style["selected"]:
+                sender.removeStyleDependentName(self.style["selected"])
+            return PopupPanel.onHideImpl(self, popup)
+
+        self.onHideImpl = _onHide
+
+    def registerClickSender(self, sender, button=BUTTON_LEFT):
+        """Bind the menu to the specified sender.
+        @param sender: the widget to which the menu should be bound
+        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
+        """
+        self._senders.setdefault(sender, [])
+        self._senders[sender].append(button)
+
+        if button == BUTTON_RIGHT:
+            # WARNING: to disable the context menu is a bit tricky...
+            # The following seems to work on Firefox 24.0, but:
+            # TODO: find a cleaner way to disable the context menu
+            sender.getElement().setAttribute("oncontextmenu", "return false")
+
+        def _onBrowserEvent(event):
+            button = DOM.eventGetButton(event)
+            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
+                self._show(sender)
+            return sender.__class__.onBrowserEvent(sender, event)
+
+        sender.onBrowserEvent = _onBrowserEvent
+
+    def registerMiddleClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_MIDDLE)
+
+    def registerRightClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_RIGHT)
+
+
+### Generic panels ###
+
+
+class ToggleStackPanel(StackPanel):
+    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
+    visible at the same time, clicking a sub-panel header will not display it and hide
+    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
+    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
+    """
+
+    def __init__(self, **kwargs):
+        StackPanel.__init__(self, **kwargs)
+
+    def onBrowserEvent(self, event):
+        if DOM.eventGetType(event) == "click":
+            index = self.getDividerIndex(DOM.eventGetTarget(event))
+            if index != -1:
+                self.toggleStack(index)
+
+    def add(self, widget, stackText="", asHTML=False, visible=False):
+        StackPanel.add(self, widget, stackText, asHTML)
+        self.setStackVisible(self.getWidgetCount() - 1, visible)
+
+    def toggleStack(self, index):
+        if index >= self.getWidgetCount():
+            return
+        visible = not self.getWidget(index).getVisible()
+        self.setStackVisible(index, visible)
+        for listener in self.stackListeners:
+            listener.onStackChanged(self, index, visible)
+
+
+class TitlePanel(ToggleStackPanel):
+    """A toggle panel to set the message title"""
+    def __init__(self):
+        ToggleStackPanel.__init__(self, Width="100%")
+        self.text_area = TextArea()
+        self.add(self.text_area, _("Title"))
+        self.addStackChangeListener(self)
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        text = self.text_area.getText()
+        suffix = "" if (visible or not text) else (": %s" % text)
+        sender.setStackText(index, _("Title") + suffix)
+
+    def getText(self):
+        return self.text_area.getText()
+
+    def setText(self, text):
+        self.text_area.setText(text)
+
+
+class ScrollPanelWrapper(SimplePanel):
+    """Scroll Panel like component, wich use the full available space
+    to work around percent size issue, it use some of the ideas found
+    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
+    specially in code given at comment #46, thanks to Stefan Bachert"""
+
+    def __init__(self, *args, **kwargs):
+        SimplePanel.__init__(self)
+        self.spanel = ScrollPanel(*args, **kwargs)
+        SimplePanel.setWidget(self, self.spanel)
+        DOM.setStyleAttribute(self.getElement(), "position", "relative")
+        DOM.setStyleAttribute(self.getElement(), "top", "0px")
+        DOM.setStyleAttribute(self.getElement(), "left", "0px")
+        DOM.setStyleAttribute(self.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.getElement(), "height", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
+        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
+
+    def setWidget(self, widget):
+        self.spanel.setWidget(widget)
+
+    def setScrollPosition(self, position):
+        self.spanel.setScrollPosition(position)
+
+    def scrollToBottom(self):
+        self.setScrollPosition(self.spanel.getElement().scrollHeight)
--- a/src/browser/sat_browser/base_panels.py	Thu Feb 05 12:05:32 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,621 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 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/>.
-
-import pyjd  # this is dummy in pyjs
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat.core.i18n import _
-from sat_frontends.tools import strings
-
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.HTMLPanel import HTMLPanel
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.PopupPanel import PopupPanel
-from pyjamas.ui.StackPanel import StackPanel
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
-from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas import DOM
-
-from datetime import datetime
-from time import time
-
-import html_tools
-from constants import Const as C
-
-
-class ChatText(HTMLPanel):
-
-    def __init__(self, timestamp, nick, mymess, msg, xhtml=None):
-        _date = datetime.fromtimestamp(float(timestamp or time()))
-        _msg_class = ["chat_text_msg"]
-        if mymess:
-            _msg_class.append("chat_text_mymess")
-        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
-                           {"timestamp": _date.strftime("%H:%M"),
-                            "nick": "[%s]" % html_tools.html_sanitize(nick),
-                            "msg_class": ' '.join(_msg_class),
-                            "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
-                           )
-        self.setStyleName('chatText')
-
-
-class Occupant(HTML):
-    """Occupant of a MUC room"""
-
-    def __init__(self, nick, state=None, special=""):
-        """
-        @param nick: the user nickname
-        @param state: the user chate state (XEP-0085)
-        @param special: a string of symbols (e.g: for activities)
-        """
-        HTML.__init__(self, StyleName="occupant")
-        self.nick = nick
-        self._state = state
-        self.special = special
-        self._refresh()
-
-    def __str__(self):
-        return self.nick
-
-    def setState(self, state):
-        self._state = state
-        self._refresh()
-
-    def addSpecial(self, special):
-        """@param special: unicode"""
-        if special not in self.special:
-            self.special += special
-            self._refresh()
-
-    def removeSpecials(self, special):
-        """@param special: unicode or list"""
-        if not isinstance(special, list):
-            special = [special]
-        for symbol in special:
-            self.special = self.special.replace(symbol, "")
-            self._refresh()
-
-    def _refresh(self):
-        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
-        special = "" if len(self.special) == 0 else " %s" % self.special
-        self.setHTML("%s%s%s" % (html_tools.html_sanitize(self.nick), special, state))
-
-
-class OccupantsList(AbsolutePanel):
-    """Panel user to show occupants of a room"""
-
-    def __init__(self):
-        AbsolutePanel.__init__(self)
-        self.occupants_list = {}
-        self.setStyleName('occupantsList')
-
-    def addOccupant(self, nick):
-        if nick in self.occupants_list:
-            return
-        _occupant = Occupant(nick)
-        self.occupants_list[nick] = _occupant
-        self.add(_occupant)
-
-    def removeOccupant(self, nick):
-        try:
-            self.remove(self.occupants_list[nick])
-        except KeyError:
-            log.error("trying to remove an unexisting nick")
-
-    def getOccupantBox(self, nick):
-        """Get the widget element of the given nick.
-
-        @return: Occupant
-        """
-        try:
-            return self.occupants_list[nick]
-        except KeyError:
-            return None
-
-    def clear(self):
-        self.occupants_list.clear()
-        AbsolutePanel.clear(self)
-
-    def updateSpecials(self, occupants=[], html=""):
-        """Set the specified html "symbol" to the listed occupants,
-        and eventually remove it from the others (if they got it).
-        This is used for example to visualize who is playing a game.
-        @param occupants: list of the occupants that need the symbol
-        @param html: unicode symbol (actually one character or more)
-        or a list to assign different symbols of the same family.
-        """
-        index = 0
-        special = html
-        for occupant in self.occupants_list.keys():
-            if occupant in occupants:
-                if isinstance(html, list):
-                    special = html[index]
-                    index = (index + 1) % len(html)
-                self.occupants_list[occupant].addSpecial(special)
-            else:
-                self.occupants_list[occupant].removeSpecials(html)
-
-
-class PopupMenuPanel(PopupPanel):
-    """This implementation of a popup menu (context menu) allow you to assign
-    two special methods which are common to all the items, in order to hide
-    certain items and also easily define their callbacks. The menu can be
-    bound to any of the mouse button (left, middle, right).
-    """
-    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
-        """
-        @param entries: a dict of dicts, where each sub-dict is representing
-        one menu item: the sub-dict key can be used as the item text and
-        description, but optional "title" and "desc" entries would be used
-        if they exists. The sub-dicts may be extended later to do
-        more complicated stuff or overwrite the common methods.
-        @param hide: function  with 2 args: widget, key as string and
-        returns True if that item should be hidden from the context menu.
-        @param callback: function with 2 args: sender, key as string
-        @param vertical: True or False, to set the direction
-        @param item_style: alternative CSS class for the menu items
-        @param menu_style: supplementary CSS class for the sender widget
-        """
-        PopupPanel.__init__(self, autoHide=True, **kwargs)
-        self._entries = entries
-        self._hide = hide
-        self._callback = callback
-        self.vertical = vertical
-        self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
-        if isinstance(style, dict):
-            self.style.update(style)
-        self._senders = {}
-
-    def _show(self, sender):
-        """Popup the menu relative to this sender's position.
-        @param sender: the widget that has been clicked
-        """
-        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
-        menu.setStyleName(self.style["menu"])
-
-        def button_cb(item):
-            """You can not put that method in the loop and rely
-            on _key, because it is overwritten by each step.
-            You can rely on item.key instead, which is copied
-            from _key after the item creation.
-            @param item: the menu item that has been clicked
-            """
-            if self._callback is not None:
-                self._callback(sender=sender, key=item.key)
-            self.hide(autoClosed=True)
-
-        for _key in self._entries.keys():
-            entry = self._entries[_key]
-            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
-                continue
-            title = entry["title"] if "title" in entry.keys() else _key
-            item = Button(title, button_cb)
-            item.key = _key
-            item.setStyleName(self.style["item"])
-            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
-            menu.add(item)
-        if len(menu.getChildren()) == 0:
-            return
-        self.add(menu)
-        if self.vertical is True:
-            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
-            y = sender.getAbsoluteTop()
-        else:
-            x = sender.getAbsoluteLeft()
-            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
-        self.setPopupPosition(x, y)
-        self.show()
-        if self.style["selected"]:
-            sender.addStyleDependentName(self.style["selected"])
-
-        def _onHide(popup):
-            if self.style["selected"]:
-                sender.removeStyleDependentName(self.style["selected"])
-            return PopupPanel.onHideImpl(self, popup)
-
-        self.onHideImpl = _onHide
-
-    def registerClickSender(self, sender, button=BUTTON_LEFT):
-        """Bind the menu to the specified sender.
-        @param sender: the widget to which the menu should be bound
-        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
-        """
-        self._senders.setdefault(sender, [])
-        self._senders[sender].append(button)
-
-        if button == BUTTON_RIGHT:
-            # WARNING: to disable the context menu is a bit tricky...
-            # The following seems to work on Firefox 24.0, but:
-            # TODO: find a cleaner way to disable the context menu
-            sender.getElement().setAttribute("oncontextmenu", "return false")
-
-        def _onBrowserEvent(event):
-            button = DOM.eventGetButton(event)
-            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
-                self._show(sender)
-            return sender.__class__.onBrowserEvent(sender, event)
-
-        sender.onBrowserEvent = _onBrowserEvent
-
-    def registerMiddleClickSender(self, sender):
-        self.registerClickSender(sender, BUTTON_MIDDLE)
-
-    def registerRightClickSender(self, sender):
-        self.registerClickSender(sender, BUTTON_RIGHT)
-
-
-class ToggleStackPanel(StackPanel):
-    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
-    visible at the same time, clicking a sub-panel header will not display it and hide
-    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
-    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
-    """
-
-    def __init__(self, **kwargs):
-        StackPanel.__init__(self, **kwargs)
-
-    def onBrowserEvent(self, event):
-        if DOM.eventGetType(event) == "click":
-            index = self.getDividerIndex(DOM.eventGetTarget(event))
-            if index != -1:
-                self.toggleStack(index)
-
-    def add(self, widget, stackText="", asHTML=False, visible=False):
-        StackPanel.add(self, widget, stackText, asHTML)
-        self.setStackVisible(self.getWidgetCount() - 1, visible)
-
-    def toggleStack(self, index):
-        if index >= self.getWidgetCount():
-            return
-        visible = not self.getWidget(index).getVisible()
-        self.setStackVisible(index, visible)
-        for listener in self.stackListeners:
-            listener.onStackChanged(self, index, visible)
-
-
-class TitlePanel(ToggleStackPanel):
-    """A toggle panel to set the message title"""
-    def __init__(self):
-        ToggleStackPanel.__init__(self, Width="100%")
-        self.text_area = TextArea()
-        self.add(self.text_area, _("Title"))
-        self.addStackChangeListener(self)
-
-    def onStackChanged(self, sender, index, visible=None):
-        if visible is None:
-            visible = sender.getWidget(index).getVisible()
-        text = self.text_area.getText()
-        suffix = "" if (visible or not text) else (": %s" % text)
-        sender.setStackText(index, _("Title") + suffix)
-
-    def getText(self):
-        return self.text_area.getText()
-
-    def setText(self, text):
-        self.text_area.setText(text)
-
-
-class BaseTextEditor(object):
-    """Basic definition of a text editor. The method edit gets a boolean parameter which
-    should be set to True when you want to edit the text and False to only display it."""
-
-    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
-        """
-        Remark when inheriting this class: since the setContent method could be
-        overwritten by the child class, you should consider calling this __init__
-        after all the parameters affecting this setContent method have been set.
-        @param content: dict with at least a 'text' key
-        @param strproc: method to be applied on strings to clean the content
-        @param modifiedCb: method to be called when the text has been modified.
-        If this method returns:
-        - True: the modification will be saved and afterEditCb called;
-        - False: the modification won't be saved and afterEditCb called;
-        - None: the modification won't be saved and afterEditCb not called.
-        @param afterEditCb: method to be called when the edition is done
-        """
-        if content is None:
-            content = {'text': ''}
-        assert('text' in content)
-        if strproc is None:
-            def strproc(text):
-                try:
-                    return text.strip()
-                except (TypeError, AttributeError):
-                    return text
-        self.strproc = strproc
-        self.__modifiedCb = modifiedCb
-        self._afterEditCb = afterEditCb
-        self.initialized = False
-        self.edit_listeners = []
-        self.setContent(content)
-
-    def setContent(self, content=None):
-        """Set the editable content. The displayed content, which is set from the child class, could differ.
-        @param content: dict with at least a 'text' key
-        """
-        if content is None:
-            content = {'text': ''}
-        elif not isinstance(content, dict):
-            content = {'text': content}
-        assert('text' in content)
-        self._original_content = {}
-        for key in content:
-            self._original_content[key] = self.strproc(content[key])
-
-    def getContent(self):
-        """Get the current edited or editable content.
-        @return: dict with at least a 'text' key
-        """
-        raise NotImplementedError
-
-    def setOriginalContent(self, content):
-        """Use this method with care! Content initialization should normally be
-        done with self.setContent. This method exists to let you trick the editor,
-        e.g. for self.modified to return True also when nothing has been modified.
-        @param content: dict
-        """
-        self._original_content = content
-
-    def getOriginalContent(self):
-        """
-        @return the original content before modification (dict)
-        """
-        return self._original_content
-
-    def modified(self, content=None):
-        """Check if the content has been modified.
-        Remark: we don't use the direct comparison because we want to ignore empty elements
-        @content: content to be check against the original content or None to use the current content
-        @return: True if the content has been modified.
-        """
-        if content is None:
-            content = self.getContent()
-        # the following method returns True if one non empty element exists in a but not in b
-        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
-        # the following method returns True if the values for the common keys are not equals
-        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
-        # finally the combination of both to return True if a difference is found
-        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
-
-        return diff(content, self._original_content)
-
-    def edit(self, edit, abort=False, sync=False):
-        """
-        Remark: the editor must be visible before you call this method.
-        @param edit: set to True to edit the content or False to only display it
-        @param abort: set to True to cancel the edition and loose the changes.
-        If edit and abort are both True, self.abortEdition can be used to ask for a
-        confirmation. When edit is False and abort is True, abortion is actually done.
-        @param sync: set to True to cancel the edition after the content has been saved somewhere else
-        """
-        if edit:
-            if not self.initialized:
-                self.syncToEditor()  # e.g.: use the selected target and unibox content
-            self.setFocus(True)
-            if abort:
-                content = self.getContent()
-                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
-                    self.edit(False, True, sync)
-                    return
-            if sync:
-                self.syncFromEditor(content)  # e.g.: save the content to unibox
-                return
-        else:
-            if not self.initialized:
-                return
-            content = self.getContent()
-            if abort:
-                self._afterEditCb(content)
-                return
-            if self.__modifiedCb and self.modified(content):
-                result = self.__modifiedCb(content)  # e.g.: send a message or update something
-                if result is not None:
-                    if self._afterEditCb:
-                        self._afterEditCb(content)  # e.g.: restore the display mode
-                    if result is True:
-                        self.setContent(content)
-            elif self._afterEditCb:
-                self._afterEditCb(content)
-
-        self.initialized = True
-
-    def setFocus(self, focus):
-        """
-        @param focus: set to True to focus the editor
-        """
-        raise NotImplementedError
-
-    def syncToEditor(self):
-        pass
-
-    def syncFromEditor(self, content):
-        pass
-
-    def abortEdition(self, content):
-        return True
-
-    def addEditListener(self, listener):
-        """Add a method to be called whenever the text is edited.
-        @param listener: method taking two arguments: sender, keycode"""
-        self.edit_listeners.append(listener)
-
-
-class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
-    """Base class for manage a simple text editor."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        """
-        @param content
-        @param modifiedCb
-        @param afterEditCb
-        @param options: dict with the following value:
-        - no_xhtml: set to True to clean any xhtml content.
-        - enhance_display: if True, the display text will be enhanced with strings.addURLToText
-        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
-        - listen_focus: set to True to terminate the edition when the focus is lost.
-        - listen_click: set to True to start the edition when you click on the widget.
-        """
-        self.options = {'no_xhtml': False,
-                        'enhance_display': True,
-                        'listen_keyboard': True,
-                        'listen_focus': False,
-                        'listen_click': False
-                        }
-        if options:
-            self.options.update(options)
-        self.__shift_down = False
-        if self.options['listen_focus']:
-            FocusHandler.__init__(self)
-        if self.options['listen_click']:
-            ClickHandler.__init__(self)
-        KeyboardHandler.__init__(self)
-        strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
-        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
-        self.textarea = self.display = None
-
-    def setContent(self, content=None):
-        BaseTextEditor.setContent(self, content)
-
-    def getContent(self):
-        raise NotImplementedError
-
-    def edit(self, edit, abort=False, sync=False):
-        BaseTextEditor.edit(self, edit)
-        if edit:
-            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
-                self.textarea.addFocusListener(self)
-            if self.options['listen_click']:
-                self.display.clearClickListener()
-            if self not in self.textarea._keyboardListeners:
-                self.textarea.addKeyboardListener(self)
-        else:
-            self.setDisplayContent()
-            if self.options['listen_focus']:
-                try:
-                    self.textarea.removeFocusListener(self)
-                except ValueError:
-                    pass
-            if self.options['listen_click'] and self not in self.display._clickListeners:
-                self.display.addClickListener(self)
-            try:
-                self.textarea.removeKeyboardListener(self)
-            except ValueError:
-                pass
-
-    def setDisplayContent(self):
-        text = self._original_content['text']
-        if not self.options['no_xhtml']:
-            text = strings.addURLToImage(text)
-        if self.options['enhance_display']:
-            text = strings.addURLToText(text)
-        self.display.setHTML(html_tools.convertNewLinesToXHTML(text))
-
-    def setFocus(self, focus):
-        raise NotImplementedError
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        for listener in self.edit_listeners:
-            listener(self.textarea, keycode)
-        if not self.options['listen_keyboard']:
-            return
-        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
-            self.__shift_down = True
-            return
-        if keycode == KEY_ENTER:  # finish the edition
-            self.textarea.setFocus(False)
-            if not self.options['listen_focus']:
-                self.edit(False)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        if keycode == KEY_SHIFT:
-            self.__shift_down = False
-
-    def onLostFocus(self, sender):
-        """Finish the edition when focus is lost"""
-        if self.options['listen_focus']:
-            self.edit(False)
-
-    def onClick(self, sender=None):
-        """Start the edition when the widget is clicked"""
-        if self.options['listen_click']:
-            self.edit(True)
-
-    def onBrowserEvent(self, event):
-        if self.options['listen_focus']:
-            FocusHandler.onBrowserEvent(self, event)
-        if self.options['listen_click']:
-            ClickHandler.onBrowserEvent(self, event)
-        KeyboardHandler.onBrowserEvent(self, event)
-
-
-class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
-    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        HTML.__init__(self)
-        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
-        self.textarea = self.display = self
-
-    def getContent(self):
-        text = DOM.getInnerHTML(self.getElement())
-        return {'text': self.strproc(text) if text else ''}
-
-    def edit(self, edit, abort=False, sync=False):
-        if edit:
-            self.textarea.setHTML(self._original_content['text'])
-        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
-        SimpleTextEditor.edit(self, edit, abort, sync)
-
-    def setFocus(self, focus):
-        if focus:
-            self.getElement().focus()
-        else:
-            self.getElement().blur()
-
-
-class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
-    """Manage a simple text editor with a TextArea for editing, HTML for display."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        SimplePanel.__init__(self)
-        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
-        self.textarea = TextArea()
-        self.display = HTML()
-
-    def getContent(self):
-        text = self.textarea.getText()
-        return {'text': self.strproc(text) if text else ''}
-
-    def edit(self, edit, abort=False, sync=False):
-        if edit:
-            self.textarea.setText(self._original_content['text'])
-        self.setWidget(self.textarea if edit else self.display)
-        SimpleTextEditor.edit(self, edit, abort, sync)
-
-    def setFocus(self, focus):
-        if focus and self.isAttached():
-            self.textarea.setCursorPos(len(self.textarea.getText()))
-        self.textarea.setFocus(focus)
--- a/src/browser/sat_browser/base_widget.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/base_widget.py	Wed Mar 18 16:15:18 2015 +0100
@@ -17,154 +17,21 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import pyjd  # this is dummy in pyjs
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.ScrollPanel import ScrollPanel
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.HTMLPanel import HTMLPanel
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Widget import Widget
-from pyjamas.ui.DragWidget import DragWidget
-from pyjamas.ui.DropWidget import DropWidget
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui import HasAlignment
-from pyjamas import DOM
-from pyjamas import Window
-
-from __pyjamas__ import doc
-
-import dialog
 import base_menu
-
-
-class DragLabel(DragWidget):
-
-    def __init__(self, text, _type):
-        DragWidget.__init__(self)
-        self._text = text
-        self._type = _type
-
-    def onDragStart(self, event):
-        dt = event.dataTransfer
-        dt.setData('text/plain', "%s\n%s" % (self._text, self._type))
-        dt.setDragImage(self.getElement(), 15, 15)
-
-
-class LiberviaDragWidget(DragLabel):
-    """ A DragLabel which keep the widget being dragged as class value """
-    current = None  # widget currently dragged
-
-    def __init__(self, text, _type, widget):
-        DragLabel.__init__(self, text, _type)
-        self.widget = widget
-
-    def onDragStart(self, event):
-        LiberviaDragWidget.current = self.widget
-        DragLabel.onDragStart(self, event)
-
-    def onDragEnd(self, event):
-        LiberviaDragWidget.current = None
+from sat_frontends.quick_frontend import quick_menus
 
 
-class DropCell(DropWidget):
-    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
-    drop_keys = {}
-
-    def __init__(self, host):
-        DropWidget.__init__(self)
-        self.host = host
-        self.setStyleName('dropCell')
-
-    @classmethod
-    def addDropKey(cls, key, callback):
-        DropCell.drop_keys[key] = callback
+### Exceptions ###
 
-    def onDragEnter(self, event):
-        if self == LiberviaDragWidget.current:
-            return
-        self.addStyleName('dragover')
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
-            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
-            # We check that we are inside widget's box, and we don't remove the style in this case because
-            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
-            # don't want that
-            self.removeStyleName('dragover')
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def _getCellAndRow(self, grid, event):
-        """Return cell and row index where the event is occuring"""
-        cell = grid.getEventTargetCell(event)
-        row = DOM.getParent(cell)
-        return (row.rowIndex, cell.cellIndex)
 
-    def onDrop(self, event):
-        self.removeStyleName('dragover')
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        try:
-            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-            # item_type = dt.getData("type")
-            log.debug("message: %s" % item)
-            log.debug("type: %s" % item_type)
-        except:
-            log.debug("no message found")
-            item = '&nbsp;'
-            item_type = None
-        if item_type == "WIDGET":
-            if not LiberviaDragWidget.current:
-                log.error("No widget registered in LiberviaDragWidget !")
-                return
-            _new_panel = LiberviaDragWidget.current
-            if self == _new_panel:  # We can't drop on ourself
-                return
-            # we need to remove the widget from the panel as it will be inserted elsewhere
-            widgets_panel = _new_panel.getWidgetsPanel()
-            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
-            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
-            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
-                # the dropped widget is the only one in the same row
-                # as the target widget (self), we don't do anything
-                return
-            widgets_panel.removeWidget(_new_panel)
-        elif item_type in self.drop_keys:
-            _new_panel = self.drop_keys[item_type](self.host, item)
-        else:
-            log.warning("unmanaged item type")
-            return
-        if isinstance(self, LiberviaWidget):
-            self.host.unregisterWidget(self)
-            self.onQuit()
-            if not isinstance(_new_panel, LiberviaWidget):
-                log.warning("droping an object which is not a class of LiberviaWidget")
-        _flextable = self.getParent()
-        _widgetspanel = _flextable.getParent().getParent()
-        row_idx, cell_idx = self._getCellAndRow(_flextable, event)
-        if self.host.getSelected == self:
-            self.host.setSelected(None)
-        _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
-        """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
-        _width = 90/float(len(_unempty_panels) or 1)
-        #now we resize all the cell of the column
-        for panel in _unempty_panels:
-            td_elt = panel.getElement().parentNode
-            DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
-        #FIXME: delete object ? Check the right way with pyjamas
+class NoLiberviaWidgetException(Exception):
+    """A Libervia widget was expected"""
+    pass
+
+
+### Menus ###
 
 
 class WidgetMenuBar(base_menu.GenericMenuBar):
@@ -172,629 +39,28 @@
     ITEM_TPL = "<img src='media/icons/misc/%s.png' />"
 
     def __init__(self, parent, host, vertical=False, styles=None):
+        """
+
+        @param parent (Widget): LiberviaWidget, or instance of another class
+            implementing the method addMenus
+        @param host (SatWebFrontend)
+        @param vertical (bool): if True, set the menu vertically
+        @param styles (dict): optional styles dict
+        """
         menu_styles = {'menu_bar': 'widgetHeader_buttonGroup'}
         if styles:
             menu_styles.update(styles)
         base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles)
 
-        if hasattr(parent, 'addMenus'):
-            # regroup all the dynamic menu categories in a sub-menu
-            sub_menu = WidgetSubMenuBar(host, vertical=True)
-            parent.addMenus(sub_menu)
-            if len(sub_menu.getCategories()) > 0:
-                self.addCategory('', '', 'plugins', sub_menu)
-
-    @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return cls.ITEM_TPL % type_
-
-
-class WidgetSubMenuBar(base_menu.GenericMenuBar):
-
-    def __init__(self, host, vertical=True):
-        base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, flat_level=1)
+        # regroup all the dynamic menu categories in a sub-menu
+        for menu_context in parent.plugin_menu_context:
+            main_cont = host.menus.getMainContainer(menu_context)
+            if len(main_cont)>0: # we don't add the icon if the menu is empty
+                sub_menu = base_menu.GenericMenuBar(host, vertical=True, flat_level=1)
+                sub_menu.update(menu_context, parent)
+                menu_category = quick_menus.MenuCategory("plugins", extra={'icon':'plugins'})
+                self.addCategory(menu_category, sub_menu)
 
     @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return menu_name_i18n
-
-
-class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
-
-    def __init__(self, parent, host, title, info=None):
-        """
-        @param parent (LiberviaWidget): LiberWidget instance
-        @param host (SatWebFrontend): SatWebFrontend instance
-        @param title (Label, HTML): text widget instance
-        @param info (Widget): text widget instance
-        """
-        AbsolutePanel.__init__(self)
-        self.add(title)
-        if info:
-            # FIXME: temporary design to display the info near the menu
-            button_group_wrapper = HorizontalPanel()
-            button_group_wrapper.add(info)
-        else:
-            button_group_wrapper = SimplePanel()
-        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
-        button_group = WidgetMenuBar(parent, host)
-        button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.MenuCmd(parent, 'onSetting'))
-        button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.MenuCmd(parent, 'onClose'))
-        button_group_wrapper.add(button_group)
-        self.add(button_group_wrapper)
-        self.addStyleName('widgetHeader')
-        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
-
-
-class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
-    """Libervia's widget which can replace itself with a dropped widget on DnD"""
-
-    def __init__(self, host, title='', info=None, selectable=False):
-        """Init the widget
-        @param host (SatWebFrontend): SatWebFrontend instance
-        @param title (str): title shown in the header of the widget
-        @param info (str, callable): info shown in the header of the widget
-        @param selectable (bool): True is widget can be selected by user"""
-        VerticalPanel.__init__(self)
-        DropCell.__init__(self, host)
-        ClickHandler.__init__(self)
-        self.__selectable = selectable
-        self.__title_id = HTMLPanel.createUniqueId()
-        self.__setting_button_id = HTMLPanel.createUniqueId()
-        self.__close_button_id = HTMLPanel.createUniqueId()
-        self.__title = Label(title)
-        self.__title.setStyleName('widgetHeader_title')
-        if info is not None:
-            if isinstance(info, str):
-                self.__info = HTML(info)
-            else:  # the info will be set by a callback
-                assert(callable(info))
-                self.__info = HTML()
-                info(self.__info.setHTML)
-            self.__info.setStyleName('widgetHeader_info')
-        else:
-            self.__info = None
-        self._close_listeners = []
-        header = WidgetHeader(self, host, self.__title, self.__info)
-        self.add(header)
-        self.setSize('100%', '100%')
-        self.addStyleName('widget')
-        if self.__selectable:
-            self.addClickListener(self)
-
-            def onClose(sender):
-                """Check dynamically if the unibox is enable or not"""
-                if self.host.uni_box:
-                    self.host.uni_box.onWidgetClosed(sender)
-
-            self.addCloseListener(onClose)
-        self.host.registerWidget(self)
-
-    def getDebugName(self):
-        return "%s (%s)" % (self, self.__title.getText())
-
-    def getWidgetsPanel(self, expect=True):
-        return self.getParent(WidgetsPanel, expect)
-
-    def getParent(self, class_=None, expect=True):
-        """Return the closest ancestor of the specified class.
-
-        Note: this method overrides pyjamas.ui.Widget.getParent
-
-        @param class_: class of the ancestor to look for or None to return the first parent
-        @param expect: set to True if the parent is expected (print a message if not found)
-        @return: the parent/ancestor or None if it has not been found
-        """
-        current = Widget.getParent(self)
-        if class_ is None:
-            return current  # this is the default behavior
-        while current is not None and not isinstance(current, class_):
-            current = Widget.getParent(current)
-        if current is None and expect:
-            log.error("Can't find parent %s for %s" % (class_, self))
-        return current
-
-    def onClick(self, sender):
-        self.host.setSelected(self)
-
-    def onClose(self, sender):
-        """ Called when the close button is pushed """
-        _widgetspanel = self.getWidgetsPanel()
-        _widgetspanel.removeWidget(self)
-        for callback in self._close_listeners:
-            callback(self)
-        self.onQuit()
-
-    def onQuit(self):
-        """ Called when the widget is actually ending """
-        pass
-
-    def addCloseListener(self, callback):
-        """Add a close listener to this widget
-        @param callback: function to be called from self.onClose"""
-        self._close_listeners.append(callback)
-
-    def refresh(self):
-        """This can be overwritten by a child class to refresh the display when,
-        instead of creating a new one, an existing widget is found and reused.
-        """
-        pass
-
-    def onSetting(self, sender):
-        widpanel = self.getWidgetsPanel()
-        row, col = widpanel.getIndex(self)
-        body = VerticalPanel()
-
-        # colspan & rowspan
-        colspan = widpanel.getColSpan(row, col)
-        rowspan = widpanel.getRowSpan(row, col)
-
-        def onColSpanChange(value):
-            widpanel.setColSpan(row, col, value)
-
-        def onRowSpanChange(value):
-            widpanel.setRowSpan(row, col, value)
-        colspan_setter = dialog.IntSetter("Columns span", colspan)
-        colspan_setter.addValueChangeListener(onColSpanChange)
-        colspan_setter.setWidth('100%')
-        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
-        rowspan_setter.addValueChangeListener(onRowSpanChange)
-        rowspan_setter.setWidth('100%')
-        body.add(colspan_setter)
-        body.add(rowspan_setter)
-
-        # size
-        width_str = self.getWidth()
-        if width_str.endswith('px'):
-            width = int(width_str[:-2])
-        else:
-            width = 0
-        height_str = self.getHeight()
-        if height_str.endswith('px'):
-            height = int(height_str[:-2])
-        else:
-            height = 0
-
-        def onWidthChange(value):
-            if not value:
-                self.setWidth('100%')
-            else:
-                self.setWidth('%dpx' % value)
-
-        def onHeightChange(value):
-            if not value:
-                self.setHeight('100%')
-            else:
-                self.setHeight('%dpx' % value)
-        width_setter = dialog.IntSetter("width (0=auto)", width)
-        width_setter.addValueChangeListener(onWidthChange)
-        width_setter.setWidth('100%')
-        height_setter = dialog.IntSetter("height (0=auto)", height)
-        height_setter.addValueChangeListener(onHeightChange)
-        height_setter.setHeight('100%')
-        body.add(width_setter)
-        body.add(height_setter)
-
-        # reset
-        def onReset(sender):
-            colspan_setter.setValue(1)
-            rowspan_setter.setValue(1)
-            width_setter.setValue(0)
-            height_setter.setValue(0)
-
-        reset_bt = Button("Reset", onReset)
-        body.add(reset_bt)
-        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
-
-        _dialog = dialog.GenericDialog("Widget setting", body)
-        _dialog.show()
-
-    def setTitle(self, text):
-        """change the title in the header of the widget
-        @param text: text of the new title"""
-        self.__title.setText(text)
-
-    def setHeaderInfo(self, text):
-        """change the info in the header of the widget
-        @param text: text of the new title"""
-        try:
-            self.__info.setHTML(text)
-        except TypeError:
-            log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
-
-    def isSelectable(self):
-        return self.__selectable
-
-    def setSelectable(self, selectable):
-        if not self.__selectable:
-            try:
-                self.removeClickListener(self)
-            except ValueError:
-                pass
-        if self.selectable and not self in self._clickListeners:
-            self.addClickListener(self)
-        self.__selectable = selectable
-
-    def getWarningData(self):
-        """ Return exposition warning level when this widget is selected and something is sent to it
-        This method should be overriden by children
-        @return: tuple (warning level type/HTML msg). Type can be one of:
-            - PUBLIC
-            - GROUP
-            - ONE2ONE
-            - MISC
-            - NONE
-        """
-        if not self.__selectable:
-            log.error("getWarningLevel must not be called for an unselectable widget")
-            raise Exception
-        # TODO: cleaner warning types (more general constants)
-        return ("NONE", None)
-
-    def setWidget(self, widget, scrollable=True):
-        """Set the widget that will be in the body of the LiberviaWidget
-        @param widget: widget to put in the body
-        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
-        if scrollable:
-            _scrollpanelwrapper = ScrollPanelWrapper()
-            _scrollpanelwrapper.setStyleName('widgetBody')
-            _scrollpanelwrapper.setWidget(widget)
-            body_wid = _scrollpanelwrapper
-        else:
-            body_wid = widget
-        self.add(body_wid)
-        self.setCellHeight(body_wid, '100%')
-
-    def doDetachChildren(self):
-        # We need to force the use of a panel subclass method here,
-        # for the same reason as doAttachChildren
-        VerticalPanel.doDetachChildren(self)
-
-    def doAttachChildren(self):
-        # We need to force the use of a panel subclass method here, else
-        # the event will not propagate to children
-        VerticalPanel.doAttachChildren(self)
-
-    def matchEntity(self, item):
-        """Check if this widget corresponds to the given entity.
-
-        This method should be overwritten by child classes.
-        @return: True if the widget matches the entity"""
-        raise NotImplementedError
-
-    def addMenus(self, menu_bar):
-        """Add menus to the header.
-
-        This method can be overwritten by child classes.
-        @param menu_bar (GenericMenuBar): menu bar of the widget's header
-        """
-        pass
-
-
-class ScrollPanelWrapper(SimplePanel):
-    """Scroll Panel like component, wich use the full available space
-    to work around percent size issue, it use some of the ideas found
-    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
-    specially in code given at comment #46, thanks to Stefan Bachert"""
-
-    def __init__(self, *args, **kwargs):
-        SimplePanel.__init__(self)
-        self.spanel = ScrollPanel(*args, **kwargs)
-        SimplePanel.setWidget(self, self.spanel)
-        DOM.setStyleAttribute(self.getElement(), "position", "relative")
-        DOM.setStyleAttribute(self.getElement(), "top", "0px")
-        DOM.setStyleAttribute(self.getElement(), "left", "0px")
-        DOM.setStyleAttribute(self.getElement(), "width", "100%")
-        DOM.setStyleAttribute(self.getElement(), "height", "100%")
-        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
-        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
-        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
-
-    def setWidget(self, widget):
-        self.spanel.setWidget(widget)
-
-    def setScrollPosition(self, position):
-        self.spanel.setScrollPosition(position)
-
-    def scrollToBottom(self):
-        self.setScrollPosition(self.spanel.getElement().scrollHeight)
-
-
-class EmptyWidget(DropCell, SimplePanel):
-    """Empty dropable panel"""
-
-    def __init__(self, host):
-        SimplePanel.__init__(self)
-        DropCell.__init__(self, host)
-        #self.setWidget(HTML(''))
-        self.setSize('100%', '100%')
-
-
-class BorderWidget(EmptyWidget):
-    def __init__(self, host):
-        EmptyWidget.__init__(self, host)
-        self.addStyleName('borderPanel')
-
-
-class LeftBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('leftBorderWidget')
-
-
-class RightBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('rightBorderWidget')
-
-
-class BottomBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('bottomBorderWidget')
-
-
-class WidgetsPanel(ScrollPanelWrapper):
-
-    def __init__(self, host, locked=False):
-        ScrollPanelWrapper.__init__(self)
-        self.setSize('100%', '100%')
-        self.host = host
-        self.locked = locked  # if True: tab will not be removed when there are no more widgets inside
-        self.selected = None
-        self.flextable = FlexTable()
-        self.flextable.setSize('100%', '100%')
-        self.setWidget(self.flextable)
-        self.setStyleName('widgetsPanel')
-        _bottom = BottomBorderWidget(self.host)
-        self.flextable.setWidget(0, 0, _bottom)  # There will be always an Empty widget on the last row,
-                                                 # dropping a widget there will add a new row
-        td_elt = _bottom.getElement().parentNode
-        DOM.setStyleAttribute(td_elt, "height", "1px")  # needed so the cell adapt to the size of the border (specially in webkit)
-        self._max_cols = 1  # give the maximum number of columns i a raw
-
-    def isLocked(self):
-        return self.locked
-
-    def changeWidget(self, row, col, wid):
-        """Change the widget in the given location, add row or columns when necessary"""
-        log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col))
-        last_row = max(0, self.flextable.getRowCount() - 1)
-        try:
-            prev_wid = self.flextable.getWidget(row, col)
-        except:
-            log.error("Trying to change an unexisting widget !")
-            return
-
-        cellFormatter = self.flextable.getFlexCellFormatter()
-
-        if isinstance(prev_wid, BorderWidget):
-            # We are on a border, we must create a row and/or columns
-            log.debug("BORDER WIDGET")
-            prev_wid.removeStyleName('dragover')
-
-            if isinstance(prev_wid, BottomBorderWidget):
-                # We are on the bottom border, we create a new row
-                self.flextable.insertRow(last_row)
-                self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
-                self.flextable.setWidget(last_row, 1, wid)
-                self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
-                cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
-                row = last_row
-
-            elif isinstance(prev_wid, LeftBorderWidget):
-                if col != 0:
-                    log.error("LeftBorderWidget must be on the first column !")
-                    return
-                self.flextable.insertCell(row, col + 1)
-                self.flextable.setWidget(row, 1, wid)
-
-            elif isinstance(prev_wid, RightBorderWidget):
-                if col != self.flextable.getCellCount(row) - 1:
-                    log.error("RightBorderWidget must be on the last column !")
-                    return
-                self.flextable.insertCell(row, col)
-                self.flextable.setWidget(row, col, wid)
-
-        else:
-            prev_wid.removeFromParent()
-            self.flextable.setWidget(row, col, wid)
-
-        _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
-        if _max_cols != self._max_cols:
-            self._max_cols = _max_cols
-            self._sizesAdjust()
-
-    def _sizesAdjust(self):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        width = 100.0 / max(1, self._max_cols - 2)  # we don't count the borders
-
-        for row_idx in xrange(self.flextable.getRowCount()):
-            for col_idx in xrange(self.flextable.getCellCount(row_idx)):
-                _widget = self.flextable.getWidget(row_idx, col_idx)
-                if not isinstance(_widget, BorderWidget):
-                    td_elt = _widget.getElement().parentNode
-                    DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
-
-        last_row = max(0, self.flextable.getRowCount() - 1)
-        cellFormatter.setColSpan(last_row, 0, self._max_cols)
-
-    def addWidget(self, wid):
-        """Add a widget to a new cell on the next to last row"""
-        last_row = max(0, self.flextable.getRowCount() - 1)
-        log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
-        self.changeWidget(last_row, 0, wid)
-
-    def removeWidget(self, wid):
-        """Remove a widget and the cell where it is"""
-        _row, _col = self.flextable.getIndex(wid)
-        self.flextable.remove(wid)
-        self.flextable.removeCell(_row, _col)
-        if not self.getLiberviaRowWidgets(_row):  # we have no more widgets, we remove the row
-            self.flextable.removeRow(_row)
-        _max_cols = 1
-        for row_idx in xrange(self.flextable.getRowCount()):
-            _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
-        if _max_cols != self._max_cols:
-            self._max_cols = _max_cols
-            self._sizesAdjust()
-        current = self
-
-        blank_page = self.getLiberviaWidgetsCount() == 0  # do we still have widgets on the page ?
-
-        if blank_page and not self.isLocked():
-            # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
-            while current is not None:
-                if isinstance(current, MainTabPanel):
-                    current.onWidgetPanelRemove(self)
-                    return
-                current = current.getParent()
-            log.error("no MainTabPanel found !")
-
-    def getWidgetCoords(self, wid):
-        return self.flextable.getIndex(wid)
-
-    def getLiberviaRowWidgets(self, row):
-        """ Return all the LiberviaWidget in the row """
-        return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
-
-    def getRowWidgets(self, row):
-        """ Return all the widgets in the row """
-        widgets = []
-        cols = self.flextable.getCellCount(row)
-        for col in xrange(cols):
-            widgets.append(self.flextable.getWidget(row, col))
-        return widgets
-
-    def getLiberviaWidgetsCount(self):
-        """ Get count of contained widgets """
-        return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
-
-    def getIndex(self, wid):
-        return self.flextable.getIndex(wid)
-
-    def getColSpan(self, row, col):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.getColSpan(row, col)
-
-    def setColSpan(self, row, col, value):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.setColSpan(row, col, value)
-
-    def getRowSpan(self, row, col):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.getRowSpan(row, col)
-
-    def setRowSpan(self, row, col, value):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.setRowSpan(row, col, value)
-
-
-class DropTab(Label, DropWidget):
-
-    def __init__(self, tab_panel, text):
-        Label.__init__(self, text)
-        DropWidget.__init__(self, tab_panel)
-        self.tab_panel = tab_panel
-        self.setStyleName('dropCell')
-        self.setWordWrap(False)
-        DOM.setStyleAttribute(self.getElement(), "min-width", "30px")
-
-    def _getIndex(self):
-        """ get current index of the DropTab """
-        # XXX: awful hack, but seems the only way to get index
-        return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
-
-    def onDragEnter(self, event):
-        #if self == LiberviaDragWidget.current:
-        #    return
-        self.addStyleName('dragover')
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        self.removeStyleName('dragover')
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        self.removeStyleName('dragover')
-        if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
-            # the widget come from the DragTab, so nothing to do, we let it there
-            return
-
-        # FIXME: quite the same stuff as in DropCell, need some factorisation
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        try:
-            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-            # item_type = dt.getData("type")
-            log.debug("message: %s" % item)
-            log.debug("type: %s" % item_type)
-        except:
-            log.debug("no message found")
-            item = '&nbsp;'
-            item_type = None
-        if item_type == "WIDGET":
-            if not LiberviaDragWidget.current:
-                log.error("No widget registered in LiberviaDragWidget !")
-                return
-            _new_panel = LiberviaDragWidget.current
-            _new_panel.getWidgetsPanel().removeWidget(_new_panel)
-        elif item_type in DropCell.drop_keys:
-            _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
-        else:
-            log.warning("unmanaged item type")
-            return
-
-        widgets_panel = self.tab_panel.getWidget(self._getIndex())
-        widgets_panel.addWidget(_new_panel)
-
-
-class MainTabPanel(TabPanel):
-
-    def __init__(self, host):
-        TabPanel.__init__(self)
-        self.host = host
-        self.tabBar.setVisible(False)
-        self.setStyleName('liberviaTabPanel')
-        self.addStyleName('mainTabPanel')
-        Window.addWindowResizeListener(self)
-
-    def getCurrentPanel(self):
-        """ Get the panel of the currently selected tab """
-        return self.deck.visibleWidget
-
-    def onWindowResized(self, width, height):
-        tab_panel_elt = self.getElement()
-        _elts = doc().getElementsByClassName('gwt-TabBar')
-        if not _elts.length:
-            log.error("no TabBar found, it should exist !")
-            tab_bar_h = 0
-        else:
-            tab_bar_h = _elts.item(0).offsetHeight
-        ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5
-        ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5
-        self.setWidth("%s%s" % (ideal_width, "px"))
-        self.setHeight("%s%s" % (ideal_height, "px"))
-
-    def add(self, widget, text=''):
-        tab = DropTab(self, text)
-        TabPanel.add(self, widget, tab, False)
-        if self.getWidgetCount() > 1:
-            self.tabBar.setVisible(True)
-            self.host.resize()
-
-    def onWidgetPanelRemove(self, panel):
-        """ Called when a child WidgetsPanel is empty and need to be removed """
-        self.remove(panel)
-        widgets_count = self.getWidgetCount()
-        if widgets_count == 1:
-            self.tabBar.setVisible(False)
-            self.host.resize()
-            self.selectTab(0)
-        else:
-            self.selectTab(widgets_count - 1)
+    def getCategoryHTML(cls, category):
+        return cls.ITEM_TPL % category.icon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/blog.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,771 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _, D_
+
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Image import Image
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.FlowPanel import FlowPanel
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.Timer import Timer
+
+from datetime import datetime
+from time import time
+
+import html_tools
+import dialog
+import richtext
+import editor_widget
+import libervia_widget
+from constants import Const as C
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.tools import jid
+
+
+unicode = str # XXX: pyjamas doesn't manage unicode
+
+
+class MicroblogItem():
+    # XXX: should be moved in a separated module
+
+    def __init__(self, data):
+        self.id = data['id']
+        self.type = data.get('type', 'main_item')
+        self.empty = data.get('new', False)
+        self.title = data.get('title', '')
+        self.title_xhtml = data.get('title_xhtml', '')
+        self.content = data.get('content', '')
+        self.content_xhtml = data.get('content_xhtml', '')
+        self.author = jid.JID(data['author'])
+        self.updated = float(data.get('updated', 0))  # XXX: int doesn't work here
+        self.published = float(data.get('published', self.updated))  # XXX: int doesn't work here
+        self.service = data.get('service', '')
+        self.node = data.get('node', '')
+        self.comments = data.get('comments', False)
+        self.comments_service = data.get('comments_service', '')
+        self.comments_node = data.get('comments_node', '')
+
+
+class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler):
+
+    def __init__(self, blog_panel, data):
+        """
+        @param blog_panel: the parent panel
+        @param data: dict containing the blog item data, or a MicroblogItem instance.
+        """
+        self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data)
+        for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml',
+                     'author', 'updated', 'published', 'comments', 'service', 'node',
+                     'comments_service', 'comments_node']:
+            getter = lambda attr: lambda inst: getattr(inst._base_item, attr)
+            setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value)
+            setattr(MicroblogEntry, attr, property(getter(attr), setter(attr)))
+
+        SimplePanel.__init__(self)
+        self._blog_panel = blog_panel
+
+        self.panel = FlowPanel()
+        self.panel.setStyleName('mb_entry')
+
+        self.header = HorizontalPanel(StyleName='mb_entry_header')
+        self.panel.add(self.header)
+
+        self.entry_actions = VerticalPanel()
+        self.entry_actions.setStyleName('mb_entry_actions')
+        self.panel.add(self.entry_actions)
+
+        entry_avatar = SimplePanel()
+        entry_avatar.setStyleName('mb_entry_avatar')
+        assert isinstance(self.author, jid.JID) # FIXME: temporary
+        self.avatar = Image(self._blog_panel.host.getAvatarURL(self.author)) # FIXME: self.author should be initially a jid.JID
+        entry_avatar.add(self.avatar)
+        self.panel.add(entry_avatar)
+
+        self.entry_dialog = VerticalPanel()
+        self.entry_dialog.setStyleName('mb_entry_dialog')
+        self.panel.add(self.entry_dialog)
+
+        self.add(self.panel)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+        self.__pub_data = (self.service, self.node, self.id)
+        self.__setContent()
+
+    def __setContent(self):
+        """Actually set the entry content (header, icons, bubble...)"""
+        self.delete_label = self.update_label = self.comment_label = None
+        self.bubble = self._current_comment = None
+        self.__setHeader()
+        self.__setBubble()
+        self.__setIcons()
+
+    def __setHeader(self):
+        """Set the entry header."""
+        if self.empty:
+            return
+        update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
+        self.header.add(HTML("""<span class='mb_entry_header_info'>
+                                  <span class='mb_entry_author'>%(author)s</span> on
+                                  <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
+                                </span>""" % {'author': html_tools.html_sanitize(unicode(self.author)),
+                                              'published': datetime.fromtimestamp(self.published),
+                                              'updated': update_text if self.published != self.updated else ''
+                                              }))
+        if self.comments:
+            self.comments_count = self.hidden_count = 0
+            self.show_comments_link = HTML('')
+            self.header.add(self.show_comments_link)
+
+    def updateHeader(self, comments_count=None, hidden_count=None, inc=None):
+        """Update the header.
+
+        @param comments_count (int): total number of comments.
+        @param hidden_count (int): number of hidden comments.
+        @param inc (int): number to increment the total number of comments with.
+        """
+        if comments_count is not None:
+            self.comments_count = comments_count
+        if hidden_count is not None:
+            self.hidden_count = hidden_count
+        if inc is not None:
+            self.comments_count += inc
+
+        if self.hidden_count > 0:
+            comments = D_('comments') if self.hidden_count > 1 else D_('comment')
+            text = D_("<a>show %(count)d previous %(comments)s</a>") % {'count': self.hidden_count,
+                                                                        'comments': comments}
+            if self not in self.show_comments_link._clickListeners:
+                self.show_comments_link.addClickListener(self)
+        else:
+            if self.comments_count > 1:
+                text = "%(count)d %(comments)s" % {'count': self.comments_count,
+                                                   'comments': D_('comments')}
+            elif self.comments_count == 1:
+                text = D_('1 comment')
+            else:
+                text = ''
+            try:
+                self.show_comments_link.removeClickListener(self)
+            except ValueError:
+                pass
+
+        self.show_comments_link.setHTML("""<span class='mb_entry_comments'>%(text)s</span></div>""" % {'text': text})
+
+    def __setIcons(self):
+        """Set the entry icons (delete, update, comment)"""
+        if self.empty:
+            return
+
+        def addIcon(label, title):
+            label = Label(label)
+            label.setTitle(title)
+            label.addClickListener(self)
+            self.entry_actions.add(label)
+            return label
+
+        if self.comments:
+            self.comment_label = addIcon(u"↶", "Comment this message")
+            self.comment_label.setStyleName('mb_entry_action_larger')
+        is_publisher = self.author == self._blog_panel.host.whoami.bare
+        if is_publisher:
+            self.update_label = addIcon(u"✍", "Edit this message")
+        if is_publisher or unicode(self.node).endswith(unicode(self._blog_panel.host.whoami.bare)):
+            self.delete_label = addIcon(u"✗", "Delete this message")
+
+    def updateAvatar(self, new_avatar):
+        """Change the avatar of the entry
+        @param new_avatar: path to the new image"""
+        self.avatar.setUrl(new_avatar)
+
+    def onClick(self, sender):
+        if sender == self:
+            self._blog_panel.setSelectedEntry(self)
+        elif sender == self.delete_label:
+            self._delete()
+        elif sender == self.update_label:
+            self.edit(True)
+        elif sender == self.comment_label:
+            self._comment()
+        elif sender == self.show_comments_link:
+            self._blog_panel.loadAllCommentsForEntry(self)
+
+    def __modifiedCb(self, content):
+        """Send the new content to the backend
+        @return: False to restore the original content if a deletion has been cancelled
+        """
+        if not content['text']:  # previous content has been emptied
+            self._delete(True)
+            return False
+        extra = {'published': unicode(self.published)}
+        if isinstance(self.bubble, richtext.RichTextEditor):
+            # TODO: if the user change his parameters after the message edition started,
+            # the message syntax could be different then the current syntax: pass the
+            # message syntax in extra for the frontend to use it instead of current syntax.
+            extra.update({'content_rich': content['text'], 'title': content['title']})
+        if self.empty:
+            if self.type == 'main_item':
+                self._blog_panel.host.bridge.call('sendMblog', None, None, tuple(self._blog_panel.accepted_groups), content['text'], extra)
+            else:
+                self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
+        else:
+            self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra)
+        return True
+
+    def __afterEditCb(self, content):
+        """Remove the entry if it was an empty one (used for creating a new blog post).
+        Data for the actual new blog post will be received from the bridge"""
+        if self.empty:
+            self._blog_panel.removeEntry(self.type, self.id, update_header=False)
+            if self.type == 'main_item':  # restore the "New message" button
+                self._blog_panel.addNewMessageEntry()
+            else:  # allow to create a new comment
+                self._parent_entry._current_comment = None
+        self.entry_dialog.setWidth('auto')
+        try:
+            self.toggle_syntax_button.removeFromParent()
+        except (AttributeError, TypeError):
+            pass
+
+    def __setBubble(self, edit=False):
+        """Set the bubble displaying the initial content."""
+        content = {'text': self.content_xhtml if self.content_xhtml else self.content,
+                   'title': self.title_xhtml if self.title_xhtml else self.title}
+        if self.content_xhtml:
+            content.update({'syntax': C.SYNTAX_XHTML})
+            if self.author != self._blog_panel.host.whoami.bare:
+                options = ['read_only']
+            else:
+                options = [] if self.empty else ['update_msg']
+            self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
+        else:  # assume raw text message have no title
+            self.bubble = editor_widget.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
+        self.bubble.addStyleName("bubble")
+        try:
+            self.toggle_syntax_button.removeFromParent()
+        except (AttributeError, TypeError):
+            pass
+        self.entry_dialog.add(self.bubble)
+        self.edit(edit)
+        self.bubble.addEditListener(self.__showWarning)
+
+    def __showWarning(self, sender, keycode):
+        if keycode == KEY_ENTER:
+            self._blog_panel.host.showWarning(None, None)
+        else:
+            self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment'))
+
+    def _delete(self, empty=False):
+        """Ask confirmation for deletion.
+        @return: False if the deletion has been cancelled."""
+        def confirm_cb(answer):
+            if answer:
+                self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments)
+            else:  # restore the text if it has been emptied during the edition
+                self.bubble.setContent(self.bubble._original_content)
+
+        if self.empty:
+            text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
+            dialog.InfoDialog(_("Information"), text).show()
+            return
+        text = ""
+        if empty:
+            text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
+        target = _('message and all its comments') if self.comments else _('comment')
+        text += _("Do you really want to delete this %s?") % target
+        dialog.ConfirmDialog(confirm_cb, text=text).show()
+
+    def _comment(self):
+        """Add an empty entry for a new comment"""
+        if self._current_comment:
+            self._current_comment.bubble.setFocus(True)
+            self._blog_panel.setSelectedEntry(self._current_comment, True)
+            return
+        data = {'id': unicode(time()),
+                'new': True,
+                'type': 'comment',
+                'author': unicode(self._blog_panel.host.whoami.bare),
+                'service': self.comments_service,
+                'node': self.comments_node
+                }
+        entry = self._blog_panel.addEntry(data, update_header=False)
+        if entry is None:
+            log.info("The entry of id %s can not be commented" % self.id)
+            return
+        entry._parent_entry = self
+        self._current_comment = entry
+        self.edit(True, entry)
+        self._blog_panel.setSelectedEntry(entry, True)
+
+    def edit(self, edit, entry=None):
+        """Toggle the bubble between display and edit mode
+        @edit: boolean value
+        @entry: MicroblogEntry instance, or None to use self
+        """
+        if entry is None:
+            entry = self
+        try:
+            entry.toggle_syntax_button.removeFromParent()
+        except (AttributeError, TypeError):
+            pass
+        entry.bubble.edit(edit)
+        if edit:
+            if isinstance(entry.bubble, richtext.RichTextEditor):
+                image = '<a class="richTextIcon">A</a>'
+                html = '<a style="color: blue;">raw text</a>'
+                title = _('Switch to raw text edition')
+            else:
+                image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
+                html = '<a style="color: blue;">rich text</a>'
+                title = _('Switch to rich text edition')
+            entry.toggle_syntax_button = HTML(html)
+            entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax)
+            entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
+            entry.entry_dialog.add(entry.toggle_syntax_button)
+            entry.toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
+            entry.toggle_syntax_button.setStyleAttribute('left', '-20px')
+
+    def toggleContentSyntax(self):
+        """Toggle the editor between raw and rich text"""
+        original_content = self.bubble.getOriginalContent()
+        rich = not isinstance(self.bubble, richtext.RichTextEditor)
+        if rich:
+            original_content['syntax'] = C.SYNTAX_XHTML
+
+        def setBubble(text):
+            self.content = text
+            self.content_xhtml = text if rich else ''
+            self.content_title = self.content_title_xhtml = ''
+            self.bubble.removeFromParent()
+            self.__setBubble(True)
+            self.bubble.setOriginalContent(original_content)
+            if rich:
+                self.bubble.setDisplayContent()  # needed in case the edition is aborted, to not end with an empty bubble
+
+        text = self.bubble.getContent()['text']
+        if not text:
+            setBubble(' ')  # something different than empty string is needed to initialize the rich text editor
+            return
+        if not rich:
+            def confirm_cb(answer):
+                if answer:
+                    self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT)
+            dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
+        else:
+            self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
+
+
+class MicroblogPanel(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget, MouseHandler):
+    warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know"
+    warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>"
+    # FIXME: all the generic parts must be moved to quick_frontends
+
+    def __init__(self, host, targets, profiles=None):
+        """Panel used to show microblog
+
+        @param targets (tuple(unicode)): contact groups displayed in this panel.
+            If empty, show all microblogs from all contacts.
+        """
+        # do not mix self.targets (set of tuple of unicode) and self.accepted_groups (set of unicode)
+        quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE)
+        libervia_widget.LiberviaWidget.__init__(self, host, ", ".join(self.accepted_groups), selectable=True)
+        MouseHandler.__init__(self)
+        self.entries = {}
+        self.comments = {}
+        self.vpanel = VerticalPanel()
+        self.vpanel.setStyleName('microblogPanel')
+        self.setWidget(self.vpanel)
+        self.addNewMessageEntry()
+
+        self.footer = HTML('', StyleName='microblogPanel_footer')
+        self.footer.waiting = False
+        self.footer.addClickListener(self)
+        self.footer.addMouseListener(self)
+        self.vpanel.add(self.footer)
+        self.next_rsm_index = 0
+
+        # 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.avatarListener = self.onAvatarUpdate
+        host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
+
+    def __str__(self):
+        return u"Blog Widget [target: {}, profile: {}]".format(', '.join(self.accepted_groups), self.profile)
+
+    def onDelete(self):
+        quick_widgets.QuickWidget.onDelete(self)
+        self.host.removeListener('avatar', self.avatarListener)
+
+    def onAvatarUpdate(self, jid_, hash_, profile):
+        """Called on avatar update events
+
+        @param jid_: jid of the entity with updated avatar
+        @param hash_: hash of the avatar
+        @param profile: %(doc_profile)s
+        """
+        whoami = self.host.profiles[self.profile].whoami
+        if self.isJidAccepted(jid_) or jid_.bare == whoami.bare:
+            self.updateValue('avatar', jid_, hash_)
+
+    def addNewMessageEntry(self):
+        """Add an empty entry for writing a new message if needed."""
+        if self.getNewMainEntry():
+            return  # there's already one
+        data = {'id': unicode(time()),
+                'new': True,
+                'author': unicode(self.host.whoami.bare),
+                }
+        entry = self.addEntry(data, update_header=False)
+        entry.edit(True)
+
+    def getNewMainEntry(self):
+        """Get the new entry being edited, or None if it doesn't exists.
+
+        @return (MicroblogEntry): the new entry being edited.
+        """
+        if len(self.vpanel.children) < 2:
+            return None  # there's only the footer
+        first = self.vpanel.children[0]
+        assert(first.type == 'main_item')
+        return first if first.empty else None
+
+    @staticmethod
+    def onGroupDrop(host, targets):
+        """Create a microblog panel for one, several or all contact groups.
+
+        @param host (SatWebFrontend): the SatWebFrontend instance
+        @param targets (tuple(unicode)): tuple of groups (empty for "all groups")
+        @return: the created MicroblogPanel
+        """
+        # XXX: pyjamas doesn't support use of cls directly
+        widget = host.displayWidget(MicroblogPanel, targets, dropped=True)
+        widget.loadMoreMainEntries()
+        return widget
+
+    @property
+    def accepted_groups(self):
+        """Return a set of the accepted groups"""
+        return set().union(*self.targets)
+
+    def loadAllCommentsForEntry(self, main_entry):
+        """Load all the comments for the given main entry.
+
+        @param main_entry (MicroblogEntry): main entry having comments.
+        """
+        index = str(main_entry.comments_count - main_entry.hidden_count)
+        rsm = {'max': str(main_entry.hidden_count), 'index': index}
+        self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm)
+
+    def loadMoreMainEntries(self):
+        if self.footer.waiting:
+            return
+        self.footer.waiting = True
+        self.footer.setHTML("loading...")
+
+        self.host.loadOurMainEntries(self.next_rsm_index, self)
+
+        type_ = 'ALL' if self.accepted_groups == [] else 'GROUP'
+        rsm = {'max': str(C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
+        self.host.bridge.getMassiveMblogs(type_, list(self.accepted_groups), rsm, profile=C.PROF_KEY_NONE, callback=self.massiveInsert)
+
+    def getWarningData(self, comment):
+        """
+        @param comment: set to True if the composed message is a comment
+        @return: a couple (type, msg) for calling self.host.showWarning"""
+        if comment:
+            return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
+        elif not self.accepted_groups:
+            # we have a meta MicroblogPanel, we publish publicly
+            return ("PUBLIC", self.warning_msg_public)
+        else:
+            # FIXME: manage several groups
+            return ("GROUP", self.warning_msg_group % ' '.join(self.accepted_groups))
+
+    def onTextEntered(self, text):
+        if not self.accepted_groups:
+            # we are entering a public microblog
+            self.bridge.call("sendMblog", None, "PUBLIC", (), text, {})
+        else:
+            self.bridge.call("sendMblog", None, "GROUP", tuple(self.accepted_groups), text, {})
+
+    def accept_all(self):
+        return not self.accepted_groups  # we accept every microblog only if we are not filtering by groups
+
+    def getEntries(self):
+        """Ask all the entries for the currenly accepted groups,
+        and fill the panel"""
+
+    def massiveInsert(self, mblogs):
+        """Insert several microblogs at once
+
+        @param mblogs (dict): dictionary mapping a publisher to microblogs data:
+            - key: publisher (str)
+            - value: couple (list[dict], dict) with:
+                - list of microblogs data
+                - RSM response data
+        """
+        count_pub = len(mblogs)
+        count_msg = sum([len(value) for value in mblogs.values()])
+        log.debug("massive insertion of {count_msg} blogs for {count_pub} contacts".format(count_msg=count_msg, count_pub=count_pub))
+        for publisher in mblogs:
+            log.debug("adding {count} blogs for [{publisher}]".format(count=len(mblogs[publisher]), publisher=publisher))
+            self.mblogsInsert(mblogs[publisher])
+        self.next_rsm_index += C.RSM_MAX_ITEMS
+        self.footer.waiting = False
+        self.footer.setHTML('show older messages')
+
+    def mblogsInsert(self, mblogs):
+        """ Insert several microblogs from the same node at once.
+
+        @param mblogs (list): couple (list[dict], dict) with:
+            - list of microblogs data
+            - RSM response data
+        """
+        mblogs, rsm = mblogs
+
+        for mblog in mblogs:
+            if "content" not in mblog:
+                log.warning("No content found in microblog [%s]" % mblog)
+                continue
+            self.addEntry(mblog, update_header=False)
+
+        hashes = set([(entry['service'], entry['node']) for entry in mblogs if entry['type'] == 'comment'])
+        assert(len(hashes) < 2)  # ensure the blogs come from the same node
+        if len(hashes) == 1:
+            main_entry = self.comments[hashes.pop()]
+            count = int(rsm['count'])
+            hidden = count - (int(rsm['index']) + len(mblogs))
+            main_entry.updateHeader(count, hidden)
+
+    def _chronoInsert(self, vpanel, entry, reverse=True):
+        """ Insert an entry in chronological order
+        @param vpanel: VerticalPanel instance
+        @param entry: MicroblogEntry
+        @param reverse: more recent entry on top if True, chronological order else"""
+        # XXX: for now we can't use "published" timestamp because the entries
+        # are retrieved using the "updated" field. We don't want new items
+        # inserted with RSM to be inserted "randomly" in the panel, they
+        # should be added at the bottom of the list.
+        assert(isinstance(reverse, bool))
+        if entry.empty:
+            entry.updated = time()
+        # we look for the right index to insert our entry:
+        # if reversed, we insert the entry above the first entry
+        # in the past
+        idx = 0
+
+        for child in vpanel.children[0:-1]:  # ignore the footer
+            if not isinstance(child, MicroblogEntry):
+                idx += 1
+                continue
+            condition_to_stop = child.empty or (child.updated > entry.updated)
+            if condition_to_stop != reverse:  # != is XOR
+                break
+            idx += 1
+
+        vpanel.insert(entry, idx)
+
+    def addEntryIfAccepted(self, sender, groups, mblog_entry):
+        """Check if an entry can go in MicroblogPanel and add to it
+
+        @param sender(jid.JID): jid of the entry sender
+        @param groups: groups which can receive this entry
+        @param mblog_entry: panels.MicroblogItem instance
+        """
+        assert isinstance(sender, jid.JID) # FIXME temporary
+        if (mblog_entry.type == "comment"
+            or self.isJidAccepted(sender)
+            or (groups is None and sender == self.host.profiles[self.profile].whoami.bare)
+            or (groups and groups.intersection(self.accepted_groups))):
+            self.addEntry(mblog_entry)
+
+    def addEntry(self, data, update_header=True):
+        """Add an entry to the panel
+
+        @param data (dict): dict containing the item data
+        @param update_header (bool): update or not the main comment header
+        @return: the added MicroblogEntry instance, or None
+        """
+        _entry = MicroblogEntry(self, data)
+        if _entry.type == "comment":
+            comments_hash = (_entry.service, _entry.node)
+            if comments_hash not in self.comments:
+                # The comments node is not known in this panel
+                return None
+            parent = self.comments[comments_hash]
+            parent_idx = self.vpanel.getWidgetIndex(parent)
+            # we find or create the panel where the comment must be inserted
+            try:
+                sub_panel = self.vpanel.getWidget(parent_idx + 1)
+            except IndexError:
+                sub_panel = None
+            if not sub_panel or not isinstance(sub_panel, VerticalPanel):
+                sub_panel = VerticalPanel()
+                sub_panel.setStyleName('microblogPanel')
+                sub_panel.addStyleName('subPanel')
+                self.vpanel.insert(sub_panel, parent_idx + 1)
+
+            for idx in xrange(0, len(sub_panel.getChildren())):
+                comment = sub_panel.getIndexedChild(idx)
+                if comment.id == _entry.id:
+                    # update an existing comment
+                    sub_panel.remove(comment)
+                    sub_panel.insert(_entry, idx)
+                    return _entry
+            # we want comments to be inserted in chronological order
+            self._chronoInsert(sub_panel, _entry, reverse=False)
+            if update_header:
+                parent.updateHeader(inc=+1)
+            return _entry
+
+        if _entry.comments:
+            # entry has comments, we keep the comments service/node as a reference
+            comments_hash = (_entry.comments_service, _entry.comments_node)
+            self.comments[comments_hash] = _entry
+            self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
+
+        if _entry.id in self.entries:  # update
+            old_entry = self.entries[_entry.id]
+            idx = self.vpanel.getWidgetIndex(old_entry)
+            counts = (old_entry.comments_count, old_entry.hidden_count)
+            self.vpanel.remove(old_entry)
+            self.vpanel.insert(_entry, idx)
+            _entry.updateHeader(*counts)
+        else:  # new entry
+            self._chronoInsert(self.vpanel, _entry)
+            if _entry.comments:
+                self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
+
+        self.entries[_entry.id] = _entry
+
+        return _entry
+
+    def removeEntry(self, type_, id_, update_header=True):
+        """Remove an entry from the panel
+
+        @param type_ (str): entry type ('main_item' or 'comment')
+        @param id_ (str): entry id
+        @param update_header (bool): update or not the main comment header
+        """
+        for child in self.vpanel.getChildren():
+            if isinstance(child, MicroblogEntry) and type_ == 'main_item':
+                if child.id == id_:
+                    main_idx = self.vpanel.getWidgetIndex(child)
+                    try:
+                        sub_panel = self.vpanel.getWidget(main_idx + 1)
+                        if isinstance(sub_panel, VerticalPanel):
+                            sub_panel.removeFromParent()
+                    except IndexError:
+                        pass
+                    child.removeFromParent()
+                    break
+            elif isinstance(child, VerticalPanel) and type_ == 'comment':
+                for comment in child.getChildren():
+                    if comment.id == id_:
+                        if update_header:
+                            hash_ = (comment.service, comment.node)
+                            self.comments[hash_].updateHeader(inc=-1)
+                        comment.removeFromParent()
+                        break
+
+    def ensureVisible(self, entry):
+        """Scroll to an entry to ensure its visibility
+
+        @param entry (MicroblogEntry): the entry
+        """
+        try:
+            self.vpanel.getParent().ensureVisible(entry)  # scroll to the clicked entry
+        except AttributeError:
+            log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!")
+
+    def setSelectedEntry(self, entry, ensure_visible=False):
+        """Select an entry.
+
+        @param entry (MicroblogEntry): the entry to select
+        @param ensure_visible (boolean): if True, also scroll to the entry
+        """
+        if ensure_visible:
+            self.ensureVisible(entry)
+
+        entry.addStyleName('selected_entry')  # blink the clicked entry
+        clicked_entry = entry  # entry may be None when the timer is done
+        Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry'))
+
+    def updateValue(self, type_, jid_, value):
+        """Update a jid value in entries
+
+        @param type_: one of 'avatar', 'nick'
+        @param jid_(jid.JID): jid concerned
+        @param value: new value"""
+        assert isinstance(jid_, jid.JID) # FIXME: temporary
+        def updateVPanel(vpanel):
+            avatar_url = self.host.getAvatarURL(jid_)
+            for child in vpanel.children:
+                if isinstance(child, MicroblogEntry) and child.author == jid_:
+                    child.updateAvatar(avatar_url)
+                elif isinstance(child, VerticalPanel):
+                    updateVPanel(child)
+        if type_ == 'avatar':
+            updateVPanel(self.vpanel)
+
+    def addAcceptedGroups(self, groups):
+        """Add one or more group(s) which can be displayed in this panel.
+
+        @param groups (tuple(unicode)): tuple of groups to add
+        """
+        # FIXME: update the widget's hash in QuickApp._widgets[MicroblogPanel]
+        self.targets.update(groups)
+
+    def isJidAccepted(self, jid_):
+        """Tell if a jid is actepted and must be shown in this panel
+
+        @param jid_(jid.JID): jid to check
+        @return: True if the jid is accepted
+        """
+        assert isinstance(jid_, jid.JID) # FIXME temporary
+        if self.accept_all():
+            return True
+        for group in self.accepted_groups:
+            if self.host.contact_lists[self.profile].isEntityInGroup(jid_, group):
+                return True
+        return False
+
+    def onClick(self, sender):
+        if sender == self.footer:
+            self.loadMoreMainEntries()
+
+    def onMouseEnter(self, sender):
+        if sender == self.footer:
+            self.loadMoreMainEntries()
+
+
+libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,)))
+
+# Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group
+libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, ()))
--- a/src/browser/sat_browser/card_game.py	Thu Feb 05 12:05:32 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,387 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 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/>.
-
-import pyjd  # this is dummy in pyjs
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.games import TarotCard
-from sat.core.i18n import _
-
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.DockPanel import DockPanel
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.Image import Image
-from pyjamas.ui.Label import Label
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui import HasAlignment
-from pyjamas import Window
-from pyjamas import DOM
-
-import dialog
-import xmlui
-
-
-CARD_WIDTH = 74
-CARD_HEIGHT = 136
-CARD_DELTA_Y = 30
-MIN_WIDTH = 950  # Minimum size of the panel
-MIN_HEIGHT = 500
-
-
-class CardWidget(TarotCard, Image, MouseHandler):
-    """This class is used to represent a card, graphically and logically"""
-
-    def __init__(self, parent, file_):
-        """@param file: path of the PNG file"""
-        self._parent = parent
-        Image.__init__(self, file_)
-        root_name = file_[file_.rfind("/") + 1:-4]
-        suit, value = root_name.split('_')
-        TarotCard.__init__(self, (suit, value))
-        MouseHandler.__init__(self)
-        self.addMouseListener(self)
-
-    def onMouseEnter(self, sender):
-        if self._parent.state == "ecart" or self._parent.state == "play":
-            DOM.setStyleAttribute(self.getElement(), "top", "0px")
-
-    def onMouseLeave(self, sender):
-        if not self in self._parent.hand:
-            return
-        if not self in list(self._parent.selected):  # FIXME: Workaround pyjs bug, must report it
-            DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y)
-
-    def onMouseUp(self, sender, x, y):
-        if self._parent.state == "ecart":
-            if self not in list(self._parent.selected):
-                self._parent.addToSelection(self)
-            else:
-                self._parent.removeFromSelection(self)
-        elif self._parent.state == "play":
-            self._parent.playCard(self)
-
-
-class CardPanel(DockPanel, ClickHandler):
-
-    def __init__(self, parent, referee, player_nick, players):
-        DockPanel.__init__(self)
-        ClickHandler.__init__(self)
-        self._parent = parent
-        self._autoplay = None  # XXX: use 0 to activate fake play, None else
-        self.referee = referee
-        self.players = players
-        self.player_nick = player_nick
-        self.bottom_nick = self.player_nick
-        idx = self.players.index(self.player_nick)
-        idx = (idx + 1) % len(self.players)
-        self.right_nick = self.players[idx]
-        idx = (idx + 1) % len(self.players)
-        self.top_nick = self.players[idx]
-        idx = (idx + 1) % len(self.players)
-        self.left_nick = self.players[idx]
-        self.bottom_nick = player_nick
-        self.selected = set()  # 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
-        self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT)
-        self.setStyleName("cardPanel")
-
-        # Now we set up the layout
-        _label = Label(self.top_nick)
-        _label.setStyleName('cardGamePlayerNick')
-        self.add(_label, DockPanel.NORTH)
-        self.setCellWidth(_label, '100%')
-        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER)
-
-        self.hand_panel = AbsolutePanel()
-        self.add(self.hand_panel, DockPanel.SOUTH)
-        self.setCellWidth(self.hand_panel, '100%')
-        self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER)
-
-        _label = Label(self.left_nick)
-        _label.setStyleName('cardGamePlayerNick')
-        self.add(_label, DockPanel.WEST)
-        self.setCellHeight(_label, '100%')
-        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
-
-        _label = Label(self.right_nick)
-        _label.setStyleName('cardGamePlayerNick')
-        self.add(_label, DockPanel.EAST)
-        self.setCellHeight(_label, '100%')
-        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT)
-        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
-
-        self.center_panel = DockPanel()
-        self.inner_left = SimplePanel()
-        self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_left, DockPanel.WEST)
-        self.center_panel.setCellHeight(self.inner_left, '100%')
-        self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT)
-        self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE)
-
-        self.inner_right = SimplePanel()
-        self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_right, DockPanel.EAST)
-        self.center_panel.setCellHeight(self.inner_right, '100%')
-        self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE)
-
-        self.inner_top = SimplePanel()
-        self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_top, DockPanel.NORTH)
-        self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER)
-        self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM)
-
-        self.inner_bottom = SimplePanel()
-        self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_bottom, DockPanel.SOUTH)
-        self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER)
-        self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP)
-
-        self.inner_center = SimplePanel()
-        self.center_panel.add(self.inner_center, DockPanel.CENTER)
-        self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER)
-        self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE)
-
-        self.add(self.center_panel, DockPanel.CENTER)
-        self.setCellWidth(self.center_panel, '100%')
-        self.setCellHeight(self.center_panel, '100%')
-        self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE)
-        self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER)
-
-        self.loadCards()
-        self.mouse_over_card = None  # contain the card to highlight
-        self.visible_size = CARD_WIDTH / 2  # number of pixels visible for cards
-        self.addClickListener(self)
-
-    def loadCards(self):
-        """Load all the cards in memory"""
-        def _getTarotCardsPathsCb(paths):
-            log.debug("_getTarotCardsPathsCb")
-            for file_ in paths:
-                log.debug("path: %s" % file_)
-                card = CardWidget(self, file_)
-                log.debug("card: %s" % card)
-                self.cards[(card.suit, card.value)] = card
-                self.deck.append(card)
-            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
-        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
-        self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb)
-
-    def onClick(self, sender):
-        if self.state == "chien":
-            self.to_show = []
-            self.state = "wait"
-            self.updateToShow()
-        elif self.state == "wait_for_ecart":
-            self.state = "ecart"
-            self.hand.extend(self.to_show)
-            self.hand.sort()
-            self.to_show = []
-            self.updateToShow()
-            self.updateHand()
-
-    def tarotGameNew(self, hand):
-        """Start a new game, with given hand"""
-        if hand is []:  # reset the display after the scores have been showed
-            self.selected.clear()
-            del self.hand[:]
-            del self.to_show[:]
-            self.state = None
-            #empty hand
-            self.updateHand()
-            #nothing on the table
-            self.updateToShow()
-            for pos in ['top', 'left', 'bottom', 'right']:
-                getattr(self, "inner_%s" % pos).setWidget(None)
-            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
-            return
-        for suit, value in hand:
-            self.hand.append(self.cards[(suit, value)])
-        self.hand.sort()
-        self.state = "init"
-        self.updateHand()
-
-    def updateHand(self):
-        """Show the cards in the hand in the hand_panel (SOUTH panel)"""
-        self.hand_panel.clear()
-        self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10))
-        x_pos = 0
-        y_pos = CARD_DELTA_Y
-        for card in self.hand:
-            self.hand_panel.add(card, x_pos, y_pos)
-            x_pos += self.visible_size
-
-    def updateToShow(self):
-        """Show cards in the center panel"""
-        if not self.to_show:
-            _widget = self.inner_center.getWidget()
-            if _widget:
-                self.inner_center.remove(_widget)
-            return
-        panel = AbsolutePanel()
-        panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT))
-        x_pos = 0
-        y_pos = 0
-        for card in self.to_show:
-            panel.add(card, x_pos, y_pos)
-            x_pos += CARD_WIDTH + 5
-        self.inner_center.setWidget(panel)
-
-    def _ecartConfirm(self, confirm):
-        if not confirm:
-            return
-        ecart = []
-        for card in self.selected:
-            ecart.append((card.suit, card.value))
-            self.hand.remove(card)
-        self.selected.clear()
-        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart)
-        self.state = "wait"
-        self.updateHand()
-
-    def addToSelection(self, card):
-        self.selected.add(card)
-        if len(self.selected) == 6:
-            dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
-
-    def tarotGameInvalidCards(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")  # FIXME: raise an exception here
-
-        for suit, value in played_cards:
-            self.hand.append(self.cards[(suit, value)])
-
-        self.hand.sort()
-        self.updateHand()
-        if self._autoplay == None:  # No dialog if there is autoplay
-            Window.alert('Cards played are invalid !')
-        self.__fakePlay()
-
-    def removeFromSelection(self, card):
-        self.selected.remove(card)
-        if len(self.selected) == 6:
-            dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
-
-    def tarotGameChooseContrat(self, xml_data):
-        """Called when the player has to select his contrat
-        @param xml_data: SàT xml representation of the form"""
-        body = xmlui.create(self._parent.host, xml_data, flags=['NO_CANCEL'])
-        _dialog = dialog.GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.show()
-
-    def tarotGameShowCards(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)])
-        self.updateToShow()
-        if game_stage == "chien" and data['attaquant'] == self.player_nick:
-            self.state = "wait_for_ecart"
-        else:
-            self.state = "chien"
-
-    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
-        log.error("This line should not be reached")
-
-    def tarotGameCardsPlayed(self, player, cards):
-        """A card has been played by player"""
-        if not len(cards):
-            log.warning("cards should not be empty")
-            return
-        if len(cards) > 1:
-            log.error("can't manage several cards played")
-        if self.to_show:
-            self.to_show = []
-            self.updateToShow()
-        suit, value = cards[0]
-        player_pos = self.getPlayerLocation(player)
-        player_panel = getattr(self, "inner_%s" % player_pos)
-
-        if player_panel.getWidget() != None:
-            #We have already cards on the table, we remove them
-            for pos in ['top', 'left', 'bottom', 'right']:
-                getattr(self, "inner_%s" % pos).setWidget(None)
-
-        card = self.cards[(suit, value)]
-        DOM.setElemAttribute(card.getElement(), "style", "")
-        player_panel.setWidget(card)
-
-    def tarotGameYourTurn(self):
-        """Called when we have to play :)"""
-        if self.state == "chien":
-            self.to_show = []
-            self.updateToShow()
-        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.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
-        del self.hand[self._autoplay]
-        self.state = "wait"
-        self._autoplay += 1
-
-    def playCard(self, card):
-        self.hand.remove(card)
-        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
-        self.state = "wait"
-        self.updateHand()
-
-    def tarotGameScore(self, xml_data, winners, loosers):
-        """Show score at the end of a round"""
-        if not winners and not loosers:
-            title = "Draw game"
-        else:
-            if self.player_nick in winners:
-                title = "You <b>win</b> !"
-            else:
-                title = "You <b>loose</b> :("
-        body = xmlui.create(self._parent.host, xml_data, title=title, flags=['NO_CANCEL'])
-        _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/chat.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,347 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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.games import SYMBOLS
+from sat_frontends.tools import strings
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus
+from sat_frontends.quick_frontend.quick_chat import QuickChat
+from sat.core.i18n import _
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas import DOM
+
+from datetime import datetime
+from time import time
+
+import html_tools
+import libervia_widget
+import base_panel
+import contact_panel
+import editor_widget
+import contact_list
+from constants import Const as C
+import plugin_xep_0085
+import game_tarot
+import game_radiocol
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+class ChatText(HTMLPanel):
+
+    def __init__(self, nick, mymess, msg, extra):
+        try:
+            timestamp = float(extra['timestamp'])
+        except KeyError:
+            timestamp=None
+        xhtml = extra.get('xhtml')
+        _date = datetime.fromtimestamp(float(timestamp or time()))
+        _msg_class = ["chat_text_msg"]
+        if mymess:
+            _msg_class.append("chat_text_mymess")
+        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
+                           {"timestamp": _date.strftime("%H:%M"),
+                            "nick": "[%s]" % html_tools.html_sanitize(nick),
+                            "msg_class": ' '.join(_msg_class),
+                            "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
+                           )
+        self.setStyleName('chatText')
+
+
+class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+        """Panel used for conversation (one 2 one or group chat)
+
+        @param host: SatWebFrontend instance
+        @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
+        @param type: one2one for simple conversation, group for MUC
+        """
+        QuickChat.__init__(self, host, target, type_, profiles=profiles)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+
+        # FIXME: temporary dirty initialization to display the OTR state
+        header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
+
+        libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True)
+        self._body = AbsolutePanel()
+        self._body.setStyleName('chatPanel_body')
+        chat_area = HorizontalPanel()
+        chat_area.setStyleName('chatArea')
+        if type_ == C.CHAT_GROUP:
+            self.occupants_panel = contact_panel.ContactsPanel(host, merge_resources=False,
+                                                               contacts_style="muc_contact",
+                                                               contacts_menus=(C.MENU_JID_CONTEXT),
+                                                               contacts_display=('resource',))
+            chat_area.add(self.occupants_panel)
+            DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell")
+        self._body.add(chat_area)
+        self.content = AbsolutePanel()
+        self.content.setStyleName('chatContent')
+        self.content_scroll = base_panel.ScrollPanelWrapper(self.content)
+        chat_area.add(self.content_scroll)
+        chat_area.setCellWidth(self.content_scroll, '100%')
+        self.vpanel.add(self._body)
+        self.vpanel.setCellHeight(self._body, '100%')
+        self.addStyleName('chatPanel')
+        self.setWidget(self.vpanel)
+        self.state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target))
+        self._state = None
+        self.refresh()
+        if type_ == C.CHAT_ONE2ONE:
+            self.historyPrint(profile=self.profile)
+
+    @property
+    def target(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    @property
+    def profile(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
+        return list(self.profiles)[0]
+
+    @property
+    def plugin_menu_context(self):
+        return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,)
+
+    # @classmethod
+    # def createPanel(cls, host, item, type_=C.CHAT_ONE2ONE):
+    #     assert(item)
+    #     _contact = item if isinstance(item, jid.JID) else jid.JID(item)
+    #     host.contact_panel.setContactMessageWaiting(_contact.bare, False)
+    #     _new_panel = Chat(host, _contact, type_)  # XXX: pyjamas doesn't seems to support creating with cls directly
+    #     _new_panel.historyPrint()
+    #     host.setSelected(_new_panel)
+    #     _new_panel.refresh()
+    #     return _new_panel
+
+    def refresh(self):
+        """Refresh the display of this widget. If the unibox is disabled,
+        add a message box at the bottom of the panel"""
+        # FIXME: must be checked
+        # self.host.contact_panel.setContactMessageWaiting(self.target.bare, False)
+        # self.content_scroll.scrollToBottom()
+
+        enable_box = self.host.uni_box is None
+        if hasattr(self, 'message_box'):
+            self.message_box.setVisible(enable_box)
+        elif enable_box:
+            self.message_box = editor_widget.MessageBox(self.host)
+            self.message_box.onSelectedChange(self)
+            self.message_box.addKeyboardListener(self)
+            self.vpanel.add(self.message_box)
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        if keycode == KEY_ENTER:
+            self.host.showWarning(None, None)
+        else:
+            self.host.showWarning(*self.getWarningData())
+
+    def getWarningData(self):
+        if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]:
+            raise Exception("Unmanaged type !")
+        if self.type == C.CHAT_ONE2ONE:
+            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
+        elif self.type == C.CHAT_GROUP:
+            msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
+        return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg)
+
+    def onTextEntered(self, text):
+        self.host.sendMessage(self.target,
+                              text,
+                              mess_type=C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+                              errback=self.host.sendError,
+                              profile_key=C.PROF_KEY_NONE
+                              )
+        self.state_machine._onEvent("active")
+
+    def onQuit(self):
+        libervia_widget.LiberviaWidget.onQuit(self)
+        if self.type == C.CHAT_GROUP:
+            self.host.bridge.call('mucLeave', None, unicode(self.target.bare))
+
+    def setUserNick(self, nick):
+        """Set the nick of the user, usefull for e.g. change the color of the user"""
+        self.nick = nick
+
+    def setPresents(self, nicks):
+        """Set the occupants of a group chat.
+
+        @param nicks (list[unicode]): sorted list of nicknames
+        """
+        QuickChat.setPresents(self, nicks)
+        self.occupants_panel.setList([jid.JID(u"%s/%s" % (self.target, nick)) for nick in nicks])
+
+    def replaceUser(self, nick, show_info=True):
+        """Add user if it is not in the group list"""
+        QuickChat.replaceUser(self, nick, show_info)
+        occupant_jid = jid.JID("%s/%s" % (unicode(self.target), nick))
+        self.occupants_panel.addContact(occupant_jid)
+
+    def removeUser(self, nick, show_info=True):
+        """Remove a user from the group list"""
+        QuickChat.removeUser(self, nick, show_info)
+        occupant_jid = jid.JID("%s/%s" % (unicode(self.target), nick))
+        self.occupants_panel.removeContact(occupant_jid)
+
+    def changeUserNick(self, old_nick, new_nick):
+        assert self.type == C.CHAT_GROUP
+        # FIXME
+        # self.occupants_panel.removeOccupant(old_nick)
+        # self.occupants_panel.addOccupant(new_nick)
+        self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick})
+
+    # def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT):
+    #     """Print the initial history"""
+    #     def getHistoryCB(history):
+    #         # display day change
+    #         day_format = "%A, %d %b %Y"
+    #         previous_day = datetime.now().strftime(day_format)
+    #         for line in history:
+    #             timestamp, from_jid_s, to_jid_s, message, mess_type, extra = line
+    #             message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
+    #             if previous_day != message_day:
+    #                 self.printInfo("* " + message_day)
+    #                 previous_day = message_day
+    #             self.printMessage(jid.JID(from_jid_s), message, extra, timestamp)
+    #     self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
+
+    def printInfo(self, msg, type_='normal', extra=None, link_cb=None):
+        """Print general info
+        @param msg: message to print
+        @param type_: one of:
+            "normal": general info like "toto has joined the room" (will be sanitized)
+            "link": general info that is clickable like "click here to join the main room" (no sanitize done)
+            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" (will stay on one line)
+        @param extra (dict): message data
+        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
+        """
+        if extra is None:
+            extra = {}
+        if type_ == 'normal':
+            _wid = HTML(strings.addURLToText(html_tools.XHTML2Text(msg)))
+            _wid.setStyleName('chatTextInfo')
+        elif type_ == 'link':
+            _wid = HTML(msg)
+            _wid.setStyleName('chatTextInfo-link')
+            if link_cb:
+                _wid.addClickListener(link_cb)
+        elif type_ == 'me':
+            _wid = Label(msg)
+            _wid.setStyleName('chatTextMe')
+        else:
+            raise ValueError("Unknown printInfo type %s" % type_)
+        self.content.add(_wid)
+        self.content_scroll.scrollToBottom()
+
+    def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
+        if extra is None:
+            extra = {}
+        try:
+            nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
+        except TypeError:
+            # None is returned, the message is managed
+            return
+        self.content.add(ChatText(nick, mymess, msg, extra))
+        self.content_scroll.scrollToBottom()
+
+    def setState(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: ignored for one2one, otherwise the MUC user nick or C.ALL_OCCUPANTS
+        """
+        return # FIXME
+        if self.type == C.CHAT_GROUP:
+            assert(nick)
+            if nick == C.ALL_OCCUPANTS:
+                occupants = self.occupants_panel.occupants_panel.keys()
+            else:
+                occupants = [nick] if nick in self.occupants_panel.occupants_panel else []
+            for occupant in occupants:
+                self.occupants_panel.occupants_panel[occupant].setState(state)
+        else:
+            self._state = state
+            self.refreshTitle()
+        self.state_machine.started = not not state  # start to send "composing" state from now
+
+    def refreshTitle(self):
+        """Refresh the title of this Chat dialog"""
+        title = unicode(self.target.bare)
+        if self._state:
+            title += " (%s)".format(self._state)
+        self.setTitle(title)
+
+    def setConnected(self, jid_s, resource, availability, priority, statuses):
+        """Set connection status
+        @param jid_s (unicode): JID userhost as unicode
+        """
+        raise Exception("should not be there") # FIXME
+        assert(jid_s == self.target.bare)
+        if self.type != C.CHAT_GROUP:
+            return
+        box = self.occupants_panel.getOccupantBox(resource)
+        if box:
+            contact_list.setPresenceStyle(box, availability)
+
+    def updateChatState(self, from_jid, state):
+        #TODO
+        pass
+
+    def addGamePanel(self, widget):
+        """Insert a game panel to this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.insert(widget, 0)
+        self.vpanel.setCellHeight(widget, widget.getHeight())
+
+    def removeGamePanel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.remove(widget)
+
+
+quick_widgets.register(QuickChat, Chat)
+quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel)
+quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel)
+libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'})
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})
--- a/src/browser/sat_browser/constants.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/constants.py	Wed Mar 18 16:15:18 2015 +0100
@@ -35,5 +35,7 @@
                      ('General', C.SHOW_EMPTY_GROUPS),
                      ]
 
-    # Empty avatar
-    EMPTY_AVATAR = "/media/misc/empty_avatar"
+    WEB_PANEL_DEFAULT_URL = "http://salut-a-toi.org"
+    WEB_PANEL_SCHEMES = {'http', 'https', 'ftp', 'file'}
+
+    CONTACT_DEFAULT_DISPLAY=('bare', 'nick')
--- a/src/browser/sat_browser/contact.py	Thu Feb 05 12:05:32 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,538 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 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/>.
-
-import pyjd  # this is dummy in pyjs
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.ScrollPanel import ScrollPanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Image import Image
-from pyjamas import Window
-from pyjamas import DOM
-from __pyjamas__ import doc
-
-from constants import Const as C
-from jid import JID
-import base_widget
-import panels
-import html_tools
-
-
-def buildPresenceStyle(presence, base_style=None):
-    """Return the CSS classname to be used for displaying the given presence information.
-    @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
-    @param base_style (str): base classname
-    @return: str
-    """
-    if not base_style:
-        base_style = "contactLabel"
-    return '%s-%s' % (base_style, presence or 'connected')
-
-
-def setPresenceStyle(widget, presence, base_style=None):
-    """
-    Set the CSS style of a contact's element according to its presence.
-
-    @param widget (Widget): the UI element of the contact
-    @param presence (str): a value in ("", "chat", "away", "dnd", "xa").
-    @param base_style (str): the base name of the style to apply
-    """
-    if not hasattr(widget, 'presence_style'):
-        widget.presence_style = None
-    style = buildPresenceStyle(presence, base_style)
-    if style == widget.presence_style:
-        return
-    if widget.presence_style is not None:
-        widget.removeStyleName(widget.presence_style)
-    widget.addStyleName(style)
-    widget.presence_style = style
-
-
-class GroupLabel(base_widget.DragLabel, Label, ClickHandler):
-    def __init__(self, host, group):
-        self.group = group
-        self.host = host
-        Label.__init__(self, group)  # , Element=DOM.createElement('div')
-        self.setStyleName('group')
-        base_widget.DragLabel.__init__(self, group, "GROUP")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group})
-
-
-class ContactLabel(HTML):
-    def __init__(self, jid, name=None):
-        HTML.__init__(self)
-        self.name = name or jid
-        self.waiting = False
-        self.refresh()
-        self.setStyleName('contactLabel')
-
-    def refresh(self):
-        if self.waiting:
-            wait_html = "<b>(*)</b>&nbsp;"
-        self.setHTML("%(wait)s%(name)s" % {'wait': wait_html,
-                                           'name': html_tools.html_sanitize(self.name)})
-
-    def setMessageWaiting(self, waiting):
-        """Show a visual indicator if message are waiting
-
-        @param waiting: True if message are waiting"""
-        self.waiting = waiting
-        self.refresh()
-
-
-class ContactMenuBar(base_widget.WidgetMenuBar):
-
-    def onBrowserEvent(self, event):
-        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
-        event.stopPropagation()  # prevent opening the chat dialog
-
-    @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return '<img src="%s"/>' % C.DEFAULT_AVATAR
-
-    def setUrl(self, url):
-        """Set the URL of the contact avatar."""
-        self.items[0].setHTML('<img src="%s" />' % url)
-
-
-class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel):
-
-    def __init__(self, host, jid, name=None, click_listener=None, handle_menu=None):
-        VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
-        base_widget.DragLabel.__init__(self, jid, "CONTACT")
-        self.host = host
-        self.jid = jid
-        self.label = ContactLabel(jid, name)
-        self.avatar = ContactMenuBar(self, host) if handle_menu else Image()
-        self.updateAvatar(host.getAvatar(jid))
-        self.add(self.avatar)
-        self.add(self.label)
-        if click_listener:
-            ClickHandler.__init__(self)
-            self.addClickListener(self)
-            self.click_listener = click_listener
-
-    def addMenus(self, menu_bar):
-        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid})
-        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid})
-
-    def setMessageWaiting(self, waiting):
-        """Show a visual indicator if message are waiting
-
-        @param waiting: True if message are waiting"""
-        self.label.setMessageWaiting(waiting)
-
-    def updateAvatar(self, url):
-        """Update the avatar.
-
-        @param url (str): image url
-        """
-        self.avatar.setUrl(url)
-
-    def onClick(self, sender):
-        self.click_listener(self.jid)
-
-
-class GroupList(VerticalPanel):
-
-    def __init__(self, parent):
-        VerticalPanel.__init__(self)
-        self.setStyleName('groupList')
-        self._parent = parent
-
-    def add(self, group):
-        _item = GroupLabel(self._parent.host, group)
-        _item.addMouseListener(self._parent)
-        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
-        index = 0
-        for group_ in [child.group for child in self.getChildren()]:
-            if group_ > group:
-                break
-            index += 1
-        VerticalPanel.insert(self, _item, index)
-
-    def remove(self, group):
-        for wid in self:
-            if isinstance(wid, GroupLabel) and wid.group == group:
-                VerticalPanel.remove(self, wid)
-                return
-
-    def getGroupBox(self, group):
-        """get the widget of a group
-
-        @param group (str): the group
-        @return: GroupLabel instance if present, else None"""
-        for wid in self:
-            if isinstance(wid, GroupLabel) and wid.group == group:
-                return wid
-        return None
-
-
-class GenericContactList(VerticalPanel):
-    """Class that can be used to represent a contact list, but not necessarily
-    the one that is displayed on the left side. Special features like popup menu
-    panel or changing the contact states must be done in a sub-class."""
-
-    def __init__(self, host, handle_click=False, handle_menu=False):
-        VerticalPanel.__init__(self)
-        self.host = host
-        self.contacts = []
-        self.click_listener = None
-        self.handle_menu = handle_menu
-
-        if handle_click:
-            def cb(contact_jid):
-                self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': contact_jid})
-            self.click_listener = cb
-
-    def add(self, jid_s, name=None):
-        """Add a contact to the list.
-
-        @param jid (str): JID of the contact
-        @param name (str): optional name of the contact
-        """
-        assert(isinstance(jid_s, str))
-        if jid_s in self.contacts:
-            return
-        index = 0
-        for contact_ in self.contacts:
-            if contact_ > jid_s:
-                break
-            index += 1
-        self.contacts.insert(index, jid_s)
-        box = ContactBox(self.host, jid_s, name, self.click_listener, self.handle_menu)
-        VerticalPanel.insert(self, box, index)
-
-    def remove(self, jid_s):
-        box = self.getContactBox(jid_s)
-        if not box:
-            return
-        VerticalPanel.remove(self, box)
-        self.contacts.remove(jid_s)
-
-    def isContactPresent(self, contact_jid):
-        """Return True if a contact is present in the panel"""
-        return contact_jid in self.contacts
-
-    def getContacts(self):
-        return self.contacts
-
-    def getContactBox(self, contact_jid_s):
-        """get the widget of a contact
-
-        @param contact_jid_s (str): the contact
-        @return: ContactBox instance if present, else None"""
-        for wid in self:
-            if isinstance(wid, ContactBox) and wid.jid == contact_jid_s:
-                return wid
-        return None
-
-    def updateAvatar(self, jid_s, url):
-        """Update the avatar of the given contact
-
-        @param jid_s (str): contact jid
-        @param url (str): image url
-        """
-        try:
-            self.getContactBox(jid_s).updateAvatar(url)
-        except TypeError:
-            pass
-
-
-class ContactList(GenericContactList):
-    """The contact list that is displayed on the left side."""
-
-    def __init__(self, host):
-        GenericContactList.__init__(self, host, handle_click=True, handle_menu=True)
-
-    def setState(self, jid, type_, state):
-        """Change the appearance of the contact, according to the state
-        @param jid: jid which need to change state
-        @param type_: one of availability, messageWaiting
-        @param state:
-            - for messageWaiting type:
-                True if message are waiting
-            - for availability type:
-                'unavailable' if not connected, else presence like RFC6121 #4.7.2.1"""
-        contact_box = self.getContactBox(jid)
-        if contact_box:
-            if type_ == 'availability':
-                setPresenceStyle(contact_box.label, state)
-            elif type_ == 'messageWaiting':
-                contact_box.setMessageWaiting(state)
-
-
-class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler):
-    def __init__(self, host, text):
-        Label.__init__(self, text)  # , Element=DOM.createElement('div')
-        self.host = host
-        self.setStyleName('contactTitle')
-        base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None})
-
-
-class ContactPanel(SimplePanel):
-    """Manage the contacts and groups"""
-
-    def __init__(self, host):
-        SimplePanel.__init__(self)
-
-        self.scroll_panel = ScrollPanel()
-
-        self.host = host
-        self.groups = {}
-        self.connected = {}  # jid connected as key and their status
-
-        self.vPanel = VerticalPanel()
-        _title = ContactTitleLabel(host, 'Contacts')
-        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
-
-        self._contact_list = ContactList(host)
-        self._contact_list.setStyleName('contactList')
-        self._groupList = GroupList(self)
-        self._groupList.setStyleName('groupList')
-
-        self.vPanel.add(_title)
-        self.vPanel.add(self._groupList)
-        self.vPanel.add(self._contact_list)
-        self.scroll_panel.add(self.vPanel)
-        self.add(self.scroll_panel)
-        self.setStyleName('contactPanel')
-        Window.addWindowResizeListener(self)
-
-    def onWindowResized(self, width, height):
-        contact_panel_elt = self.getElement()
-        classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else'gwt-TabBar'
-        _elts = doc().getElementsByClassName(classname)
-        if not _elts.length:
-            log.error("no element of class %s found, it should exist !" % classname)
-            tab_bar_h = height
-        else:
-            tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height  # getAbsoluteTop can be 0 if tabBar is hidden
-
-        ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5
-        self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))
-
-    def updateContact(self, jid_s, attributes, groups):
-        """Add a contact to the panel if it doesn't exist, update it else
-        @param jid_s: jid userhost as unicode
-        @param attributes: cf SàT Bridge API's newContact
-        @param groups: list of groups"""
-        _current_groups = self.getContactGroups(jid_s)
-        _new_groups = set(groups)
-        _key = "@%s: "
-
-        for group in _current_groups.difference(_new_groups):
-            # We remove the contact from the groups where he isn't anymore
-            self.groups[group].remove(jid_s)
-            if not self.groups[group]:
-                # The group is now empty, we must remove it
-                del self.groups[group]
-                self._groupList.remove(group)
-                if self.host.uni_box:
-                    self.host.uni_box.removeKey(_key % group)
-
-        for group in _new_groups.difference(_current_groups):
-            # We add the contact to the groups he joined
-            if group not in self.groups.keys():
-                self.groups[group] = set()
-                self._groupList.add(group)
-                if self.host.uni_box:
-                    self.host.uni_box.addKey(_key % group)
-            self.groups[group].add(jid_s)
-
-        # We add the contact to contact list, it will check if contact already exists
-        self._contact_list.add(jid_s)
-        self.updateVisibility([jid_s], self.getContactGroups(jid_s))
-
-    def removeContact(self, jid):
-        """Remove contacts from groups where he is and contact list"""
-        self.updateContact(jid, {}, [])  # we remove contact from every group
-        self._contact_list.remove(jid)
-
-    def setConnected(self, jid_s, resource, availability, priority, statuses):
-        """Set connection status
-        @param jid_s (str): JID userhost as unicode
-        """
-        if availability == 'unavailable':
-            if jid_s in self.connected:
-                if resource in self.connected[jid_s]:
-                    del self.connected[jid_s][resource]
-                if not self.connected[jid_s]:
-                    del self.connected[jid_s]
-        else:
-            if jid_s not in self.connected:
-                self.connected[jid_s] = {}
-            self.connected[jid_s][resource] = (availability, priority, statuses)
-
-        # check if the contact is connected with another resource, use the one with highest priority
-        if jid_s in self.connected:
-            max_resource = max_priority = None
-            for tmp_resource in self.connected[jid_s]:
-                if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
-                    max_resource = tmp_resource
-                    max_priority = self.connected[jid_s][tmp_resource][1]
-            if availability == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
-                priority = max_priority
-                availability = self.connected[jid_s][max_resource][0]
-        if jid_s not in self.connected or priority >= max_priority:
-            # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
-            # case 2: update (or confirm) with the values of the resource which takes precedence
-            self._contact_list.setState(jid_s, "availability", availability)
-
-        # update the connected contacts chooser live
-        if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None:
-            self.host.room_contacts_chooser.resetContacts()
-
-        self.updateVisibility([jid_s], self.getContactGroups(jid_s))
-
-    def setContactMessageWaiting(self, jid, waiting):
-        """Show an visual indicator that contact has send a message
-        @param jid: jid of the contact
-        @param waiting: True if message are waiting"""
-        self._contact_list.setState(jid, "messageWaiting", waiting)
-
-    def getConnected(self, filter_muc=False):
-        """return a list of all jid (bare jid) connected
-        @param filter_muc: if True, remove the groups from the list
-        """
-        contacts = self.connected.keys()
-        contacts.sort()
-        return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts())))
-
-    def getContactGroups(self, contact_jid_s):
-        """Get groups where contact is
-       @param group: string of single group, or list of string
-       @param contact_jid_s: jid to test, as unicode
-        """
-        result = set()
-        for group in self.groups:
-            if self.isContactInGroup(group, contact_jid_s):
-                result.add(group)
-        return result
-
-    def isContactInGroup(self, group, contact_jid):
-        """Test if the contact_jid is in the group
-        @param group: string of single group, or list of string
-        @param contact_jid: jid to test
-        @return: True if contact_jid is in on of the groups"""
-        if group in self.groups and contact_jid in self.groups[group]:
-            return True
-        return False
-
-    def isContactInRoster(self, contact_jid):
-        """Test if the contact is in our roster list"""
-        for contact_box in self._contact_list:
-            if contact_jid == contact_box.jid:
-                return True
-        return False
-
-    def getContacts(self):
-        return self._contact_list.getContacts()
-
-    def getGroups(self):
-        return self.groups.keys()
-
-    def onMouseMove(self, sender, x, y):
-        pass
-
-    def onMouseDown(self, sender, x, y):
-        pass
-
-    def onMouseUp(self, sender, x, y):
-        pass
-
-    def onMouseEnter(self, sender):
-        if isinstance(sender, GroupLabel):
-            for contact in self._contact_list:
-                if contact.jid in self.groups[sender.group]:
-                    contact.label.addStyleName("selected")
-
-    def onMouseLeave(self, sender):
-        if isinstance(sender, GroupLabel):
-            for contact in self._contact_list:
-                if contact.jid in self.groups[sender.group]:
-                    contact.label.removeStyleName("selected")
-
-    def updateAvatar(self, jid_s, url):
-        """Update the avatar of the given contact
-
-        @param jid_s (str): contact jid
-        @param url (str): image url
-        """
-        self._contact_list.updateAvatar(jid_s, url)
-
-    def hasVisibleMembers(self, group):
-        """Tell if the given group actually has visible members
-
-        @param group (str): the group to check
-        @return: boolean
-        """
-        for jid in self.groups[group]:
-            if self._contact_list.getContactBox(jid).isVisible():
-                return True
-        return False
-
-    def offlineContactsToShow(self):
-        """Tell if offline contacts should be visible according to the user settings
-
-        @return: boolean
-        """
-        return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true'
-
-    def emtyGroupsToShow(self):
-        """Tell if empty groups should be visible according to the user settings
-
-        @return: boolean
-        """
-        return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true'
-
-    def updateVisibility(self, jids, groups):
-        """Set the widgets visibility for the given contacts and groups
-
-        @param jids (list[str]): list of JID
-        @param groups (list[str]): list of groups
-        """
-        for jid_s in jids:
-            try:
-                self._contact_list.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
-            except TypeError:
-                log.warning('No box for contact %s: this code line should not be reached' % jid_s)
-        for group in groups:
-            try:
-                self._groupList.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
-            except TypeError:
-                log.warning('No box for group %s: this code line should not be reached' % group)
-
-    def refresh(self):
-        """Show or hide disconnected contacts and empty groups"""
-        self.updateVisibility(self._contact_list.contacts, self.groups.keys())
--- a/src/browser/sat_browser/contact_group.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/contact_group.py	Wed Mar 18 16:15:18 2015 +0100
@@ -28,14 +28,28 @@
 
 import dialog
 import list_manager
-import contact
+import contact_panel
+import contact_list
+
+
+unicode = str  # FIXME: pyjamas workaround
 
 
 class ContactGroupManager(list_manager.ListManager):
-    """A manager for sub-panels to assign contacts to each group."""
 
-    def __init__(self, parent, keys_dict, contact_list, offsets, style):
-        list_manager.ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style)
+    def __init__(self, container, keys, contacts, offsets, style):
+        """
+        @param container (FlexTable): FlexTable parent widget
+        @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items
+            keys to their display config data.
+        @param contacts (list): list of contacts
+        @param offsets (dict): define widgets positions offsets within container:
+            - "x_first": the x offset for the first widget's row on the grid
+            - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
+            - "y": the y offset for all widgets columns on the grid
+        @param style (dict): define CSS styles
+        """
+        list_manager.ListManager.__init__(self, container, keys, contacts, offsets, style)
         self.registerPopupMenuPanel(entries={"Remove group": {}},
                                     callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
 
@@ -44,43 +58,51 @@
 
         def confirm_cb(answer):
             if answer:
-                list_manager.ListManager.removeContactKey(self, key)
-                self._parent.removeKeyFromAddGroupPanel(key)
+                list_manager.ListManager.removeItemKey(self, key)
+                self.container.removeKeyFromAddGroupPanel(key)
 
         _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key)
         _dialog.show()
 
     def removeFromRemainingList(self, contacts):
         list_manager.ListManager.removeFromRemainingList(self, contacts)
-        self._parent.updateContactList(contacts=contacts)
+        self.container.updateContactList(contacts)
 
     def addToRemainingList(self, contacts, ignore_key=None):
         list_manager.ListManager.addToRemainingList(self, contacts, ignore_key)
-        self._parent.updateContactList(contacts=contacts)
+        self.container.updateContactList(contacts)
 
 
 class ContactGroupEditor(DockPanel):
-    """Panel for the contact groups manager."""
+    """A big panel including a ContactGroupManager and other UI stuff."""
+
+    def __init__(self, host, container=None, onCloseCallback=None):
+        """
 
-    def __init__(self, host, parent=None, onCloseCallback=None):
+        @param host (SatWebFrontend)
+        @param container (PanelBase): parent panel or None to display in a popup
+        @param onCloseCallback (callable)
+        """
         DockPanel.__init__(self)
         self.host = host
 
         # eventually display in a popup
-        if parent is None:
-            parent = DialogBox(autoHide=False, centered=True)
-            parent.setHTML("Manage contact groups")
-        self._parent = parent
+        if container is None:
+            container = DialogBox(autoHide=False, centered=True)
+            container.setHTML("Manage contact groups")
+        self.container = container
         self._on_close_callback = onCloseCallback
-        self.all_contacts = self.host.contact_panel.getContacts()
 
-        groups_list = self.host.contact_panel.groups.keys()
-        groups_list.sort()
+        self.all_contacts = contact_list.JIDList(self.host.contact_list.roster_entities)
+        roster_entities_by_group = self.host.contact_list.roster_entities_by_group
+        del roster_entities_by_group[None]  # remove the empty group
+        roster_groups = roster_entities_by_group.keys()
+        roster_groups.sort()
 
-        self.add_group_panel = self.getAddGroupPanel(groups_list)
-        south_panel = self.getCloseSaveButtons()
-        center_panel = self.getContactGroupManager(groups_list)
-        east_panel = self.getContactList()
+        self.add_group_panel = self.initAddGroupPanel(roster_groups)
+        south_panel = self.initCloseSaveButtons()
+        center_panel = self.initContactGroupManager(roster_groups)
+        east_panel = self.initContactList()
 
         self.add(self.add_group_panel, DockPanel.CENTER)
         self.add(east_panel, DockPanel.EAST)
@@ -97,108 +119,123 @@
         self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER)
 
         # need to be done after the contact list has been initialized
-        self.groups.setContacts(self.host.contact_panel.groups)
+        self.groups.resetItems(roster_entities_by_group)
         self.toggleContacts(showAll=True)
 
         # Hide the contacts list from the main panel to not confuse the user
         self.restore_contact_panel = False
-        if self.host.contact_panel.getVisible():
+        clist = self.host.contact_list
+        if clist.getVisible():
             self.restore_contact_panel = True
             self.host.panel._contactsSwitch()
 
-        parent.add(self)
-        parent.setVisible(True)
-        if isinstance(parent, DialogBox):
-            parent.center()
+        container.add(self)
+        container.setVisible(True)
+        if isinstance(container, DialogBox):
+            container.center()
 
-    def getContactGroupManager(self, groups_list):
-        """Set the list manager for the groups"""
-        flex_table = FlexTable(len(groups_list), 2)
+    def initContactGroupManager(self, groups):
+        """Initialise the contact group manager.
+
+        @param groups (list[unicode]): contact groups
+        """
+        flex_table = FlexTable()
         flex_table.addStyleName('contactGroupEditor')
+
         # overwrite the default style which has been set for rich text editor
-        style = {
-           "keyItem": "group",
-           "popupMenuItem": "popupMenuItem",
-           "removeButton": "contactGroupRemoveButton",
-           "buttonCell": "contactGroupButtonCell",
-           "keyPanel": "contactGroupPanel"
-        }
-        self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style)
-        self.groups.createWidgets()  # widgets are automatically added to FlexTable
+        style = {"keyItem": "group",
+                 "popupMenuItem": "popupMenuItem",
+                 "removeButton": "contactGroupRemoveButton",
+                 "buttonCell": "contactGroupButtonCell",
+                 "keyPanel": "contactGroupPanel"
+                 }
+
+        groups = {group: {} for group in groups}
+        self.groups = ContactGroupManager(flex_table, groups, self.all_contacts, style=style)
+        self.groups.createWidgets()  # widgets are automatically added to the FlexTable
+
         # FIXME: clean that part which is dangerous
         flex_table.updateContactList = self.updateContactList
         flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove
+
         return flex_table
 
-    def getAddGroupPanel(self, groups_list):
-        """Add the 'Add group' panel to the FlexTable"""
+    def initAddGroupPanel(self, groups):
+        """Initialise the 'Add group' panel.
 
-        def add_group_cb(text):
-            self.groups.addContactKey(text)
+        @param groups (list[unicode]): contact groups
+        """
+
+        def add_group_cb(key):
+            self.groups.addItemKey(key)
             self.add_group_panel.textbox.setFocus(True)
 
-        add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb)
+        add_group_panel = dialog.AddGroupPanel(groups, add_group_cb)
         add_group_panel.addStyleName("addContactGroupPanel")
         return add_group_panel
 
-    def getCloseSaveButtons(self):
-        """Add the buttons to close the dialog / save the groups"""
+    def initCloseSaveButtons(self):
+        """Add the buttons to close the dialog and save the groups."""
         buttons = HorizontalPanel()
         buttons.addStyleName("marginAuto")
         buttons.add(Button("Save", listener=self.closeAndSave))
         buttons.add(Button("Cancel", listener=self.cancelWithoutSaving))
         return buttons
 
-    def getContactList(self):
-        """Add the contact list to the DockPanel"""
+    def initContactList(self):
+        """Add the contact list to the DockPanel."""
         self.toggle = Button("", self.toggleContacts)
         self.toggle.addStyleName("toggleAssignedContacts")
-        self.contacts = contact.GenericContactList(self.host)
-        for contact_ in self.all_contacts:
-            self.contacts.add(contact_)
-        contact_panel = VerticalPanel()
-        contact_panel.add(self.toggle)
-        contact_panel.add(self.contacts)
-        return contact_panel
+        self.contacts = contact_panel.ContactsPanel(self.host)
+        for contact in self.all_contacts:
+            self.contacts.add(contact)
+        panel = VerticalPanel()
+        panel.add(self.toggle)
+        panel.add(self.contacts)
+        return panel
 
     def toggleContacts(self, sender=None, showAll=None):
-        """Callback for the toggle button"""
-        if sender is None:
-            sender = self.toggle
-        sender.showAll = showAll if showAll is not None else not sender.showAll
-        if sender.showAll:
-            sender.setText("Hide assigned")
-        else:
-            sender.setText("Show assigned")
-        self.updateContactList(sender)
+        """Toggle the button to show contacts and the contact list.
 
-    def updateContactList(self, sender=None, contacts=None):
-        """Update the contact list regarding the toggle button"""
+        @param sender (Button)
+        @param showAll (bool): if set, initialise with True to show all contacts
+            or with False to show only the ones that are not assigned yet.
+        """
+        self.toggle.showAll = (not self.toggle.showAll) if showAll is None else showAll
+        self.toggle.setText("Hide assigned" if self.toggle.showAll else "Show assigned")
+        self.updateContactList()
+
+    def updateContactList(self, contacts=None):
+        """Update the contact list's items visibility, depending of the toggle
+        button and the "contacts" attribute.
+
+        @param contacts (list): contacts to be updated, or None to update all.
+        """
         if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"):
             return
-        sender = self.toggle
         if contacts is not None:
-            if not isinstance(contacts, list):
-                contacts = [contacts]
-            for contact_ in contacts:
-                if contact_ not in self.all_contacts:
-                    contacts.remove(contact_)
+            to_remove = set()
+            for contact in contacts:
+                if contact not in self.all_contacts:
+                    to_remove.add(contact)
+            for contact in to_remove:
+                contacts.remove(contact)
         else:
             contacts = self.all_contacts
-        for contact_ in contacts:
-            if sender.showAll:
-                self.contacts.getContactBox(contact_).setVisible(True)
+        for contact in contacts:
+            if self.toggle.showAll:
+                self.contacts.getContactBox(contact).setVisible(True)
             else:
-                if contact_ in self.groups.remaining_list:
-                    self.contacts.getContactBox(contact_).setVisible(True)
+                if contact in self.groups.items_remaining:
+                    self.contacts.getContactBox(contact).setVisible(True)
                 else:
-                    self.contacts.getContactBox(contact_).setVisible(False)
+                    self.contacts.getContactBox(contact).setVisible(False)
 
     def __close(self):
         """Remove the widget from parent or close the popup."""
-        if isinstance(self._parent, DialogBox):
-            self._parent.hide()
-        self._parent.remove(self)
+        if isinstance(self.container, DialogBox):
+            self.container.hide()
+        self.container.remove(self)
         if self._on_close_callback is not None:
             self._on_close_callback()
         if self.restore_contact_panel:
@@ -215,22 +252,21 @@
 
     def closeAndSave(self):
         """Call bridge methods to save the changes and close the dialog"""
-        map_ = {}
-        for contact_ in self.all_contacts:
-            map_[contact_] = set()
-        contacts = self.groups.getContacts()
-        for group in contacts.keys():
-            for contact_ in contacts[group]:
-                try:
-                    map_[contact_].add(group)
-                except KeyError:
-                    dialog.InfoDialog("Invalid contact",
-                           "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) +
-                           "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
-                    return
-        for contact_ in map_.keys():
-            groups = map_[contact_]
-            current_groups = self.host.contact_panel.getContactGroups(contact_)
-            if groups != current_groups:
-                self.host.bridge.call('updateContact', None, contact_, '', list(groups))
+        old_groups_by_entity = contact_list.JIDDict(self.host.contact_list.roster_groups_by_entity)
+        old_entities = old_groups_by_entity.keys()
+        groups_by_entity = contact_list.JIDDict(self.groups.getKeysByItem())
+        entities = groups_by_entity.keys()
+
+        for invalid in entities.difference(self.all_contacts):
+            dialog.InfoDialog("Invalid contact(s)",
+                              "The contact '%s' is not in your contact list but has been assigned to: '%s'." % (invalid, "', '".join(groups_by_entity[invalid])) +
+                              "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
+            return
+
+        for entity in old_entities.difference(entities):
+            self.host.bridge.call('updateContact', None, unicode(entity), '', [])
+
+        for entity, groups in groups_by_entity.iteritems():
+            if entity not in old_groups_by_entity or groups != old_groups_by_entity[entity]:
+                self.host.bridge.call('updateContact', None, unicode(entity), '', list(groups))
         self.__close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_list.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,476 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.Label import Label
+from pyjamas import Window
+from pyjamas import DOM
+
+from constants import Const as C
+import libervia_widget
+import contact_panel
+import blog
+import chat
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+def buildPresenceStyle(presence, base_style=None):
+    """Return the CSS classname to be used for displaying the given presence information.
+
+    @param presence (unicode): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
+    @param base_style (unicode): base classname
+    @return: unicode
+    """
+    if not base_style:
+        base_style = "contactLabel"
+    return '%s-%s' % (base_style, presence or 'connected')
+
+
+def setPresenceStyle(widget, presence, base_style=None):
+    """
+    Set the CSS style of a contact's element according to its presence.
+
+    @param widget (Widget): the UI element of the contact
+    @param presence (unicode): a value in ("", "chat", "away", "dnd", "xa").
+    @param base_style (unicode): the base name of the style to apply
+    """
+    if not hasattr(widget, 'presence_style'):
+        widget.presence_style = None
+    style = buildPresenceStyle(presence, base_style)
+    if style == widget.presence_style:
+        return
+    if widget.presence_style is not None:
+        widget.removeStyleName(widget.presence_style)
+    widget.addStyleName(style)
+    widget.presence_style = style
+
+
+class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler):
+    def __init__(self, host, group):
+        """
+
+        @param host (SatWebFrontend)
+        @param group (unicode): group name
+        """
+        self.group = group
+        Label.__init__(self, group)  # , Element=DOM.createElement('div')
+        self.setStyleName('group')
+        libervia_widget.DragLabel.__init__(self, group, "GROUP", host)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.displayWidget(blog.MicroblogPanel, (self.group,))
+
+
+class GroupPanel(VerticalPanel):
+
+    def __init__(self, parent):
+        VerticalPanel.__init__(self)
+        self.setStyleName('groupPanel')
+        self._parent = parent
+        self._groups = set()
+
+    def add(self, group):
+        if group in self._groups:
+            log.warning("trying to add an already existing group")
+            return
+        _item = GroupLabel(self._parent.host, group)
+        _item.addMouseListener(self._parent)
+        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
+        index = 0
+        for group_ in [child.group for child in self.getChildren()]:
+            if group_ > group:
+                break
+            index += 1
+        VerticalPanel.insert(self, _item, index)
+        self._groups.add(group)
+
+    def remove(self, group):
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                VerticalPanel.remove(self, wid)
+                self._groups.remove(group)
+                return
+        log.warning("Trying to remove a non existent group")
+
+    def getGroupBox(self, group):
+        """get the widget of a group
+
+        @param group (unicode): the group
+        @return: GroupLabel instance if present, else None"""
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                return wid
+        return None
+
+    def getGroups(self):
+        return self._groups
+
+
+class ContactsPanel(contact_panel.ContactsPanel):
+    """The contact list that is displayed on the left side."""
+
+    def __init__(self, host):
+
+        def on_click(contact_jid):
+            self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE)
+
+        contact_panel.ContactsPanel.__init__(self, host, contacts_click=on_click,
+                                             contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT))
+
+    def setState(self, jid_, type_, state):
+        """Change the appearance of the contact, according to the state
+
+        @param jid_ (jid.JID): jid.JID which need to change state
+        @param type_ (unicode): one of "availability", "messageWaiting"
+        @param state:
+            - for messageWaiting type:
+                True if message are waiting
+            - for availability type:
+                C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1"""
+        assert type_ in ('availability', 'messageWaiting')
+        contact_box = self.getContactBox(jid_)
+        if type_ == 'availability':
+            if state is None:
+                state = C.PRESENCE_UNAVAILABLE
+            setPresenceStyle(contact_box.label, state)
+        elif type_ == 'messageWaiting':
+            contact_box.setAlert(state)
+
+
+class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler):
+
+    def __init__(self, host, text):
+        Label.__init__(self, text)  # , Element=DOM.createElement('div')
+        self.setStyleName('contactTitle')
+        libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.displayWidget(blog.MicroblogPanel, ())
+
+
+class ContactList(SimplePanel, QuickContactList):
+    """Manage the contacts and groups"""
+
+    def __init__(self, host):
+        QuickContactList.__init__(self, host, C.PROF_KEY_NONE)
+        SimplePanel.__init__(self)
+        self.host = host
+        self.scroll_panel = ScrollPanel()
+        self.vPanel = VerticalPanel()
+        _title = ContactTitleLabel(host, 'Contacts')
+        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
+        self._contacts_panel = ContactsPanel(host)
+        self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists !
+        self._group_panel = GroupPanel(self)
+
+        self.vPanel.add(_title)
+        self.vPanel.add(self._group_panel)
+        self.vPanel.add(self._contacts_panel)
+        self.scroll_panel.add(self.vPanel)
+        self.add(self.scroll_panel)
+        self.setStyleName('contactList')
+        Window.addWindowResizeListener(self)
+
+        # 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.avatarListener = self.onAvatarUpdate
+        host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
+
+    @property
+    def profile(self):
+        return C.PROF_KEY_NONE
+
+    def onDelete(self):
+        QuickContactList.onDelete(self)
+        self.host.removeListener('avatar', self.avatarListener)
+
+    def update(self):
+        ### GROUPS ###
+        _keys = self._groups.keys()
+        try:
+            # XXX: Pyjamas doesn't do the set casting if None is present
+            _keys.remove(None)
+        except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options
+            pass
+        current_groups = set(_keys)
+        shown_groups = self._group_panel.getGroups()
+        new_groups = current_groups.difference(shown_groups)
+        removed_groups = shown_groups.difference(current_groups)
+        for group in new_groups:
+            self._group_panel.add(group)
+        for group in removed_groups:
+            self._group_panel.remove(group)
+
+        ### JIDS ###
+        to_show = [jid_ for jid_ in self.roster_entities if self.entityToShow(jid_) and jid_ != self.whoami.bare]
+        to_show.sort()
+
+        self._contacts_panel.setList(to_show)
+
+        for jid_ in self._alerts:
+            self._contacts_panel.setState(jid_, "messageWaiting", True)
+
+    def remove(self, entity):
+        # FIXME: SimplePanel and QuickContactList both have a 'remove' method
+        QuickContactList.remove(self, entity)
+
+    def onWindowResized(self, width, height):
+        ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5
+        tab_panel = self.host.panel.tab_panel
+        if tab_panel.getWidgetCount() > 1:
+            ideal_height -= tab_panel.getTabBar().getOffsetHeight()
+        self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))
+
+    # def updateContact(self, jid_s, attributes, groups):
+    #     """Add a contact to the panel if it doesn't exist, update it else
+
+    #     @param jid_s: jid userhost as unicode
+    #     @param attributes: cf SàT Bridge API's newContact
+    #     @param groups: list of groups"""
+    #     _current_groups = self.getContactGroups(jid_s)
+    #     _new_groups = set(groups)
+    #     _key = "@%s: "
+
+    #     for group in _current_groups.difference(_new_groups):
+    #         # We remove the contact from the groups where he isn't anymore
+    #         self.groups[group].remove(jid_s)
+    #         if not self.groups[group]:
+    #             # The group is now empty, we must remove it
+    #             del self.groups[group]
+    #             self._group_panel.remove(group)
+    #             if self.host.uni_box:
+    #                 self.host.uni_box.removeKey(_key % group)
+
+    #     for group in _new_groups.difference(_current_groups):
+    #         # We add the contact to the groups he joined
+    #         if group not in self.groups.keys():
+    #             self.groups[group] = set()
+    #             self._group_panel.add(group)
+    #             if self.host.uni_box:
+    #                 self.host.uni_box.addKey(_key % group)
+    #         self.groups[group].add(jid_s)
+
+    #     # We add the contact to contact list, it will check if contact already exists
+    #     self._contacts_panel.add(jid_s)
+    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))
+
+    # def removeContact(self, jid):
+    #     """Remove contacts from groups where he is and contact list"""
+    #     self.updateContact(jid, {}, [])  # we remove contact from every group
+    #     self._contacts_panel.remove(jid)
+
+    # def setConnected(self, jid_s, resource, availability, priority, statuses):
+    #     """Set connection status
+    #     @param jid_s (unicode): JID userhost as unicode
+    #     """
+    #     if availability == 'unavailable':
+    #         if jid_s in self.connected:
+    #             if resource in self.connected[jid_s]:
+    #                 del self.connected[jid_s][resource]
+    #             if not self.connected[jid_s]:
+    #                 del self.connected[jid_s]
+    #     else:
+    #         if jid_s not in self.connected:
+    #             self.connected[jid_s] = {}
+    #         self.connected[jid_s][resource] = (availability, priority, statuses)
+
+    #     # check if the contact is connected with another resource, use the one with highest priority
+    #     if jid_s in self.connected:
+    #         max_resource = max_priority = None
+    #         for tmp_resource in self.connected[jid_s]:
+    #             if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
+    #                 max_resource = tmp_resource
+    #                 max_priority = self.connected[jid_s][tmp_resource][1]
+    #         if availability == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
+    #             priority = max_priority
+    #             availability = self.connected[jid_s][max_resource][0]
+    #     if jid_s not in self.connected or priority >= max_priority:
+    #         # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
+    #         # case 2: update (or confirm) with the values of the resource which takes precedence
+    #         self._contacts_panel.setState(jid_s, "availability", availability)
+
+    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))
+
+    def setContactMessageWaiting(self, jid, waiting):
+        """Show a visual indicator that contact has send a message
+
+        @param jid: jid of the contact
+        @param waiting: True if message are waiting"""
+        raise Exception("Should not be there")
+        # self._contacts_panel.setState(jid, "messageWaiting", waiting)
+
+    # def getConnected(self, filter_muc=False):
+    #     """return a list of all jid (bare jid) connected
+    #     @param filter_muc: if True, remove the groups from the list
+    #     """
+    #     contacts = self.connected.keys()
+    #     contacts.sort()
+    #     return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts())))
+
+    # def getContactGroups(self, contact_jid_s):
+    #     """Get groups where contact is
+    #    @param group: string of single group, or list of string
+    #    @param contact_jid_s: jid to test, as unicode
+    #     """
+    #     result = set()
+    #     for group in self.groups:
+    #         if self.isContactInGroup(group, contact_jid_s):
+    #             result.add(group)
+    #     return result
+
+    # def isContactInGroup(self, group, contact_jid):
+    #     """Test if the contact_jid is in the group
+    #     @param group: string of single group, or list of string
+    #     @param contact_jid: jid to test
+    #     @return: True if contact_jid is in on of the groups"""
+    #     if group in self.groups and contact_jid in self.groups[group]:
+    #         return True
+    #     return False
+
+    def isContactInRoster(self, contact_jid):
+        """Test if the contact is in our roster list"""
+        for contact_box in self._contacts_panel:
+            if contact_jid == contact_box.jid:
+                return True
+        return False
+
+    # def getContacts(self):
+    #     return self._contacts_panel.getContacts()
+
+    def getGroups(self):
+        return self.groups.keys()
+
+    def onMouseMove(self, sender, x, y):
+        pass
+
+    def onMouseDown(self, sender, x, y):
+        pass
+
+    def onMouseUp(self, sender, x, y):
+        pass
+
+    def onMouseEnter(self, sender):
+        if isinstance(sender, GroupLabel):
+            jids = self.getGroupData(sender.group, "jids")
+            for contact in self._contacts_panel:
+                if contact.jid in jids:
+                    contact.label.addStyleName("selected")
+
+    def onMouseLeave(self, sender):
+        if isinstance(sender, GroupLabel):
+            jids = self.getGroupData(sender.group, "jids")
+            for contact in self._contacts_panel:
+                if contact.jid in jids:
+                    contact.label.removeStyleName("selected")
+
+    def onAvatarUpdate(self, jid_, hash_, profile):
+        """Called on avatar update events
+
+        @param jid_: jid of the entity with updated avatar
+        @param hash_: hash of the avatar
+        @param profile: %(doc_profile)s
+        """
+        self._contacts_panel.updateAvatar(jid_, self.host.getAvatarURL(jid_))
+
+    def onNickUpdate(self, jid_, new_nick, profile):
+        self._contacts_panel.updateNick(jid_, new_nick)
+
+    def hasVisibleMembers(self, group):
+        """Tell if the given group actually has visible members
+
+        @param group (unicode): the group to check
+        @return: boolean
+        """
+        raise Exception  # FIXME: remove this method
+        for jid_ in self.groups[group]:
+            if self._contacts_panel.getContactBox(jid_).isVisible():
+                return True
+        return False
+
+    def offlineContactsToShow(self):
+        """Tell if offline contacts should be visible according to the user settings
+
+        @return: boolean
+        """
+        return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS))
+
+    def emtyGroupsToShow(self):
+        """Tell if empty groups should be visible according to the user settings
+
+        @return: boolean
+        """
+        return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS))
+
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
+        QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile)
+        entity_bare = entity.bare
+        show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only
+        self._contacts_panel.setState(entity_bare, "availability", show)
+        self.update()  # FIXME: should update the list without rebuilding it all
+
+    # def updateVisibility(self, jids, groups):
+    #     """Set the widgets visibility for the given contacts and groups
+
+    #     @param jids (list[unicode]): list of JID
+    #     @param groups (list[unicode]): list of groups
+    #     """
+    #     for jid_s in jids:
+    #         try:
+    #             self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
+    #         except TypeError:
+    #             log.warning('No box for contact %s: this code line should not be reached' % jid_s)
+    #     for group in groups:
+    #         try:
+    #             self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
+    #         except TypeError:
+    #             log.warning('No box for group %s: this code line should not be reached' % group)
+
+    # def refresh(self):
+    #     """Show or hide disconnected contacts and empty groups"""
+    #     self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())
+
+
+class JIDList(list):
+    """JID-friendly list implementation for Pyjamas"""
+
+    def __contains__(self, item):
+        """Tells if the list contains the given item.
+
+        @param item (object): element to check
+        @return: bool
+        """
+        # Since our JID doesn't inherit from str/unicode, without this method
+        # the test would return True only when the objects references are the
+        # same. Tests have shown that the other iterable "set" and "dict" don't
+        # need this hack to reproduce the Twisted's behavior.
+        for other in self:
+            if other == item:
+                return True
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_panel.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,247 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+""" Contacts / jids related panels """
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import jid
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+
+import html_tools
+import contact_widget
+from constants import Const as C
+
+
+# FIXME: must be removed
+class Occupant(HTML):
+    """Occupant of a MUC room"""
+
+    def __init__(self, nick, state=None, special=""):
+        """
+        @param nick: the user nickname
+        @param state: the user chate state (XEP-0085)
+        @param special: a string of symbols (e.g: for activities)
+        """
+        HTML.__init__(self, StyleName="occupant")
+        self.nick = nick
+        self._state = state
+        self.special = special
+        self._refresh()
+
+    def __str__(self):
+        return self.nick
+
+    def setState(self, state):
+        self._state = state
+        self._refresh()
+
+    def addSpecial(self, special):
+        """@param special: unicode"""
+        if special not in self.special:
+            self.special += special
+            self._refresh()
+
+    def removeSpecials(self, special):
+        """@param special: unicode or list"""
+        if not isinstance(special, list):
+            special = [special]
+        for symbol in special:
+            self.special = self.special.replace(symbol, "")
+            self._refresh()
+
+    def _refresh(self):
+        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
+        special = "" if len(self.special) == 0 else " %s" % self.special
+        self.setHTML("%s%s%s" % (html_tools.html_sanitize(self.nick), special, state))
+
+
+class ContactsPanel(VerticalPanel):
+    """ContactList graphic representation
+
+    Special features like popup menu panel or changing the contact states must be done in a sub-class.
+    """
+
+    def __init__(self, host, merge_resources=True, contacts_click=None,
+                 contacts_style=None, contacts_menus=True,
+                 contacts_display=C.CONTACT_DEFAULT_DISPLAY):
+        """
+
+        @param host (SatWebFrontend): host instance
+        @param merge_resources (bool): if True, the entities sharing the same
+            bare JID will also share the same contact box.
+        @param contacts_click (callable): click callback for the contact boxes
+        @param contacts_style (unicode): CSS style name for the contact boxes
+        @param contacts_menus (tuple): define the menu types that fit this
+            contact panel, with values from the menus type constants.
+        @param contacts_display (tuple): prioritize the display methods of the
+            contact's label with values in ("jid", "nick", "bare", "resource")
+        """
+        VerticalPanel.__init__(self)
+        self.host = host
+        self.merge_resources = merge_resources
+        self._contacts = {}  # entity jid to ContactBox map
+        self.click_listener = None
+
+        if contacts_click is not None:
+            self.onClick = contacts_click
+
+        self.contacts_style = contacts_style
+        self.contacts_menus = contacts_menus
+        self.contacts_display = contacts_display
+
+    def _key(self, contact_jid):
+        """Return internal key for this contact.
+
+        @param contact_jid (jid.JID): contact JID
+        @return: jid.JID
+        """
+        return contact_jid.bare if self.merge_resources else contact_jid
+
+    def setList(self, jids):
+        """set all contacts in the list in one shot.
+
+        @param jids (list[jid.JID]): jids to display (the order is kept)
+        @param name (unicode): optional name of the contact
+        """
+        # FIXME: we do a full clear and add boxes after, we should only remove recently hidden boxes and add new ones, and re-order
+        current = [box.jid for box in self.children if isinstance(box, contact_widget.ContactBox)]
+        if current == jids:
+            # the display doesn't change
+            return
+        self.clear()
+        for contact_jid in jids:
+            assert isinstance(contact_jid, jid.JID)
+            self.addContact(contact_jid)
+
+    def getContactBox(self, contact_jid):
+        """Get a contact box for a contact, add it if it doesn't exist yet.
+
+        @param contact_jid (jid.JID): contact JID
+        @return: ContactBox
+        """
+        try:
+            return self._contacts[self._key(contact_jid)]
+        except KeyError:
+            box = contact_widget.ContactBox(self.host, contact_jid,
+                                            style_name=self.contacts_style,
+                                            display=self.contacts_display,
+                                            plugin_menu_context=self.contacts_menus)
+            self._contacts[self._key(contact_jid)] = box
+            return box
+
+    def addContact(self, contact_jid):
+        """Add a contact to the list.
+
+        @param contact_jid (jid.JID): contact JID
+        """
+        box = self.getContactBox(contact_jid)
+        if box not in self.children:
+            VerticalPanel.append(self, box)
+
+    def removeContact(self, contact_jid):
+        """Remove a contact from the list.
+
+        @param contact_jid (jid.JID): contact JID
+        """
+        box = self._contacts.pop(self._key(contact_jid))
+        VerticalPanel.remove(self, box)
+
+    def updateAvatar(self, contact_jid, url):
+        """Update the avatar of the given contact
+
+        @param contact_jid (jid.JID): contact JID
+        @param url (unicode): image url
+        """
+        try:
+            self.getContactBox(contact_jid).updateAvatar(url)
+        except TypeError:
+            pass
+
+    def updateNick(self, contact_jid, new_nick):
+        """Update the avatar of the given contact.
+
+        @param contact_jid (jid.JID): contact JID
+        @param new_nick (unicode): new nick of the contact
+        """
+        try:
+            self.getContactBox(contact_jid).updateNick(new_nick)
+        except TypeError:
+            pass
+
+
+
+# FIXME: must be removed and ContactsPanel must be used instead
+class OccupantsList(AbsolutePanel):
+    """Panel user to show occupants of a room"""
+
+    def __init__(self):
+        AbsolutePanel.__init__(self)
+        self.occupants_list = {}
+        self.setStyleName('occupantsList')
+
+    def addOccupant(self, nick):
+        if nick in self.occupants_list:
+            return
+        _occupant = Occupant(nick)
+        self.occupants_list[nick] = _occupant
+        self.add(_occupant)
+
+    def removeOccupant(self, nick):
+        try:
+            self.remove(self.occupants_list[nick])
+        except KeyError:
+            log.error("trying to remove an unexisting nick")
+
+    def getOccupantBox(self, nick):
+        """Get the widget element of the given nick.
+
+        @return: Occupant
+        """
+        try:
+            return self.occupants_list[nick]
+        except KeyError:
+            return None
+
+    def clear(self):
+        self.occupants_list.clear()
+        AbsolutePanel.clear(self)
+
+    def updateSpecials(self, occupants=[], html=""):
+        """Set the specified html "symbol" to the listed occupants,
+        and eventually remove it from the others (if they got it).
+        This is used for example to visualize who is playing a game.
+        @param occupants: list of the occupants that need the symbol
+        @param html: unicode symbol (actually one character or more)
+        or a list to assign different symbols of the same family.
+        """
+        index = 0
+        special = html
+        for occupant in self.occupants_list.keys():
+            if occupant in occupants:
+                if isinstance(html, list):
+                    special = html[index]
+                    index = (index + 1) % len(html)
+                self.occupants_list[occupant].addSpecial(special)
+            else:
+                self.occupants_list[occupant].removeSpecials(html)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_widget.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,172 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core import exceptions
+from sat_frontends.quick_frontend import quick_menus
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Image import Image
+from pyjamas.ui.ClickListener import ClickHandler
+from constants import Const as C
+import html_tools
+import base_widget
+import libervia_widget
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+class ContactLabel(HTML):
+    """Display a contact in HTML, selecting best display (jid/nick/etc)"""
+
+    def __init__(self, host, jid_, display=C.CONTACT_DEFAULT_DISPLAY):
+        """
+
+        @param host (SatWebFrontend): host instance
+        @param jid_ (jid.JID): contact JID
+        @param display (tuple): prioritize the display methods of the contact's
+            label with values in ("jid", "nick", "bare", "resource").
+        """
+        # TODO: add a listener for nick changes
+        HTML.__init__(self)
+        self.host = host
+        self.jid = jid_
+        if "nick" in display:
+            self.nick = self.host.contact_lists[C.PROF_KEY_NONE].getCache(self.jid, "nick")
+        self.display = display
+        self.alert = False
+        self.refresh()
+        self.setStyleName('contactLabel')
+
+    def refresh(self):
+        alert_html = "<strong>(*)</strong>&nbsp;" if self.alert else ""
+        contact_raw = None
+        for disp in self.display:
+            if disp == "jid":
+                contact_raw = unicode(self.jid)
+            elif disp == "nick":
+                contact_raw = self.nick
+            elif disp == "bare":
+                contact_raw = unicode(self.jid.bare)
+            elif disp == "resource":
+                contact_raw = self.jid.resource
+            else:
+                raise exceptions.InternalError(u"Unknown display argument [{}]".format(disp))
+            if contact_raw:
+                break
+        if not contact_raw:
+            log.error(u"Counld not find a contact display for jid {jid} (display: {display})".format(jid=self.jid, display=self.display))
+            contact_raw = "UNNAMED"
+        contact_html = html_tools.html_sanitize(contact_raw)
+        html = "%(alert)s%(contact)s" % {'alert': alert_html,
+                                         'contact': contact_html}
+        self.setHTML(html)
+
+    def updateNick(self, new_nick):
+        """Change the current nick
+
+        @param new_nick(unicode): new nick to use
+        """
+        self.nick = new_nick
+        self.refresh()
+
+    def setAlert(self, alert):
+        """Show a visual indicator
+
+        @param alert: True if alert must be shown
+        """
+        self.alert = alert
+        self.refresh()
+
+
+class ContactMenuBar(base_widget.WidgetMenuBar):
+
+    def onBrowserEvent(self, event):
+        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
+        event.stopPropagation()  # prevent opening the chat dialog
+
+    @classmethod
+    def getCategoryHTML(cls, menu_name_i18n, type_):
+        return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL
+
+    def setUrl(self, url):
+        """Set the URL of the contact avatar."""
+        self.items[0].setHTML('<img src="%s" />' % url)
+
+
+class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel):
+
+    def __init__(self, host, jid_, style_name=None, display=C.CONTACT_DEFAULT_DISPLAY, plugin_menu_context=None):
+        """
+        @param host (SatWebFrontend): host instance
+        @param jid_ (jid.JID): contact JID
+        @param style_name (unicode): CSS style name
+        @param contacts_display (tuple): prioritize the display methods of the
+            contact's label with values in ("jid", "nick", "bare", "resource").
+        @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
+
+        """
+        self.plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
+        VerticalPanel.__init__(self, StyleName=style_name or 'contactBox', VerticalAlignment='middle')
+        ClickHandler.__init__(self)
+        libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", host)
+        self.jid = jid_
+        self.label = ContactLabel(host, self.jid, display=display)
+        self.avatar = ContactMenuBar(self, host) if plugin_menu_context else Image()
+        try:  # FIXME: dirty hack to force using an Image when the menu is actually empty
+            self.avatar.items[0]
+        except IndexError:
+            self.avatar = Image()
+        self.updateAvatar(host.getAvatarURL(self.jid.bare))
+        self.add(self.avatar)
+        self.add(self.label)
+        self.addClickListener(self)
+
+    def setAlert(self, alert):
+        """Show a visual indicator
+
+        @param alert: True if alert indicator show be shown"""
+        self.label.setAlert(alert)
+
+    def updateAvatar(self, url):
+        """Update the avatar.
+
+        @param url (unicode): image url
+        """
+        self.avatar.setUrl(url)
+
+    def updateNick(self, new_nick):
+        """Update the nickname.
+
+        @param new_nick (unicode): new nickname to use
+        """
+        self.label.updateNick(new_nick)
+
+    def onClick(self, sender):
+        try:
+            self.parent.onClick(self.jid.bare)
+        except (AttributeError, TypeError):
+            pass
+        else:
+            self.setAlert(False)
+
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_JID_CONTEXT, lambda caller, dummy: {'jid': unicode(caller.jid.bare)})
--- a/src/browser/sat_browser/dialog.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/dialog.py	Wed Mar 18 16:15:18 2015 +0100
@@ -19,7 +19,10 @@
 
 from sat.core.log import getLogger
 log = getLogger(__name__)
+
+from constants import Const as C
 from sat_frontends.tools.misc import DEFAULT_MUC
+from sat_frontends.tools import jid
 
 from pyjamas.ui.VerticalPanel import VerticalPanel
 from pyjamas.ui.Grid import Grid
@@ -37,19 +40,26 @@
 from pyjamas.ui.MouseListener import MouseWheelHandler
 from pyjamas import Window
 
-import base_panels
+import base_panel
 
 
 # List here the patterns that are not allowed in contact group names
 FORBIDDEN_PATTERNS_IN_GROUP = ()
 
 
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
 class RoomChooser(Grid):
     """Select a room from the rooms you already joined, or create a new one"""
 
     GENERATE_MUC = "<use random name>"
 
     def __init__(self, host, default_room=DEFAULT_MUC):
+        """
+
+        @param host (SatWebFrontend)
+        """
         Grid.__init__(self, 2, 2, Width='100%')
         self.host = host
 
@@ -70,7 +80,19 @@
 
         self.exist_radio.setVisible(False)
         self.rooms_list.setVisible(False)
-        self.setRooms()
+        self.refreshOptions()
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
+
+        @return: jid.JID or None to let the backend generate a new name
+        """
+        if self.exist_radio.getChecked():
+            values = self.rooms_list.getSelectedValues()
+            return jid.JID(values[0]) if values else None
+        value = self.box.getText()
+        return None if value in ('', self.GENERATE_MUC) else jid.JID(value)
 
     def onFocus(self, sender):
         if sender == self.rooms_list:
@@ -85,21 +107,17 @@
             if self.box.getText() == "":
                 self.box.setText(self.GENERATE_MUC)
 
-    def setRooms(self):
-        for room in self.host.room_list:
+    def refreshOptions(self):
+        """Refresh the already joined room list"""
+        contact_list = self.host.contact_list
+        muc_rooms = contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP)
+        for room in muc_rooms:
             self.rooms_list.addItem(room.bare)
-        if len(self.host.room_list) > 0:
+        if len(muc_rooms) > 0:
             self.exist_radio.setVisible(True)
             self.rooms_list.setVisible(True)
             self.exist_radio.setChecked(True)
 
-    def getRoom(self):
-        if self.exist_radio.getChecked():
-            values = self.rooms_list.getSelectedValues()
-            return "" if values == [] else values[0]
-        value = self.box.getText()
-        return "" if value == self.GENERATE_MUC else value
-
 
 class ContactsChooser(VerticalPanel):
     """Select one or several connected contacts"""
@@ -120,9 +138,9 @@
         elif nb_contact is not None:
             nb_contact = (nb_contact, nb_contact)
         if nb_contact is None:
-            log.warning("Need to select as many contacts as you want")
+            log.debug("Need to select as many contacts as you want")
         else:
-            log.warning("Need to select between %d and %d contacts" % nb_contact)
+            log.debug("Need to select between %d and %d contacts" % nb_contact)
         self.nb_contact = nb_contact
         self.ok_button = ok_button
         VerticalPanel.__init__(self, Width='100%')
@@ -132,34 +150,41 @@
         self.contacts_list.addStyleName('contactsChooser')
         self.contacts_list.addChangeListener(self.onChange)
         self.add(self.contacts_list)
-        self.setContacts()
+        self.refreshOptions()
         self.onChange()
 
+    @property
+    def contacts(self):
+        """Return the selected contacts.
+
+        @return: list[jid.JID]
+        """
+        return [jid.JID(contact) for contact in self.contacts_list.getSelectedValues(True)]
+
     def onChange(self, sender=None):
         if self.ok_button is None:
             return
         if self.nb_contact:
             selected = len(self.contacts_list.getSelectedValues(True))
-            if  selected >= self.nb_contact[0] and selected <= self.nb_contact[1]:
+            if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]:
                 self.ok_button.setEnabled(True)
             else:
                 self.ok_button.setEnabled(False)
 
-    def setContacts(self, selected=[]):
-        """Fill the list with the connected contacts
-        @param select: list of the contacts to select by default
+    def refreshOptions(self, keep_selected=False):
+        """Fill the list with the connected contacts.
+
+        @param keep_selected (boolean): if True, keep the current selection
         """
+        selection = self.contacts if keep_selected else []
         self.contacts_list.clear()
-        contacts = self.host.contact_panel.getConnected(filter_muc=True)
+        contacts = self.host.contact_list.roster_entities_connected
         self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5)
         self.contacts_list.addItem("")
         for contact in contacts:
-            if contact not in [room.bare for room in self.host.room_list]:
-                self.contacts_list.addItem(contact)
-        self.contacts_list.setItemTextSelection(selected)
-
-    def getContacts(self):
-        return self.contacts_list.getSelectedValues(True)
+            self.contacts_list.addItem(contact)
+        if selection:
+            self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection])
 
 
 class RoomAndContactsChooser(DialogBox):
@@ -182,7 +207,7 @@
         self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC)
         self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
 
-        self.stack_panel = base_panels.ToggleStackPanel(Width="100%")
+        self.stack_panel = base_panel.ToggleStackPanel(Width="100%")
         self.stack_panel.add(self.room_panel, visible=visible[0])
         self.stack_panel.add(self.contact_panel, visible=visible[1])
         self.stack_panel.addStackChangeListener(self)
@@ -198,60 +223,72 @@
         self.setHTML(title)
         self.show()
 
-        # needed to update the contacts list when someone logged in/out
-        self.host.room_contacts_chooser = self
+        # 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.refreshContactList
+        # update the contacts list when someone logged in/out
+        self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
 
-    def getRoom(self, asSuffix=False):
-        room = self.room_panel.getRoom()
-        if asSuffix:
-            return room if room == "" else ": %s" % room
-        else:
-            return room
+        @return: jid.JID or None
+        """
+        return self.room_panel.room
 
-    def getContacts(self, asSuffix=False):
-        contacts = self.contact_panel.getContacts()
-        if asSuffix:
-            return "" if contacts == [] else ": %s" % ", ".join(contacts)
-        else:
-            return contacts
+    @property
+    def contacts(self):
+        """Return the selected contacts.
+
+        @return: list[jid.JID]
+        """
+        return self.contact_panel.contacts
 
     def onStackChanged(self, sender, index, visible=None):
         if visible is None:
             visible = sender.getWidget(index).getVisible()
         if index == 0:
-            sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True)))
+            suffix = "" if (visible or not self.room) else ": %s" % self.room
+            sender.setStackText(0, self.title_room + suffix)
         elif index == 1:
-            sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True)))
+            suffix = "" if (visible or not self.contacts) else ": %s" % ", ".join([unicode(contact) for contact in self.contacts])
+            sender.setStackText(1, self.title_invite + suffix)
 
-    def resetContacts(self):
-        """Called when someone log in/out to update the list"""
-        self.contact_panel.setContacts(self.getContacts())
+    def refreshContactList(self, *args, **kwargs):
+        """Called when someone log in/out to update the list.
+
+        @param args: set by the event call but not used here
+        """
+        self.contact_panel.refreshOptions(keep_selected=True)
 
     def onOK(self, sender):
-        room_jid = self.getRoom()
-        if room_jid != "" and "@" not in room_jid:
+        room = self.room  # pyjamas issue: you need to use an intermediate variable to access a property's method
+        if room and not room.is_valid():
             Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain)
             return
         self.hide()
-        self.callback(room_jid, self.getContacts())
+        self.callback(room, self.contacts)
 
     def onCancel(self, sender):
         self.hide()
 
     def hide(self):
-        self.host.room_contacts_chooser = None
+        self.host.removeListener('presence', self.presenceListener)
         DialogBox.hide(self, autoClosed=True)
 
 
 class GenericConfirmDialog(DialogBox):
 
-    def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs):
+    def __init__(self, widgets, callback, title='Confirmation', prompt_widgets=None, **kwargs):
         """
         Dialog to confirm an action
         @param widgets (list[Widget]): widgets to attach
-        @param callback: method to call when a button is clicked
+        @param callback (callable): method to call when a button is pressed,
+            with the following arguments:
+                - result (bool): set to True if the dialog has been confirmed
+                - *args: a list of unicode (the values for the prompt_widgets)
         @param title: title of the dialog
-        @param prompt (TextBox, list[TextBox]): input widgets from which to retrieve
+        @param prompt_widgets (list[TextBox]): input widgets from which to retrieve
         the string value(s) to be passed to the callback when OK button is pressed.
         If None, OK button will return "True". Cancel button always returns "False".
         """
@@ -261,16 +298,14 @@
         if added_style:
             self.addStyleName(added_style)
 
-        if prompt is None:
-            prompt = []
-        elif isinstance(prompt, TextBox):
-            prompt = [prompt]
+        if prompt_widgets is None:
+            prompt_widgets = []
 
         content = VerticalPanel()
         content.setWidth('100%')
         for wid in widgets:
             content.add(wid)
-            if wid in prompt:
+            if wid in prompt_widgets:
                 wid.setWidth('100%')
         button_panel = HorizontalPanel()
         button_panel.addStyleName("marginAuto")
@@ -281,11 +316,12 @@
         content.add(button_panel)
         self.setHTML(title)
         self.setWidget(content)
-        self.prompt = prompt
+        self.prompt_widgets = prompt_widgets
 
     def onConfirm(self, sender):
         self.hide()
-        result = [box.getText() for box in self.prompt] if self.prompt else [True]
+        result = [True]
+        result.extend([box.getText() for box in self.prompt_widgets])
         self.callback(*result)
 
     def onCancel(self, sender):
@@ -294,8 +330,8 @@
 
     def show(self):
         DialogBox.show(self)
-        if self.prompt:
-            self.prompt[0].setFocus(True)
+        if self.prompt_widgets:
+            self.prompt_widgets[0].setFocus(True)
 
 
 class ConfirmDialog(GenericConfirmDialog):
@@ -328,7 +364,7 @@
         _body.add(main_widget)
         _body.setCellWidth(main_widget, '100%')
         _body.setCellHeight(main_widget, '100%')
-        if not 'NO_CLOSE' in options:
+        if 'NO_CLOSE' not in options:
             _close_button = Button("Close", self.onClose)
             _body.add(_close_button)
             _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
@@ -357,19 +393,18 @@
     def __init__(self, callback, textes=None, values=None, title='User input', **kwargs):
         """Prompt the user for one or more input(s).
 
-        @param callback (callable): method to call when clicking OK
-        @param textes (str, list[str]): HTML textes to display before the inputs
-        @param values (str, list[str]): default values for each input
-        @param title (str): dialog title
+        @param callback (callable): method to call when a button is pressed,
+            with the following arguments:
+                - result (bool): set to True if the dialog has been confirmed
+                - *args: a list of unicode (the values entered by the user)
+        @param textes (list[unicode]): HTML textes to display before the inputs
+        @param values (list[unicode]): default values for each input
+        @param title (unicode): dialog title
         """
         if textes is None:
             textes = ['']  # display a single input without any description
-        elif not isinstance(textes, list):
-            textes = [textes]  # allow to pass a single string instead of a list
         if values is None:
             values = []
-        elif not isinstance(values, list):
-            values = [values]  # allow to pass a single string instead of a list
         all_widgets = []
         prompt_widgets = []
         for count in xrange(len(textes)):
@@ -388,7 +423,7 @@
 
     def onEventPreview(self, event):
         if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
-            #needed to prevent request cancellation in Firefox
+            # needed to prevent request cancellation in Firefox
             event.preventDefault()
         return PopupPanel.onEventPreview(self, event)
 
@@ -528,7 +563,7 @@
         minus_button = Button("-", self.onMinus)
         self.box = WheelTextBox()
         self.box.setVisibleLength(visible_len)
-        self.box.setText(str(value))
+        self.box.setText(unicode(value))
         self.box.addInputListener(self)
         self.box.addMouseWheelListener(self)
         plus_button = Button("+", self.onPlus)
@@ -551,19 +586,19 @@
     def setValue(self, value):
         """Change the value and fire valueChange listeners"""
         self.value = value
-        self.box.setText(str(value))
+        self.box.setText(unicode(value))
         self._callListeners()
 
     def onMinus(self, sender, step=1):
         self.value = max(0, self.value - step)
-        self.box.setText(str(self.value))
+        self.box.setText(unicode(self.value))
         self._callListeners()
 
     def onPlus(self, sender, step=1):
         self.value += step
         if self.value_max:
             self.value = min(self.value, self.value_max)
-        self.box.setText(str(self.value))
+        self.box.setText(unicode(self.value))
         self._callListeners()
 
     def onInput(self, sender):
@@ -574,7 +609,7 @@
             pass
         if self.value_max:
             self.value = min(self.value, self.value_max)
-        self.box.setText(str(self.value))
+        self.box.setText(unicode(self.value))
         self._callListeners()
 
     def onMouseWheel(self, sender, velocity):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/editor_widget.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,390 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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 import strings
+
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+
+import html_tools
+
+
+class MessageBox(TextArea):
+    """A basic text area for entering messages"""
+
+    def __init__(self, host):
+        TextArea.__init__(self)
+        self.host = host
+        self.size = (0, 0)
+        self.setStyleName('messageBox')
+        self.addKeyboardListener(self)
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+
+    def onBrowserEvent(self, event):
+        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
+        #     so the TextBox's cancelKey doens't work. This is a workaround
+        #     FIXME: fix the bug upstream
+        self.currentEvent = event
+        TextArea.onBrowserEvent(self, event)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        _txt = self.getText()
+
+        def history_cb(text):
+            self.setText(text)
+            Timer(5, lambda timer: self.setCursorPos(len(text)))
+
+        if keycode == KEY_ENTER:
+            if _txt:
+                self.host.selected_widget.onTextEntered(_txt)
+                self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
+            self.setText('')
+            sender.cancelKey()
+        elif keycode == KEY_UP:
+            self.host._updateInputHistory(_txt, -1, history_cb)
+        elif keycode == KEY_DOWN:
+            self.host._updateInputHistory(_txt, +1, history_cb)
+        else:
+            self._onComposing()
+
+    def _onComposing(self):
+        """Callback when the user is composing a text."""
+        self.host.selected_widget.state_machine._onEvent("composing")
+
+    def onMouseUp(self, sender, x, y):
+        size = (self.getOffsetWidth(), self.getOffsetHeight())
+        if size != self.size:
+            self.size = size
+            self.host.resize()
+
+    def onSelectedChange(self, selected):
+        self._selected_cache = selected
+
+
+class BaseTextEditor(object):
+    """Basic definition of a text editor. The method edit gets a boolean parameter which
+    should be set to True when you want to edit the text and False to only display it."""
+
+    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
+        """
+        Remark when inheriting this class: since the setContent method could be
+        overwritten by the child class, you should consider calling this __init__
+        after all the parameters affecting this setContent method have been set.
+        @param content: dict with at least a 'text' key
+        @param strproc: method to be applied on strings to clean the content
+        @param modifiedCb: method to be called when the text has been modified.
+        If this method returns:
+        - True: the modification will be saved and afterEditCb called;
+        - False: the modification won't be saved and afterEditCb called;
+        - None: the modification won't be saved and afterEditCb not called.
+        @param afterEditCb: method to be called when the edition is done
+        """
+        if content is None:
+            content = {'text': ''}
+        assert('text' in content)
+        if strproc is None:
+            def strproc(text):
+                try:
+                    return text.strip()
+                except (TypeError, AttributeError):
+                    return text
+        self.strproc = strproc
+        self.__modifiedCb = modifiedCb
+        self._afterEditCb = afterEditCb
+        self.initialized = False
+        self.edit_listeners = []
+        self.setContent(content)
+
+    def setContent(self, content=None):
+        """Set the editable content. The displayed content, which is set from the child class, could differ.
+        @param content: dict with at least a 'text' key
+        """
+        if content is None:
+            content = {'text': ''}
+        elif not isinstance(content, dict):
+            content = {'text': content}
+        assert('text' in content)
+        self._original_content = {}
+        for key in content:
+            self._original_content[key] = self.strproc(content[key])
+
+    def getContent(self):
+        """Get the current edited or editable content.
+        @return: dict with at least a 'text' key
+        """
+        raise NotImplementedError
+
+    def setOriginalContent(self, content):
+        """Use this method with care! Content initialization should normally be
+        done with self.setContent. This method exists to let you trick the editor,
+        e.g. for self.modified to return True also when nothing has been modified.
+        @param content: dict
+        """
+        self._original_content = content
+
+    def getOriginalContent(self):
+        """
+        @return the original content before modification (dict)
+        """
+        return self._original_content
+
+    def modified(self, content=None):
+        """Check if the content has been modified.
+        Remark: we don't use the direct comparison because we want to ignore empty elements
+        @content: content to be check against the original content or None to use the current content
+        @return: True if the content has been modified.
+        """
+        if content is None:
+            content = self.getContent()
+        # the following method returns True if one non empty element exists in a but not in b
+        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
+        # the following method returns True if the values for the common keys are not equals
+        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
+        # finally the combination of both to return True if a difference is found
+        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
+
+        return diff(content, self._original_content)
+
+    def edit(self, edit, abort=False, sync=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        @param sync: set to True to cancel the edition after the content has been saved somewhere else
+        """
+        if edit:
+            if not self.initialized:
+                self.syncToEditor()  # e.g.: use the selected target and unibox content
+            self.setFocus(True)
+            if abort:
+                content = self.getContent()
+                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
+                    self.edit(False, True, sync)
+                    return
+            if sync:
+                self.syncFromEditor(content)  # e.g.: save the content to unibox
+                return
+        else:
+            if not self.initialized:
+                return
+            content = self.getContent()
+            if abort:
+                self._afterEditCb(content)
+                return
+            if self.__modifiedCb and self.modified(content):
+                result = self.__modifiedCb(content)  # e.g.: send a message or update something
+                if result is not None:
+                    if self._afterEditCb:
+                        self._afterEditCb(content)  # e.g.: restore the display mode
+                    if result is True:
+                        self.setContent(content)
+            elif self._afterEditCb:
+                self._afterEditCb(content)
+
+        self.initialized = True
+
+    def setFocus(self, focus):
+        """
+        @param focus: set to True to focus the editor
+        """
+        raise NotImplementedError
+
+    def syncToEditor(self):
+        pass
+
+    def syncFromEditor(self, content):
+        pass
+
+    def abortEdition(self, content):
+        return True
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        self.edit_listeners.append(listener)
+
+
+class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
+    """Base class for manage a simple text editor."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        """
+        @param content
+        @param modifiedCb
+        @param afterEditCb
+        @param options: dict with the following value:
+        - no_xhtml: set to True to clean any xhtml content.
+        - enhance_display: if True, the display text will be enhanced with strings.addURLToText
+        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
+        - listen_focus: set to True to terminate the edition when the focus is lost.
+        - listen_click: set to True to start the edition when you click on the widget.
+        """
+        self.options = {'no_xhtml': False,
+                        'enhance_display': True,
+                        'listen_keyboard': True,
+                        'listen_focus': False,
+                        'listen_click': False
+                        }
+        if options:
+            self.options.update(options)
+        self.__shift_down = False
+        if self.options['listen_focus']:
+            FocusHandler.__init__(self)
+        if self.options['listen_click']:
+            ClickHandler.__init__(self)
+        KeyboardHandler.__init__(self)
+        strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
+        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
+        self.textarea = self.display = None
+
+    def setContent(self, content=None):
+        BaseTextEditor.setContent(self, content)
+
+    def getContent(self):
+        raise NotImplementedError
+
+    def edit(self, edit, abort=False, sync=False):
+        BaseTextEditor.edit(self, edit)
+        if edit:
+            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
+                self.textarea.addFocusListener(self)
+            if self.options['listen_click']:
+                self.display.clearClickListener()
+            if self not in self.textarea._keyboardListeners:
+                self.textarea.addKeyboardListener(self)
+        else:
+            self.setDisplayContent()
+            if self.options['listen_focus']:
+                try:
+                    self.textarea.removeFocusListener(self)
+                except ValueError:
+                    pass
+            if self.options['listen_click'] and self not in self.display._clickListeners:
+                self.display.addClickListener(self)
+            try:
+                self.textarea.removeKeyboardListener(self)
+            except ValueError:
+                pass
+
+    def setDisplayContent(self):
+        text = self._original_content['text']
+        if not self.options['no_xhtml']:
+            text = strings.addURLToImage(text)
+        if self.options['enhance_display']:
+            text = strings.addURLToText(text)
+        self.display.setHTML(html_tools.convertNewLinesToXHTML(text))
+
+    def setFocus(self, focus):
+        raise NotImplementedError
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        for listener in self.edit_listeners:
+            listener(self.textarea, keycode)
+        if not self.options['listen_keyboard']:
+            return
+        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
+            self.__shift_down = True
+            return
+        if keycode == KEY_ENTER:  # finish the edition
+            self.textarea.setFocus(False)
+            if not self.options['listen_focus']:
+                self.edit(False)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        if keycode == KEY_SHIFT:
+            self.__shift_down = False
+
+    def onLostFocus(self, sender):
+        """Finish the edition when focus is lost"""
+        if self.options['listen_focus']:
+            self.edit(False)
+
+    def onClick(self, sender=None):
+        """Start the edition when the widget is clicked"""
+        if self.options['listen_click']:
+            self.edit(True)
+
+    def onBrowserEvent(self, event):
+        if self.options['listen_focus']:
+            FocusHandler.onBrowserEvent(self, event)
+        if self.options['listen_click']:
+            ClickHandler.onBrowserEvent(self, event)
+        KeyboardHandler.onBrowserEvent(self, event)
+
+
+class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        HTML.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = self.display = self
+
+    def getContent(self):
+        text = DOM.getInnerHTML(self.getElement())
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setHTML(self._original_content['text'])
+        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus:
+            self.getElement().focus()
+        else:
+            self.getElement().blur()
+
+
+class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with a TextArea for editing, HTML for display."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        SimplePanel.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = TextArea()
+        self.display = HTML()
+
+    def getContent(self):
+        text = self.textarea.getText()
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setText(self._original_content['text'])
+        self.setWidget(self.textarea if edit else self.display)
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus and self.isAttached():
+            self.textarea.setCursorPos(len(self.textarea.getText()))
+        self.textarea.setFocus(focus)
--- a/src/browser/sat_browser/file_tools.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/file_tools.py	Wed Mar 18 16:15:18 2015 +0100
@@ -20,7 +20,7 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from constants import Const as C
-from sat.core.i18n import D_
+from sat.core.i18n import _, D_
 from pyjamas.ui.FileUpload import FileUpload
 from pyjamas.ui.FormPanel import FormPanel
 from pyjamas import Window
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/game_radiocol.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,343 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _, D_
+from sat_frontends.tools.misc import DEFAULT_MUC
+from sat_frontends.tools import host_listener
+from constants import Const as C
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.Hidden import Hidden
+from pyjamas.ui.CaptionPanel import CaptionPanel
+from pyjamas.media.Audio import Audio
+from pyjamas import Window
+from pyjamas.Timer import Timer
+
+import html_tools
+import file_tools
+import dialog
+
+
+class MetadataPanel(FlexTable):
+
+    def __init__(self):
+        FlexTable.__init__(self)
+        title_lbl = Label("title:")
+        title_lbl.setStyleName('radiocol_metadata_lbl')
+        artist_lbl = Label("artist:")
+        artist_lbl.setStyleName('radiocol_metadata_lbl')
+        album_lbl = Label("album:")
+        album_lbl.setStyleName('radiocol_metadata_lbl')
+        self.title = Label("")
+        self.title.setStyleName('radiocol_metadata')
+        self.artist = Label("")
+        self.artist.setStyleName('radiocol_metadata')
+        self.album = Label("")
+        self.album.setStyleName('radiocol_metadata')
+        self.setWidget(0, 0, title_lbl)
+        self.setWidget(1, 0, artist_lbl)
+        self.setWidget(2, 0, album_lbl)
+        self.setWidget(0, 1, self.title)
+        self.setWidget(1, 1, self.artist)
+        self.setWidget(2, 1, self.album)
+        self.setStyleName("radiocol_metadata_pnl")
+
+    def setTitle(self, title):
+        self.title.setText(title)
+
+    def setArtist(self, artist):
+        self.artist.setText(artist)
+
+    def setAlbum(self, album):
+        self.album.setText(album)
+
+
+class ControlPanel(FormPanel):
+    """Panel used to show controls to add a song, or vote for the current one"""
+
+    def __init__(self, parent):
+        FormPanel.__init__(self)
+        self.setEncoding(FormPanel.ENCODING_MULTIPART)
+        self.setMethod(FormPanel.METHOD_POST)
+        self.setAction("upload_radiocol")
+        self.timer_on = False
+        self._parent = parent
+        vPanel = VerticalPanel()
+
+        types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'),
+                 ('video/ogg', '*.ogv', 'Ogg Vorbis Video'),
+                 ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'),
+                 ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'),
+                 ('audio/mp3', '*.mp3', 'MPEG-Layer 3'),
+                 ]
+        self.file_upload = file_tools.FilterFileUpload("song", 10, types)
+        vPanel.add(self.file_upload)
+
+        hPanel = HorizontalPanel()
+        self.upload_btn = Button("Upload song", getattr(self, "onBtnClick"))
+        hPanel.add(self.upload_btn)
+        self.status = Label()
+        self.updateStatus()
+        hPanel.add(self.status)
+        #We need to know the filename and the referee
+        self.filename_field = Hidden('filename', '')
+        hPanel.add(self.filename_field)
+        referee_field = Hidden('referee', self._parent.referee)
+        hPanel.add(self.filename_field)
+        hPanel.add(referee_field)
+        vPanel.add(hPanel)
+
+        self.add(vPanel)
+        self.addFormHandler(self)
+
+    def updateStatus(self):
+        if self.timer_on:
+            return
+        # TODO: the status should be different if a song is being played or not
+        queue = self._parent.getQueueSize()
+        queue_data = self._parent.queue_data
+        if queue < queue_data[0]:
+            left = queue_data[0] - queue
+            self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else ""))
+        elif queue < queue_data[1]:
+            left = queue_data[1] - queue
+            self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else ""))
+        elif queue >= queue_data[1]:
+                self.status.setText("[The queue is currently full]")
+        self.status.setStyleName('radiocol_status')
+
+    def onBtnClick(self):
+        if self.file_upload.check():
+            self.status.setText('[Submitting, please wait...]')
+            self.filename_field.setValue(self.file_upload.getFilename())
+            if self.file_upload.getFilename().lower().endswith('.mp3'):
+                self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000)
+            self.submit()
+            self.file_upload.setFilename("")
+
+    def onSubmit(self, event):
+        pass
+
+    def blockUpload(self):
+        self.file_upload.setVisible(False)
+        self.upload_btn.setEnabled(False)
+
+    def unblockUpload(self):
+        self.file_upload.setVisible(True)
+        self.upload_btn.setEnabled(True)
+
+    def setTemporaryStatus(self, text, style):
+        self.status.setText(text)
+        self.status.setStyleName('radiocol_upload_status_%s' % style)
+        self.timer_on = True
+
+        def cb(timer):
+            self.timer_on = False
+            self.updateStatus()
+
+        Timer(5000, cb)
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result == C.UPLOAD_OK:
+            # the song can still be rejected (not readable, full queue...)
+            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
+        elif result == C.UPLOAD_KO:
+            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
+            self._parent.radiocolSongRejectedHandler(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted."))
+            # TODO: would be great to re-use the original Exception class and message
+            # but it is lost in the middle of the traceback and encapsulated within
+            # a DBusException instance --> extract the data from the traceback?
+        else:
+            Window.alert(_('Submit error: %s' % result))
+            self.status.setText('')
+
+
+class Player(Audio):
+
+    def __init__(self, player_id, metadata_panel):
+        Audio.__init__(self)
+        self._id = player_id
+        self.metadata = metadata_panel
+        self.timestamp = ""
+        self.title = ""
+        self.artist = ""
+        self.album = ""
+        self.filename = None
+        self.played = False  # True when the song is playing/has played, becomes False on preload
+        self.setAutobuffer(True)
+        self.setAutoplay(False)
+        self.setVisible(False)
+
+    def preload(self, timestamp, filename, title, artist, album):
+        """preload the song but doesn't play it"""
+        self.timestamp = timestamp
+        self.filename = filename
+        self.title = title
+        self.artist = artist
+        self.album = album
+        self.played = False
+        self.setSrc("radiocol/%s" % html_tools.html_sanitize(filename))
+        log.debug("preloading %s in %s" % (title, self._id))
+
+    def play(self, play=True):
+        """Play or pause the song
+        @param play: set to True to play or to False to pause
+        """
+        if play:
+            self.played = True
+            self.metadata.setTitle(self.title)
+            self.metadata.setArtist(self.artist)
+            self.metadata.setAlbum(self.album)
+            Audio.play(self)
+        else:
+            self.pause()
+
+
+class RadioColPanel(HorizontalPanel, ClickHandler):
+
+    def __init__(self, parent, referee, players, queue_data):
+        """
+        @param parent
+        @param referee
+        @param players
+        @param queue_data: list of integers (queue to start, queue limit)
+        """
+        # We need to set it here and not in the CSS :(
+        HorizontalPanel.__init__(self, Height="90px")
+        ClickHandler.__init__(self)
+        self._parent = parent
+        self.referee = referee
+        self.queue_data = queue_data
+        self.setStyleName("radiocolPanel")
+
+        # Now we set up the layout
+        self.metadata_panel = MetadataPanel()
+        self.add(CaptionPanel("Now playing", self.metadata_panel))
+        self.playlist_panel = VerticalPanel()
+        self.add(CaptionPanel("Songs queue", self.playlist_panel))
+        self.control_panel = ControlPanel(self)
+        self.add(CaptionPanel("Controls", self.control_panel))
+
+        self.next_songs = []
+        self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)]
+        self.current_player = None
+        for player in self.players:
+            self.add(player)
+        self.addClickListener(self)
+
+        help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br />
+        Please do not submit files that are protected by copyright.<br />
+        Click <a style="color: red;">here</a> if you need some support :)"""
+        link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick)
+        self._parent.printInfo(help_msg, type_='link', link_cb=link_cb)
+
+    def pushNextSong(self, title):
+        """Add a song to the left panel's next songs queue"""
+        next_song = Label(title)
+        next_song.setStyleName("radiocol_next_song")
+        self.next_songs.append(next_song)
+        self.playlist_panel.append(next_song)
+        self.control_panel.updateStatus()
+
+    def popNextSong(self):
+        """Remove the first song of next songs list
+        should be called when the song is played"""
+        #FIXME: should check that the song we remove is the one we play
+        next_song = self.next_songs.pop(0)
+        self.playlist_panel.remove(next_song)
+        self.control_panel.updateStatus()
+
+    def getQueueSize(self):
+        return len(self.playlist_panel.getChildren())
+
+    def radiocolCheckPreload(self, timestamp):
+        for player in self.players:
+            if player.timestamp == timestamp:
+                return False
+        return True
+
+    def radiocolPreloadHandler(self, timestamp, filename, title, artist, album, sender):
+        if not self.radiocolCheckPreload(timestamp):
+            return  # song already preloaded
+        preloaded = False
+        for player in self.players:
+            if not player.filename or \
+               (player.played and player != self.current_player):
+                #if player has no file loaded, or it has already played its song
+                #we use it to preload the next one
+                player.preload(timestamp, filename, title, artist, album)
+                preloaded = True
+                break
+        if not preloaded:
+            log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1])
+        else:
+            self.pushNextSong(title)
+            self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title})
+
+    def radiocolPlayHandler(self, filename):
+        found = False
+        for player in self.players:
+            if not found and player.filename == filename:
+                player.play()
+                self.popNextSong()
+                self.current_player = player
+                found = True
+            else:
+                player.play(False)  # in case the previous player was not sync
+        if not found:
+            log.error("Song not found in queue, can't play it. This should not happen")
+
+    def radiocolNoUploadHandler(self):
+        self.control_panel.blockUpload()
+
+    def radiocolUploadOkHandler(self):
+        self.control_panel.unblockUpload()
+
+    def radiocolSongRejectedHandler(self, reason):
+        Window.alert("Song rejected: %s" % reason)
+
+
+##  Menu
+
+def hostReady(host):
+    def onCollectiveRadio(self):
+        def callback(room_jid, contacts):
+            contacts = [unicode(contact) for contact in contacts]
+            room_jid_s = unicode(room_jid) if room_jid else ''
+            host.bridge.RadioCollective(contacts, room_jid_s, profile=C.PROF_KEY_NONE)
+        dialog.RoomAndContactsChooser(host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
+
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Collective radio")), callback=onCollectiveRadio)
+
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/game_tarot.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,408 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _, D_
+from sat_frontends.tools.games import TarotCard
+from sat_frontends.tools import host_listener
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.DockPanel import DockPanel
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Label import Label
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui import HasAlignment
+from pyjamas import Window
+from pyjamas import DOM
+from constants import Const as C
+
+import dialog
+import xmlui
+
+
+CARD_WIDTH = 74
+CARD_HEIGHT = 136
+CARD_DELTA_Y = 30
+MIN_WIDTH = 950  # Minimum size of the panel
+MIN_HEIGHT = 500
+
+
+class CardWidget(TarotCard, Image, MouseHandler):
+    """This class is used to represent a card, graphically and logically"""
+
+    def __init__(self, parent, file_):
+        """@param file: path of the PNG file"""
+        self._parent = parent
+        Image.__init__(self, file_)
+        root_name = file_[file_.rfind("/") + 1:-4]
+        suit, value = root_name.split('_')
+        TarotCard.__init__(self, (suit, value))
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+
+    def onMouseEnter(self, sender):
+        if self._parent.state == "ecart" or self._parent.state == "play":
+            DOM.setStyleAttribute(self.getElement(), "top", "0px")
+
+    def onMouseLeave(self, sender):
+        if not self in self._parent.hand:
+            return
+        if not self in list(self._parent.selected):  # FIXME: Workaround pyjs bug, must report it
+            DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y)
+
+    def onMouseUp(self, sender, x, y):
+        if self._parent.state == "ecart":
+            if self not in list(self._parent.selected):
+                self._parent.addToSelection(self)
+            else:
+                self._parent.removeFromSelection(self)
+        elif self._parent.state == "play":
+            self._parent.playCard(self)
+
+
+class TarotPanel(DockPanel, ClickHandler):
+
+    def __init__(self, parent, referee, players):
+        DockPanel.__init__(self)
+        ClickHandler.__init__(self)
+        self._parent = parent
+        self._autoplay = None  # XXX: use 0 to activate fake play, None else
+        self.referee = referee
+        self.players = players
+        self.player_nick = parent.nick
+        self.bottom_nick = self.player_nick
+        idx = self.players.index(self.player_nick)
+        idx = (idx + 1) % len(self.players)
+        self.right_nick = self.players[idx]
+        idx = (idx + 1) % len(self.players)
+        self.top_nick = self.players[idx]
+        idx = (idx + 1) % len(self.players)
+        self.left_nick = self.players[idx]
+        self.bottom_nick = self.player_nick
+        self.selected = set()  # 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
+        self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT)
+        self.setStyleName("cardPanel")
+
+        # Now we set up the layout
+        _label = Label(self.top_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.NORTH)
+        self.setCellWidth(_label, '100%')
+        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER)
+
+        self.hand_panel = AbsolutePanel()
+        self.add(self.hand_panel, DockPanel.SOUTH)
+        self.setCellWidth(self.hand_panel, '100%')
+        self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER)
+
+        _label = Label(self.left_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.WEST)
+        self.setCellHeight(_label, '100%')
+        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
+
+        _label = Label(self.right_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.EAST)
+        self.setCellHeight(_label, '100%')
+        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT)
+        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
+
+        self.center_panel = DockPanel()
+        self.inner_left = SimplePanel()
+        self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_left, DockPanel.WEST)
+        self.center_panel.setCellHeight(self.inner_left, '100%')
+        self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT)
+        self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE)
+
+        self.inner_right = SimplePanel()
+        self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_right, DockPanel.EAST)
+        self.center_panel.setCellHeight(self.inner_right, '100%')
+        self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE)
+
+        self.inner_top = SimplePanel()
+        self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_top, DockPanel.NORTH)
+        self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM)
+
+        self.inner_bottom = SimplePanel()
+        self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_bottom, DockPanel.SOUTH)
+        self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP)
+
+        self.inner_center = SimplePanel()
+        self.center_panel.add(self.inner_center, DockPanel.CENTER)
+        self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE)
+
+        self.add(self.center_panel, DockPanel.CENTER)
+        self.setCellWidth(self.center_panel, '100%')
+        self.setCellHeight(self.center_panel, '100%')
+        self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE)
+        self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER)
+
+        self.loadCards()
+        self.mouse_over_card = None  # contain the card to highlight
+        self.visible_size = CARD_WIDTH / 2  # number of pixels visible for cards
+        self.addClickListener(self)
+
+    def loadCards(self):
+        """Load all the cards in memory"""
+        def _getTarotCardsPathsCb(paths):
+            log.debug("_getTarotCardsPathsCb")
+            for file_ in paths:
+                log.debug("path: %s" % file_)
+                card = CardWidget(self, file_)
+                log.debug("card: %s" % card)
+                self.cards[(card.suit, card.value)] = card
+                self.deck.append(card)
+            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
+        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
+        self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb)
+
+    def onClick(self, sender):
+        if self.state == "chien":
+            self.to_show = []
+            self.state = "wait"
+            self.updateToShow()
+        elif self.state == "wait_for_ecart":
+            self.state = "ecart"
+            self.hand.extend(self.to_show)
+            self.hand.sort()
+            self.to_show = []
+            self.updateToShow()
+            self.updateHand()
+
+    def tarotGameNewHandler(self, hand):
+        """Start a new game, with given hand"""
+        if hand is []:  # reset the display after the scores have been showed
+            self.selected.clear()
+            del self.hand[:]
+            del self.to_show[:]
+            self.state = None
+            #empty hand
+            self.updateHand()
+            #nothing on the table
+            self.updateToShow()
+            for pos in ['top', 'left', 'bottom', 'right']:
+                getattr(self, "inner_%s" % pos).setWidget(None)
+            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
+            return
+        for suit, value in hand:
+            self.hand.append(self.cards[(suit, value)])
+        self.hand.sort()
+        self.state = "init"
+        self.updateHand()
+
+    def updateHand(self):
+        """Show the cards in the hand in the hand_panel (SOUTH panel)"""
+        self.hand_panel.clear()
+        self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10))
+        x_pos = 0
+        y_pos = CARD_DELTA_Y
+        for card in self.hand:
+            self.hand_panel.add(card, x_pos, y_pos)
+            x_pos += self.visible_size
+
+    def updateToShow(self):
+        """Show cards in the center panel"""
+        if not self.to_show:
+            _widget = self.inner_center.getWidget()
+            if _widget:
+                self.inner_center.remove(_widget)
+            return
+        panel = AbsolutePanel()
+        panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT))
+        x_pos = 0
+        y_pos = 0
+        for card in self.to_show:
+            panel.add(card, x_pos, y_pos)
+            x_pos += CARD_WIDTH + 5
+        self.inner_center.setWidget(panel)
+
+    def _ecartConfirm(self, confirm):
+        if not confirm:
+            return
+        ecart = []
+        for card in self.selected:
+            ecart.append((card.suit, card.value))
+            self.hand.remove(card)
+        self.selected.clear()
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart)
+        self.state = "wait"
+        self.updateHand()
+
+    def addToSelection(self, card):
+        self.selected.add(card)
+        if len(self.selected) == 6:
+            dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").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"""
+
+        if phase == "play":
+            self.state = "play"
+        elif phase == "ecart":
+            self.state = "ecart"
+        else:
+            log.error("INTERNAL ERROR: unmanaged game phase")  # FIXME: raise an exception here
+
+        for suit, value in played_cards:
+            self.hand.append(self.cards[(suit, value)])
+
+        self.hand.sort()
+        self.updateHand()
+        if self._autoplay == None:  # No dialog if there is autoplay
+            Window.alert('Cards played are invalid !')
+        self.__fakePlay()
+
+    def removeFromSelection(self, card):
+        self.selected.remove(card)
+        if len(self.selected) == 6:
+            dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
+
+    def tarotGameChooseContratHandler(self, xml_data):
+        """Called when the player has to select his contrat
+        @param xml_data: SàT xml representation of the form"""
+        body = xmlui.create(self._parent.host, xml_data, flags=['NO_CANCEL'])
+        _dialog = dialog.GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.show()
+
+    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)])
+        self.updateToShow()
+        if game_stage == "chien" and data['attaquant'] == self.player_nick:
+            self.state = "wait_for_ecart"
+        else:
+            self.state = "chien"
+
+    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
+        log.error("This line should not be reached")
+
+    def tarotGameCardsPlayedHandler(self, player, cards):
+        """A card has been played by player"""
+        if not len(cards):
+            log.warning("cards should not be empty")
+            return
+        if len(cards) > 1:
+            log.error("can't manage several cards played")
+        if self.to_show:
+            self.to_show = []
+            self.updateToShow()
+        suit, value = cards[0]
+        player_pos = self.getPlayerLocation(player)
+        player_panel = getattr(self, "inner_%s" % player_pos)
+
+        if player_panel.getWidget() != None:
+            #We have already cards on the table, we remove them
+            for pos in ['top', 'left', 'bottom', 'right']:
+                getattr(self, "inner_%s" % pos).setWidget(None)
+
+        card = self.cards[(suit, value)]
+        DOM.setElemAttribute(card.getElement(), "style", "")
+        player_panel.setWidget(card)
+
+    def tarotGameYourTurnHandler(self):
+        """Called when we have to play :)"""
+        if self.state == "chien":
+            self.to_show = []
+            self.updateToShow()
+        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.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
+        del self.hand[self._autoplay]
+        self.state = "wait"
+        self._autoplay += 1
+
+    def playCard(self, card):
+        self.hand.remove(card)
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
+        self.state = "wait"
+        self.updateHand()
+
+    def tarotGameScoreHandler(self, xml_data, winners, loosers):
+        """Show score at the end of a round"""
+        if not winners and not loosers:
+            title = "Draw game"
+        else:
+            if self.player_nick in winners:
+                title = "You <b>win</b> !"
+            else:
+                title = "You <b>loose</b> :("
+        body = xmlui.create(self._parent.host, xml_data, title=title, flags=['NO_CANCEL'])
+        _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.show()
+
+
+##  Menu
+
+def hostReady(host):
+    def onTarotGame():
+        def onPlayersSelected(room_jid, other_players):
+            other_players = [unicode(contact) for contact in other_players]
+            room_jid_s = unicode(room_jid) if room_jid else ''
+            host.bridge.launchTarotGame(other_players, room_jid_s, profile=C.PROF_KEY_NONE)
+        dialog.RoomAndContactsChooser(host, onPlayersSelected, 3, title="Tarot", title_invite=_(u"Please select 3 other players"), visible=(False, True))
+
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Games"), D_(u"Tarot")), callback=onTarotGame)
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)
--- a/src/browser/sat_browser/jid.py	Thu Feb 05 12:05:32 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 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/>.
-
-# FIXME: Libervia should use sat_frontends.tools.jid but pyjamas doesn't
-# know the unicode type and also experiences issues with __new__.
-
-
-class JID(object):
-    """This class help manage JID (Node@Domaine/Resource)"""
-
-    def __init__(self, jid):
-        assert(isinstance(jid, str))
-        self.__raw = str(JID.__normalize(jid))
-        self.__parse()
-
-    @classmethod
-    def __normalize(cls, jid):
-        """Naive normalization before instantiating and parsing the JID"""
-        if not jid:
-            return jid
-        tokens = jid.split('/')
-        tokens[0] = tokens[0].lower()  # force node and domain to lower-case
-        return '/'.join(tokens)
-
-    @property
-    def node(self):
-        return self.__node
-
-    @property
-    def domain(self):
-        return self.__domain
-
-    @property
-    def resource(self):
-        return self.__resource
-
-    @property
-    def bare(self):
-        return self.domain if not self.node else (self.node + "@" + self.domain)
-
-    # XXX: Avoid property setters, Pyjamas doesn't seem to handle them well. It seems
-    # that it will just naively assign the value without actually calling the method.
-    # FIXME: find a way to raise an error if the (undefined!) setter is called.
-    def setNode(self, node):
-        self.__node = node
-        self.__build()
-
-    def setDomain(self, domain):
-        self.__domain = domain
-        self.__build()
-
-    def setResource(self, resource):
-        self.__resource = resource
-        self.__build()
-
-    def setBare(self, bare):
-        self.__parseBare(bare)
-        self.__build()
-
-    def __build(self):
-        """Build the JID string from the node, domain and resource"""
-        self.__raw = self.bare if not self.resource else (self.bare + '/' + self.resource)
-
-    def __parse(self):
-        """Parse the JID string to extract the node, domain and resource"""
-        tokens = self.__raw.split('/')
-        bare, self.__resource = (tokens[0], tokens[1]) if len(tokens) > 1 else (self.__raw, '')
-        self.__parseBare(bare)
-
-    def __parseBare(self, bare):
-        """Parse the given JID bare to extract the node and domain
-
-        @param bare (str): JID bare to parse
-        """
-        tokens = bare.split('@')
-        self.__node, self.__domain = (tokens[0], tokens[1]) if len(tokens) > 1 else ('', bare)
-
-    def __str__(self):
-        try:
-            return self.__raw
-        except AttributeError:
-            raise AttributeError("Trying to output a JID which has not been parsed yet")
-
-    def is_valid(self):
-        """
-        @return: True if the JID is XMPP compliant
-        """
-        return self.domain != ''
-
-    def __eq__(self, other):
-        """Redefine equality operator to implement the naturally expected behavior"""
-        return self.__raw == other.__raw
-
-    def __hash__(self):
-        """Redefine hash to implement the naturally expected behavior"""
-        return hash(self.__raw)
-
-    def full(self):
-        return str(self)
-
-    def bareJID(self):
-        return JID(self.bare)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/json.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,269 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+
+
+### logging configuration ###
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from pyjamas.Timer import Timer
+from pyjamas import Window
+from pyjamas import JSONService
+
+from sat_browser.constants import Const as C
+
+
+class LiberviaMethodProxy(object):
+    """This class manage calling for one method"""
+
+    def __init__(self, parent, method):
+        self._parent = parent
+        self._method = method
+
+    def call(self, *args, **kwargs):
+        """Method called when self._method attribue is used in JSON_PROXY_PARENT
+
+        This method manage callback/errback in kwargs, and profile(_key) removing
+        @param *args: positional arguments of self._method
+        @param **kwargs: keyword arguments of self._method
+        """
+        callback=kwargs.pop('callback', None)
+        errback=kwargs.pop('errback', None)
+
+        # as profile is linked to browser session and managed server side, we remove them
+        profile_removed = False
+        try:
+            kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised with del
+            del kwargs['profile']
+            profile_removed = True
+        except KeyError:
+            pass
+
+        try:
+            kwargs['profile_key'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del
+            del kwargs['profile_key']
+            profile_removed = True
+        except KeyError:
+            pass
+
+        if not profile_removed and args:
+            # if profile was not in kwargs, there is most probably one in args
+            args = list(args)
+            assert isinstance(args[-1], basestring) # Detect when we want to remove a callback (or something else) instead of the profile
+            del args[-1]
+
+        if kwargs:
+            # kwargs should be empty here, we don't manage keyword arguments on bridge calls
+            log.error("kwargs is not empty after treatment on method call: kwargs={}".format(kwargs))
+
+        id_ = self._parent.callMethod(self._method, args)
+
+        # callback or errback are managed in parent LiberviaJsonProxy with call id
+        if callback is not None:
+            self._parent.cb[id_] = callback
+        if errback is not None:
+            self._parent.eb[id_] = errback
+
+
+class LiberviaJsonProxy(JSONService.JSONService):
+
+    def __init__(self, url, methods):
+        self._serviceURL = url
+        self.methods = methods
+        JSONService.JSONService.__init__(self, url, self)
+        self.cb = {}
+        self.eb = {}
+        self._registerMethods(methods)
+
+    def _registerMethods(self, methods):
+        if methods:
+            for method in methods:
+                log.debug("Registering JSON method call [{}]".format(method))
+                setattr(self,
+                        method,
+                        getattr(LiberviaMethodProxy(self, method), 'call')
+                       )
+
+    def callMethod(self, method, params, handler = None):
+        ret = super(LiberviaJsonProxy, self).callMethod(method, params, handler)
+        return ret
+
+    def call(self, method, cb, *args):
+        # FIXME: deprecated call method, must be removed once it's not used anymore
+        id_ = self.callMethod(method, args)
+        log.debug("call: method={} [id={}], args={}".format(method, id_, args))
+        if cb:
+            if isinstance(cb, tuple):
+                if len(cb) != 2:
+                    log.error("tuple syntax for bridge.call is (callback, errback), aborting")
+                    return
+                if cb[0] is not None:
+                    self.cb[id_] = cb[0]
+                self.eb[id_] = cb[1]
+            else:
+                self.cb[id_] = cb
+
+    def onRemoteResponse(self, response, request_info):
+        try:
+            _cb = self.cb[request_info.id]
+        except KeyError:
+            pass
+        else:
+            # if isinstance(_cb, tuple):
+            #     #we have arguments attached to the callback
+            #     #we send them after the answer
+            #     callback, args = _cb
+            #     callback(response, *args)
+            # else:
+            #     #No additional argument, we call directly the callback
+            _cb(response)
+            del self.cb[request_info.id]
+
+        try:
+            del self.eb[request_info.id]
+        except KeyError:
+            pass
+
+    def onRemoteError(self, code, errobj, request_info):
+        """def dump(obj):
+            print "\n\nDUMPING %s\n\n" % obj
+            for i in dir(obj):
+                print "%s: %s" % (i, getattr(obj,i))"""
+        try:
+            _eb = self.eb[request_info.id]
+        except KeyError:
+            if code != 0:
+                log.error("Internal server error")
+                """for o in code, error, request_info:
+                    dump(o)"""
+            else:
+                if isinstance(errobj['message'], dict):
+                    log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
+                else:
+                    log.error("%s" % errobj['message'])
+        else:
+            _eb((code, errobj))
+            del self.eb[request_info.id]
+
+        try:
+            del self.cb[request_info.id]
+        except KeyError:
+            pass
+
+
+class RegisterCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/register_api",
+                        ["isRegistered", "isConnected", "asyncConnect", "registerParams", "getMenus"])
+
+
+class BridgeCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/json_api",
+                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
+                         "getMblogs", "getMassiveMblogs", "getMblogComments",
+                         "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined",
+                         "getRoomsSubjects", "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
+                         "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments",
+                         "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
+                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
+                         "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer",
+                         "syntaxConvert", "getAccountDialogUI", "getMainResource", "getWaitingConf", "getEntitiesData",
+                        ])
+    def __call__(self, *args, **kwargs):
+        return LiberviaJsonProxy.__call__(self, *args, **kwargs)
+
+    def getConfig(self, dummy1, dummy2): # FIXME
+        log.warning("getConfig is not implemeted in Libervia yet")
+        return ''
+
+    def isConnected(self, dummy): # FIXME
+        log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened")
+        return True
+
+    def getAvatarFile(self, hash_, callback=None):
+        log.warning("getAvatarFile only return hash in Libervia")
+        if callback is not None:
+            callback(hash_)
+        return hash_
+
+
+class BridgeSignals(LiberviaJsonProxy):
+    RETRY_BASE_DELAY = 1000
+
+    def __init__(self, host):
+        self.host = host
+        self.retry_delay = self.RETRY_BASE_DELAY
+        LiberviaJsonProxy.__init__(self, "/json_signal_api",
+                        ["getSignals"])
+        self._signals = {} # key: signal name, value: callback
+
+    def onRemoteResponse(self, response, request_info):
+        if self.retry_delay != self.RETRY_BASE_DELAY:
+            log.info("Connection with server restablished")
+        self.retry_delay = self.RETRY_BASE_DELAY
+        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)
+
+    def onRemoteError(self, code, errobj, request_info):
+        if errobj['message'] == 'Empty Response':
+            log.warning(u"Empty reponse bridgeSignal\ncode={}\nrequest_info: id={} method={} handler={}".format(code, request_info.id, request_info.method, request_info.handler))
+            # FIXME: to check/replace by a proper session end on disconnected signal
+            # Window.getLocation().reload()  # XXX: reset page in case of session ended.
+                                           # FIXME: Should be done more properly without hard reload
+        LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info)
+        #we now try to reconnect
+        if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0:
+            Window.alert('You are not allowed to connect to server')
+        else:
+            def _timerCb(timer):
+                log.info("Trying to reconnect to server...")
+                self.getSignals(callback=self.signalHandler)
+            # TODO: display a notification to user
+            log.warning("Lost connection, trying to reconnect in {} s".format(self.retry_delay/1000))
+            Timer(notify=_timerCb).schedule(self.retry_delay)
+            self.retry_delay *= 2
+
+    def register(self, name, callback, with_profile=True):
+        """Register a signal
+
+        @param: name of the signal to register
+        @param callback: method to call
+        @param with_profile: True if the original bridge method need a profile
+        """
+        log.debug("Registering signal {}".format(name))
+        if name in self._signals:
+            log.error("Trying to register and already registered signal ({})".format(name))
+        else:
+            self._signals[name] = (callback, with_profile)
+
+    def signalHandler(self,  signal_data):
+        self.getSignals(callback=self.signalHandler)
+        if len(signal_data) == 1:
+            signal_data.append([])
+        log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1]))
+        name, args = signal_data
+        try:
+            callback, with_profile = self._signals[name]
+        except KeyError:
+            log.warning("Ignoring {} signal: no handler registered !".format(name))
+            return
+        if with_profile:
+            args.append(C.PROF_KEY_NONE)
+        callback(*args)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/libervia_widget.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,824 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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/>.
+"""Libervia base widget"""
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _
+from sat.core import exceptions
+from sat_frontends.quick_frontend import quick_widgets
+
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Widget import Widget
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui import HasAlignment
+from pyjamas.ui.DragWidget import DragWidget
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas import DOM
+from pyjamas import Window
+
+import dialog
+import base_menu
+import base_widget
+import base_panel
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+# FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell).
+# TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules
+
+
+### Drag n Drop ###
+
+
+class DragLabel(DragWidget):
+
+    def __init__(self, text, type_, host=None):
+        """Base of Drag n Drop mecanism in Libervia
+
+        @param text: data embedded with in drag n drop operation
+        @param type_: type of data that we are dragging
+        @param host: if not None, the host will be use to highlight BorderWidgets
+        """
+        DragWidget.__init__(self)
+        self.host = host
+        self._text = text
+        self.type_ = type_
+
+    def onDragStart(self, event):
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self._text, self.type_))
+        dt.setDragImage(self.getElement(), 15, 15)
+        if self.host is not None:
+            current_panel = self.host.tab_panel.getCurrentPanel()
+            for widget in current_panel.widgets:
+                if isinstance(widget, BorderWidget):
+                    widget.addStyleName('borderWidgetOnDrag')
+
+    def onDragEnd(self, event):
+        if self.host is not None:
+            current_panel = self.host.tab_panel.getCurrentPanel()
+            for widget in current_panel.widgets:
+                if isinstance(widget, BorderWidget):
+                    widget.removeStyleName('borderWidgetOnDrag')
+
+
+class LiberviaDragWidget(DragLabel):
+    """ A DragLabel which keep the widget being dragged as class value """
+    current = None  # widget currently dragged
+
+    def __init__(self, text, type_, widget):
+        DragLabel.__init__(self, text, type_, widget.host)
+        self.widget = widget
+
+    def onDragStart(self, event):
+        LiberviaDragWidget.current = self.widget
+        DragLabel.onDragStart(self, event)
+
+    def onDragEnd(self, event):
+        DragLabel.onDragEnd(self, event)
+        LiberviaDragWidget.current = None
+
+
+class DropCell(DropWidget):
+    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
+    drop_keys = {}
+
+    def __init__(self, host):
+        DropWidget.__init__(self)
+        self.host = host
+        self.setStyleName('dropCell')
+
+    @classmethod
+    def addDropKey(cls, key, cb):
+        """Add a association between a key and a class to create on drop.
+
+        @param key: key to be associated (e.g. "CONTACT", "CHAT")
+        @param cb: a callable (either a class or method) returning a
+            LiberviaWidget instance
+        """
+        DropCell.drop_keys[key] = cb
+
+    def onDragEnter(self, event):
+        if self == LiberviaDragWidget.current:
+            return
+        self.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
+            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
+            # We check that we are inside widget's box, and we don't remove the style in this case because
+            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
+            # don't want that
+            self.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def _getCellAndRow(self, grid, event):
+        """Return cell and row index where the event is occuring"""
+        cell = grid.getEventTargetCell(event)
+        row = DOM.getParent(cell)
+        return (row.rowIndex, cell.cellIndex)
+
+    def onDrop(self, event):
+        """
+        @raise NoLiberviaWidgetException: something else than a LiberviaWidget
+            has been returned by the callback.
+        """
+        self.removeStyleName('dragover')
+        DOM.eventPreventDefault(event)
+        dt = event.dataTransfer
+        # 'text', 'text/plain', and 'Text' are equivalent.
+        try:
+            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
+            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
+                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
+            # item_type = dt.getData("type")
+            log.debug("message: %s" % item)
+            log.debug("type: %s" % item_type)
+        except:
+            log.debug("no message found")
+            item = '&nbsp;'
+            item_type = None
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            if self == _new_panel:  # We can't drop on ourself
+                return
+            # we need to remove the widget from the panel as it will be inserted elsewhere
+            widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
+            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
+            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
+            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
+                # the dropped widget is the only one in the same row
+                # as the target widget (self), we don't do anything
+                return
+            widgets_panel.removeWidget(_new_panel)
+        elif item_type in self.drop_keys:
+            _new_panel = self.drop_keys[item_type](self.host, item)
+            if not isinstance(_new_panel, LiberviaWidget):
+                raise base_widget.NoLiberviaWidgetException
+        else:
+            log.warning("unmanaged item type")
+            return
+        if isinstance(self, LiberviaWidget):
+            # self.host.unregisterWidget(self) # FIXME
+            self.onQuit()