changeset 467:97c72fe4a5f2

browser_side: import fixes: - moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly - refactored bad import practices: classes are most of time not imported directly, module is imported instead.
author Goffi <goffi@goffi.org>
date Mon, 09 Jun 2014 22:15:26 +0200
parents 01880aa8ea2d
children 830b50593597
files src/browser/__init__.py src/browser/base_panels.py src/browser/base_widget.py src/browser/card_game.py src/browser/constants.py src/browser/contact.py src/browser/contact_group.py src/browser/dialog.py src/browser/file_tools.py src/browser/html_tools.py src/browser/jid.py src/browser/libervia_main.py src/browser/list_manager.py src/browser/logging.py src/browser/menu.py src/browser/nativedom.py src/browser/notification.py src/browser/panels.py src/browser/plugin_xep_0085.py src/browser/radiocol.py src/browser/register.py src/browser/richtext.py src/browser/sat_browser/__init__.py src/browser/sat_browser/base_panels.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/card_game.py src/browser/sat_browser/constants.py src/browser/sat_browser/contact.py src/browser/sat_browser/contact_group.py src/browser/sat_browser/dialog.py src/browser/sat_browser/file_tools.py src/browser/sat_browser/html_tools.py src/browser/sat_browser/jid.py src/browser/sat_browser/list_manager.py src/browser/sat_browser/logging.py src/browser/sat_browser/menu.py src/browser/sat_browser/nativedom.py src/browser/sat_browser/notification.py src/browser/sat_browser/panels.py src/browser/sat_browser/plugin_xep_0085.py src/browser/sat_browser/radiocol.py src/browser/sat_browser/register.py src/browser/sat_browser/richtext.py src/browser/sat_browser/xmlui.py src/browser/xmlui.py
diffstat 43 files changed, 7446 insertions(+), 7448 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/base_panels.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,609 +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.strings import addURLToText, addURLToImage
-
-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
-
-from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML
-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_sanitize(nick),
-                            "msg_class": ' '.join(_msg_class),
-                            "msg": addURLToText(html_sanitize(msg)) if not xhtml else 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)
-        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("<div class='occupant'>%s%s%s</div>" % (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):
-        _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 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 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_sanitize(html_strip(text)) if self.options['no_xhtml'] else 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 = addURLToImage(text)
-        if self.options['enhance_display']:
-            text = addURLToText(text)
-        self.display.setHTML(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:
-            self.textarea.setCursorPos(len(self.textarea.getText()))
-        self.textarea.setFocus(focus)
--- a/src/browser/base_widget.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,732 +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.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.Button import Button
-from pyjamas.ui.Image import Image
-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
-
-
-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
-
-
-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
-
-    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 WidgetHeader(AbsolutePanel, LiberviaDragWidget):
-
-    def __init__(self, parent, title):
-        AbsolutePanel.__init__(self)
-        self.add(title)
-        button_group_wrapper = SimplePanel()
-        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
-        button_group = HorizontalPanel()
-        button_group.setStyleName('widgetHeader_buttonGroup')
-        setting_button = Image("media/icons/misc/settings.png")
-        setting_button.setStyleName('widgetHeader_settingButton')
-        setting_button.addClickListener(parent.onSetting)
-        close_button = Image("media/icons/misc/close.png")
-        close_button.setStyleName('widgetHeader_closeButton')
-        close_button.addClickListener(parent.onClose)
-        button_group.add(setting_button)
-        button_group.add(close_button)
-        button_group_wrapper.setWidget(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='', selectable=False):
-        """Init the widget
-        @param host: SatWebFrontend object
-        @param title: title show in the header of the widget
-        @param selectable: 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')
-        self._close_listeners = []
-        header = WidgetHeader(self, self.__title)
-        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.debug("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 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, entity):
-        """This method should be overwritten by child classes."""
-        raise NotImplementedError
-
-
-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)
--- a/src/browser/card_game.py	Mon Jun 09 20:37:28 2014 +0200
+++ /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
-
-from dialog import ConfirmDialog, GenericDialog
-from xmlui 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:", file_)
-                card = CardWidget(self, file_)
-                log.debug("card:", 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:
-            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:
-            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(self._parent.host, xml_data, flags=['NO_CANCEL'])
-        _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(self._parent.host, xml_data, title=title, flags=['NO_CANCEL'])
-        _dialog = GenericDialog(title, body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.show()
--- a/src/browser/constants.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from libervia.common.constants import Const as C
-
-
-# Auxiliary functions
-param_to_bool = lambda value: value == 'true'
-
-
-class Const(C):
-    """Add here the constants that are only used by the browser side."""
-
-    # Parameters that have an incidence on UI display/refresh:
-    #     - they can be any parameter (not necessarily specific to Libervia)
-    #     - 'cast_from_str' is a method used to eventually convert to a non string type
-    #     - 'initial_value' is used to initialize the display before any profile connection
-    UI_PARAMS = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM,
-                            "category": C.COMPOSITION_KEY,
-                            "cast_from_str": param_to_bool,
-                            "initial_value": False
-                             },
-                 }
--- a/src/browser/contact.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,419 +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.ClickListener import ClickHandler
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas import Window
-from pyjamas import DOM
-from __pyjamas__ import doc
-
-from jid import JID
-
-from base_panels import PopupMenuPanel
-from base_widget import DragLabel
-from panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel
-from html_tools import html_sanitize
-
-
-def setPresenceStyle(element, presence, base_style="contact"):
-    """
-    Set the CSS style of a contact's element according to its presence.
-    @param item: the UI element of the contact
-    @param presence: a value in ("", "chat", "away", "dnd", "xa").
-    @param base_style: the base name of the style to apply
-    """
-    if not hasattr(element, 'presence_style'):
-        element.presence_style = None
-    style = '%s-%s' % (base_style, presence or 'connected')
-    if style == element.presence_style:
-        return
-    if element.presence_style is not None:
-        element.removeStyleName(element.presence_style)
-    element.addStyleName(style)
-    element.presence_style = style
-
-
-class GroupLabel(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')
-        DragLabel.__init__(self, group, "GROUP")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group)
-
-
-class ContactLabel(DragLabel, HTML, ClickHandler):
-    def __init__(self, host, jid, name=None, handleClick=True):
-        HTML.__init__(self)
-        self.host = host
-        self.name = name or jid
-        self.waiting = False
-        self.jid = jid
-        self._fill()
-        self.setStyleName('contact')
-        DragLabel.__init__(self, jid, "CONTACT")
-        if handleClick:
-            ClickHandler.__init__(self)
-            self.addClickListener(self)
-
-    def _fill(self):
-        if self.waiting:
-            _wait_html = "<b>(*)</b>&nbsp;"
-        self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html,
-                                           'name': 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._fill()
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(ChatPanel, 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)
-
-
-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, handleClick=False):
-        VerticalPanel.__init__(self)
-        self.host = host
-        self.contacts = []
-        self.handleClick = handleClick
-
-    def add(self, jid, name=None, item_cb=None):
-        if jid in self.contacts:
-            return
-        index = 0
-        for contact_ in self.contacts:
-            if contact_ > jid:
-                break
-            index += 1
-        self.contacts.insert(index, jid)
-        _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick)
-        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
-        VerticalPanel.insert(self, _item, index)
-        if item_cb is not None:
-            item_cb(_item)
-
-    def remove(self, jid):
-        wid = self.getContactLabel(jid)
-        if not wid:
-            return
-        VerticalPanel.remove(self, wid)
-        self.contacts.remove(jid)
-
-    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 getContactLabel(self, contact_jid):
-        """get contactList widget of a contact
-        @return: ContactLabel item if present, else None"""
-        for wid in self:
-            if isinstance(wid, ContactLabel) and wid.jid == contact_jid:
-                return wid
-        return None
-
-
-class ContactList(GenericContactList):
-    """The contact list that is displayed on the left side."""
-
-    def __init__(self, host):
-        GenericContactList.__init__(self, host, handleClick=True)
-        self.menu_entries = {"blog": {"title": "Public blog..."}}
-        self.context_menu = PopupMenuPanel(entries=self.menu_entries,
-                                           hide=self.contextMenuHide,
-                                           callback=self.contextMenuCallback,
-                                           vertical=False, style={"selected": "menu-selected"})
-
-    def contextMenuHide(self, sender, key):
-        """Return True if the item for that sender should be hidden."""
-        # TODO: enable the blogs of users that are on another server
-        return JID(sender.jid).domain != self.host._defaultDomain
-
-    def contextMenuCallback(self, sender, key):
-        if key == "blog":
-            # TODO: use the bare when all blogs can be retrieved
-            node = JID(sender.jid).node
-            web_panel = WebPanel(self.host, "/blog/%s" % node)
-            self.host.addTab("%s's blog" % node, web_panel)
-        else:
-            sender.onClick(sender)
-
-    def add(self, jid_s, name=None):
-        """Add a contact
-
-        @param jid_s (str): JID as unicode
-        @param name (str): nickname
-        """
-        def item_cb(item):
-            self.context_menu.registerRightClickSender(item)
-        GenericContactList.add(self, jid_s, name, item_cb)
-
-    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"""
-        _item = self.getContactLabel(jid)
-        if _item:
-            if type_ == 'availability':
-                setPresenceStyle(_item, state)
-            elif type_ == 'messageWaiting':
-                _item.setMessageWaiting(state)
-
-
-class ContactTitleLabel(DragLabel, Label, ClickHandler):
-    def __init__(self, host, text):
-        Label.__init__(self, text)  # , Element=DOM.createElement('div')
-        self.host = host
-        self.setStyleName('contactTitle')
-        DragLabel.__init__(self, text, "CONTACT_TITLE")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(MicroblogPanel, 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('contactBox')
-        Window.addWindowResizeListener(self)
-
-    def onWindowResized(self, width, height):
-        contact_panel_elt = self.getElement()
-        classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), 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 not group 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)
-
-    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, resource, availability, priority, statuses):
-        """Set connection status
-        @param jid: JID userhost as unicode
-        """
-        if availability == 'unavailable':
-            if jid in self.connected:
-                if resource in self.connected[jid]:
-                    del self.connected[jid][resource]
-                if not self.connected[jid]:
-                    del self.connected[jid]
-        else:
-            if not jid in self.connected:
-                self.connected[jid] = {}
-            self.connected[jid][resource] = (availability, priority, statuses)
-
-        # check if the contact is connected with another resource, use the one with highest priority
-        if jid in self.connected:
-            max_resource = max_priority = None
-            for tmp_resource in self.connected[jid]:
-                if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority:
-                    max_resource = tmp_resource
-                    max_priority = self.connected[jid][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][max_resource][0]
-        if jid 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, "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()
-
-    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_label in self._contact_list:
-            if contact_jid == _contact_label.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.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.removeStyleName("selected")
--- a/src/browser/contact_group.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,237 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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 pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.DockPanel import DockPanel
-from pyjamas.Timer import Timer
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui import HasAlignment
-
-from dialog import ConfirmDialog, InfoDialog
-from list_manager import ListManager
-import dialog
-import contact
-
-
-class ContactGroupManager(ListManager):
-    """A manager for sub-panels to assign contacts to each group."""
-
-    def __init__(self, parent, keys_dict, contact_list, offsets, style):
-        ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style)
-        self.registerPopupMenuPanel(entries={"Remove group": {}},
-                                    callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
-
-    def removeContactKey(self, sender, key):
-        key = sender.getText()
-
-        def confirm_cb(answer):
-            if answer:
-                ListManager.removeContactKey(self, key)
-                self._parent.removeKeyFromAddGroupPanel(key)
-
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key)
-        _dialog.show()
-
-    def removeFromRemainingList(self, contacts):
-        ListManager.removeFromRemainingList(self, contacts)
-        self._parent.updateContactList(contacts=contacts)
-
-    def addToRemainingList(self, contacts, ignore_key=None):
-        ListManager.addToRemainingList(self, contacts, ignore_key)
-        self._parent.updateContactList(contacts=contacts)
-
-
-class ContactGroupEditor(DockPanel):
-    """Panel for the contact groups manager."""
-
-    def __init__(self, host, parent=None, onCloseCallback=None):
-        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
-        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.add_group_panel = self.getAddGroupPanel(groups_list)
-        south_panel = self.getCloseSaveButtons()
-        center_panel = self.getContactGroupManager(groups_list)
-        east_panel = self.getContactList()
-
-        self.add(self.add_group_panel, DockPanel.CENTER)
-        self.add(east_panel, DockPanel.EAST)
-        self.add(center_panel, DockPanel.NORTH)
-        self.add(south_panel, DockPanel.SOUTH)
-
-        self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT)
-        self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP)
-        self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT)
-        self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP)
-        self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM)
-        self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT)
-        self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM)
-        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.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():
-            self.restore_contact_panel = True
-            self.host.panel._contactsSwitch()
-
-        parent.add(self)
-        parent.setVisible(True)
-        if isinstance(parent, DialogBox):
-            parent.center()
-
-    def getContactGroupManager(self, groups_list):
-        """Set the list manager for the groups"""
-        flex_table = FlexTable(len(groups_list), 2)
-        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
-        # 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 add_group_cb(text):
-            self.groups.addContactKey(text)
-            self.add_group_panel.textbox.setFocus(True)
-
-        add_group_panel = dialog.AddGroupPanel(groups_list, 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"""
-        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"""
-        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
-
-    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)
-
-    def updateContactList(self, sender=None, contacts=None):
-        """Update the contact list regarding the toggle button"""
-        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_)
-        else:
-            contacts = self.all_contacts
-        for contact_ in contacts:
-            if sender.showAll:
-                self.contacts.getContactLabel(contact_).setVisible(True)
-            else:
-                if contact_ in self.groups.remaining_list:
-                    self.contacts.getContactLabel(contact_).setVisible(True)
-                else:
-                    self.contacts.getContactLabel(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 self._on_close_callback is not None:
-            self._on_close_callback()
-        if self.restore_contact_panel:
-            self.host.panel._contactsSwitch()
-
-    def cancelWithoutSaving(self):
-        """Ask for confirmation before closing the dialog."""
-        def confirm_cb(answer):
-            if answer:
-                self.__close()
-
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?")
-        _dialog.show()
-
-    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:
-                    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))
-        self.__close()
--- a/src/browser/dialog.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,547 +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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools.misc import DEFAULT_MUC
-
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.Grid import Grid
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.PopupPanel import PopupPanel
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.Button import Button
-from pyjamas.ui.TextBox import TextBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.RadioButton import RadioButton
-from pyjamas.ui import HasAlignment
-from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER
-from pyjamas.ui.MouseListener import MouseWheelHandler
-from pyjamas import Window
-
-from base_panels import ToggleStackPanel
-
-
-# List here the patterns that are not allowed in contact group names
-FORBIDDEN_PATTERNS_IN_GROUP = ()
-
-
-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):
-        Grid.__init__(self, 2, 2, Width='100%')
-        self.host = host
-
-        self.new_radio = RadioButton("room", "Discussion room:")
-        self.new_radio.setChecked(True)
-        self.box = TextBox(Width='95%')
-        self.box.setText(self.GENERATE_MUC if default_room == "" else default_room)
-        self.exist_radio = RadioButton("room", "Already joined:")
-        self.rooms_list = ListBox(Width='95%')
-
-        self.add(self.new_radio, 0, 0)
-        self.add(self.box, 0, 1)
-        self.add(self.exist_radio, 1, 0)
-        self.add(self.rooms_list, 1, 1)
-
-        self.box.addFocusListener(self)
-        self.rooms_list.addFocusListener(self)
-
-        self.exist_radio.setVisible(False)
-        self.rooms_list.setVisible(False)
-        self.setRooms()
-
-    def onFocus(self, sender):
-        if sender == self.rooms_list:
-            self.exist_radio.setChecked(True)
-        elif sender == self.box:
-            if self.box.getText() == self.GENERATE_MUC:
-                self.box.setText("")
-            self.new_radio.setChecked(True)
-
-    def onLostFocus(self, sender):
-        if sender == self.box:
-            if self.box.getText() == "":
-                self.box.setText(self.GENERATE_MUC)
-
-    def setRooms(self):
-        for room in self.host.room_list:
-            self.rooms_list.addItem(room.bare)
-        if len(self.host.room_list) > 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"""
-
-    def __init__(self, host, nb_contact=None, ok_button=None):
-        """
-        @param host: SatWebFrontend instance
-        @param nb_contact: number of contacts that have to be selected, None for no limit
-        If a tuple is given instead of an integer, nb_contact[0] is the minimal and
-        nb_contact[1] is the maximal number of contacts to be chosen.
-        """
-        self.host = host
-        if isinstance(nb_contact, tuple):
-            if len(nb_contact) == 0:
-                nb_contact = None
-            elif len(nb_contact) == 1:
-                nb_contact = (nb_contact[0], nb_contact[0])
-        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")
-        else:
-            log.warning("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%')
-        self.contacts_list = ListBox()
-        self.contacts_list.setMultipleSelect(True)
-        self.contacts_list.setWidth("95%")
-        self.contacts_list.addStyleName('contactsChooser')
-        self.contacts_list.addChangeListener(self.onChange)
-        self.add(self.contacts_list)
-        self.setContacts()
-        self.onChange()
-
-    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]:
-                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
-        """
-        self.contacts_list.clear()
-        contacts = self.host.contact_panel.getConnected(filter_muc=True)
-        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)
-
-
-class RoomAndContactsChooser(DialogBox):
-    """Select a room and some users to invite in"""
-
-    def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups",
-                 title_room="Join room", title_invite="Invite contacts", visible=(True, True)):
-        DialogBox.__init__(self, centered=True)
-        self.host = host
-        self.callback = callback
-        self.title_room = title_room
-        self.title_invite = title_invite
-
-        button_panel = HorizontalPanel()
-        button_panel.addStyleName("marginAuto")
-        ok_button = Button("OK", self.onOK)
-        button_panel.add(ok_button)
-        button_panel.add(Button("Cancel", self.onCancel))
-
-        self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC)
-        self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
-
-        self.stack_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)
-        self.onStackChanged(self.stack_panel, 0, visible[0])
-        self.onStackChanged(self.stack_panel, 1, visible[1])
-
-        main_panel = VerticalPanel()
-        main_panel.setStyleName("room-contact-chooser")
-        main_panel.add(self.stack_panel)
-        main_panel.add(button_panel)
-
-        self.setWidget(main_panel)
-        self.setHTML(title)
-        self.show()
-
-        # needed to update the contacts list when someone logged in/out
-        self.host.room_contacts_chooser = self
-
-    def getRoom(self, asSuffix=False):
-        room = self.room_panel.getRoom()
-        if asSuffix:
-            return room if room == "" else ": %s" % room
-        else:
-            return room
-
-    def getContacts(self, asSuffix=False):
-        contacts = self.contact_panel.getContacts()
-        if asSuffix:
-            return "" if contacts == [] else ": %s" % ", ".join(contacts)
-        else:
-            return 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)))
-        elif index == 1:
-            sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True)))
-
-    def resetContacts(self):
-        """Called when someone log in/out to update the list"""
-        self.contact_panel.setContacts(self.getContacts())
-
-    def onOK(self, sender):
-        room_jid = self.getRoom()
-        if room_jid != "" and "@" not in room_jid:
-            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())
-
-    def onCancel(self, sender):
-        self.hide()
-
-    def hide(self):
-        self.host.room_contacts_chooser = None
-        DialogBox.hide(self, autoClosed=True)
-
-
-class GenericConfirmDialog(DialogBox):
-
-    def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs):
-        """
-        Dialog to confirm an action
-        @param widgets: widgets to attach
-        @param callback: method to call when a button is clicked
-        @param title: title of the dialog
-        @param prompt: textbox from which to retrieve the string value to be passed to the callback when
-        OK button is pressed. If None, OK button will return "True". Cancel button always returns "False".
-        """
-        self.callback = callback
-        DialogBox.__init__(self, centered=True, **kwargs)
-
-        content = VerticalPanel()
-        content.setWidth('100%')
-        for wid in widgets:
-            content.add(wid)
-            if wid == prompt:
-                wid.setWidth('100%')
-        button_panel = HorizontalPanel()
-        button_panel.addStyleName("marginAuto")
-        self.confirm_button = Button("OK", self.onConfirm)
-        button_panel.add(self.confirm_button)
-        self.cancel_button = Button("Cancel", self.onCancel)
-        button_panel.add(self.cancel_button)
-        content.add(button_panel)
-        self.setHTML(title)
-        self.setWidget(content)
-        self.prompt = prompt
-
-    def onConfirm(self, sender):
-        self.hide()
-        self.callback(self.prompt.getText() if self.prompt else True)
-
-    def onCancel(self, sender):
-        self.hide()
-        self.callback(False)
-
-    def show(self):
-        DialogBox.show(self)
-        if self.prompt:
-            self.prompt.setFocus(True)
-
-
-class ConfirmDialog(GenericConfirmDialog):
-
-    def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs):
-        GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs)
-
-
-class GenericDialog(DialogBox):
-    """Dialog which just show a widget and a close button"""
-
-    def __init__(self, title, main_widget, callback=None, options=None, **kwargs):
-        """Simple notice dialog box
-        @param title: HTML put in the header
-        @param main_widget: widget put in the body
-        @param callback: method to call on closing
-        @param options: one or more of the following options:
-                        - NO_CLOSE: don't add a close button"""
-        DialogBox.__init__(self, centered=True, **kwargs)
-        self.callback = callback
-        if not options:
-            options = []
-        _body = VerticalPanel()
-        _body.setSize('100%', '100%')
-        _body.setSpacing(4)
-        _body.add(main_widget)
-        _body.setCellWidth(main_widget, '100%')
-        _body.setCellHeight(main_widget, '100%')
-        if not 'NO_CLOSE' in options:
-            _close_button = Button("Close", self.onClose)
-            _body.add(_close_button)
-            _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
-        self.setHTML(title)
-        self.setWidget(_body)
-        self.panel.setSize('100%', '100%')  # Need this hack to have correct size in Gecko & Webkit
-
-    def close(self):
-        """Same effect as clicking the close button"""
-        self.onClose(None)
-
-    def onClose(self, sender):
-        self.hide()
-        if self.callback:
-            self.callback()
-
-
-class InfoDialog(GenericDialog):
-
-    def __init__(self, title, body, callback=None, options=None, **kwargs):
-        GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs)
-
-
-class PromptDialog(GenericConfirmDialog):
-
-    def __init__(self, callback, text='', title='User input', **kwargs):
-        prompt = TextBox()
-        prompt.setText(text)
-        GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs)
-
-
-class PopupPanelWrapper(PopupPanel):
-    """This wrapper catch Escape event to avoid request cancellation by Firefox"""
-
-    def onEventPreview(self, event):
-        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
-            #needed to prevent request cancellation in Firefox
-            event.preventDefault()
-        return PopupPanel.onEventPreview(self, event)
-
-
-class ExtTextBox(TextBox):
-    """Extended TextBox"""
-
-    def __init__(self, *args, **kwargs):
-        if 'enter_cb' in kwargs:
-            self.enter_cb = kwargs['enter_cb']
-            del kwargs['enter_cb']
-        TextBox.__init__(self, *args, **kwargs)
-        self.addKeyboardListener(self)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        pass
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        pass
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        if self.enter_cb and keycode == KEY_ENTER:
-            self.enter_cb(self)
-
-
-class GroupSelector(DialogBox):
-
-    def __init__(self, top_widgets, initial_groups, selected_groups,
-                 ok_title="OK", ok_cb=None, cancel_cb=None):
-        DialogBox.__init__(self, centered=True)
-        main_panel = VerticalPanel()
-        self.ok_cb = ok_cb
-        self.cancel_cb = cancel_cb
-
-        for wid in top_widgets:
-            main_panel.add(wid)
-
-        main_panel.add(Label('Select in which groups your contact is:'))
-        self.list_box = ListBox()
-        self.list_box.setMultipleSelect(True)
-        self.list_box.setVisibleItemCount(5)
-        self.setAvailableGroups(initial_groups)
-        self.setGroupsSelected(selected_groups)
-        main_panel.add(self.list_box)
-
-        def cb(text):
-            self.list_box.addItem(text)
-            self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected")
-
-        main_panel.add(AddGroupPanel(initial_groups, cb))
-
-        button_panel = HorizontalPanel()
-        button_panel.addStyleName("marginAuto")
-        button_panel.add(Button(ok_title, self.onOK))
-        button_panel.add(Button("Cancel", self.onCancel))
-        main_panel.add(button_panel)
-
-        self.setWidget(main_panel)
-
-    def getSelectedGroups(self):
-        """Return a list of selected groups"""
-        return self.list_box.getSelectedValues()
-
-    def setAvailableGroups(self, groups):
-        _groups = list(set(groups))
-        _groups.sort()
-        self.list_box.clear()
-        for group in _groups:
-            self.list_box.addItem(group)
-
-    def setGroupsSelected(self, selected_groups):
-        self.list_box.setItemTextSelection(selected_groups)
-
-    def onOK(self, sender):
-        self.hide()
-        if self.ok_cb:
-            self.ok_cb(self)
-
-    def onCancel(self, sender):
-        self.hide()
-        if self.cancel_cb:
-            self.cancel_cb(self)
-
-
-class AddGroupPanel(HorizontalPanel):
-    def __init__(self, groups, cb=None):
-        """
-        @param groups: list of the already existing groups
-        """
-        HorizontalPanel.__init__(self)
-        self.groups = groups
-        self.add(Label('Add group:'))
-        self.textbox = ExtTextBox(enter_cb=self.onGroupInput)
-        self.add(self.textbox)
-        self.add(Button("add", lambda sender: self.onGroupInput(self.textbox)))
-        self.cb = cb
-
-    def onGroupInput(self, sender):
-        text = sender.getText()
-        if text == "":
-            return
-        for group in self.groups:
-            if text == group:
-                Window.alert("The group '%s' already exists." % text)
-                return
-        for pattern in FORBIDDEN_PATTERNS_IN_GROUP:
-            if pattern in text:
-                Window.alert("The pattern '%s' is not allowed in group names." % pattern)
-                return
-        sender.setText('')
-        self.groups.append(text)
-        if self.cb is not None:
-            self.cb(text)
-
-
-class WheelTextBox(TextBox, MouseWheelHandler):
-
-    def __init__(self, *args, **kwargs):
-        TextBox.__init__(self, *args, **kwargs)
-        MouseWheelHandler.__init__(self)
-
-
-class IntSetter(HorizontalPanel):
-    """This class show a bar with button to set an int value"""
-
-    def __init__(self, label, value=0, value_max=None, visible_len=3):
-        """initialize the intSetter
-        @param label: text shown in front of the setter
-        @param value: initial value
-        @param value_max: limit value, None or 0 for unlimited"""
-        HorizontalPanel.__init__(self)
-        self.value = value
-        self.value_max = value_max
-        _label = Label(label)
-        self.add(_label)
-        self.setCellWidth(_label, "100%")
-        minus_button = Button("-", self.onMinus)
-        self.box = WheelTextBox()
-        self.box.setVisibleLength(visible_len)
-        self.box.setText(str(value))
-        self.box.addInputListener(self)
-        self.box.addMouseWheelListener(self)
-        plus_button = Button("+", self.onPlus)
-        self.add(minus_button)
-        self.add(self.box)
-        self.add(plus_button)
-        self.valueChangedListener = []
-
-    def addValueChangeListener(self, listener):
-        self.valueChangedListener.append(listener)
-
-    def removeValueChangeListener(self, listener):
-        if listener in self.valueChangedListener:
-            self.valueChangedListener.remove(listener)
-
-    def _callListeners(self):
-        for listener in self.valueChangedListener:
-            listener(self.value)
-
-    def setValue(self, value):
-        """Change the value and fire valueChange listeners"""
-        self.value = value
-        self.box.setText(str(value))
-        self._callListeners()
-
-    def onMinus(self, sender, step=1):
-        self.value = max(0, self.value - step)
-        self.box.setText(str(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._callListeners()
-
-    def onInput(self, sender):
-        """Accept only valid integer && normalize print (no leading 0)"""
-        try:
-            self.value = int(self.box.getText()) if self.box.getText() else 0
-        except ValueError:
-            pass
-        if self.value_max:
-            self.value = min(self.value, self.value_max)
-        self.box.setText(str(self.value))
-        self._callListeners()
-
-    def onMouseWheel(self, sender, velocity):
-        if velocity > 0:
-            self.onMinus(sender, 10)
-        else:
-            self.onPlus(sender, 10)
--- a/src/browser/file_tools.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas.ui.FileUpload import FileUpload
-from pyjamas.ui.FormPanel import FormPanel
-from pyjamas import Window
-from pyjamas import DOM
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Label import Label
-
-
-class FilterFileUpload(FileUpload):
-
-    def __init__(self, name, max_size, types=None):
-        """
-        @param name: the input element name and id
-        @param max_size: maximum file size in MB
-        @param types: allowed types as a list of couples (x, y, z):
-        - x: MIME content type e.g. "audio/ogg"
-        - y: file extension e.g. "*.ogg"
-        - z: description for the user e.g. "Ogg Vorbis Audio"
-        If types is None, all file format are accepted
-        """
-        FileUpload.__init__(self)
-        self.setName(name)
-        while DOM.getElementById(name):
-            name = "%s_" % name
-        self.setID(name)
-        self._id = name
-        self.max_size = max_size
-        self.types = types
-
-    def getFileInfo(self):
-        from __pyjamas__ import JS
-        JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]")
-
-    def check(self):
-        if self.getFilename() == "":
-            return False
-        (size, filetype) = self.getFileInfo()
-        if self.types and filetype not in [x for (x, y, z) in self.types]:
-            types = []
-            for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]:
-                if type_ not in types:
-                    types.append(type_)
-            Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types))
-            return False
-        if size > self.max_size * pow(2, 20):
-            Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size)
-            return False
-        return True
-
-
-class FileUploadPanel(FormPanel):
-
-    def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None):
-        """Build a form panel to upload a file.
-        @param action_url: the form action URL
-        @param input_id: the input element name and id
-        @param max_size: maximum file size in MB
-        @param texts: a dict to ovewrite the default textual values
-        @param close_cb: the close button callback method
-        """
-        FormPanel.__init__(self)
-        self.texts = {'ok_button': 'Upload file',
-                     'cancel_button': 'Cancel',
-                     'body': 'Please select a file.',
-                     'submitting': '<strong>Submitting, please wait...</strong>',
-                     'errback': "Your file has been rejected...",
-                     'body_errback': 'Please select another file.',
-                     'callback': "Your file has been accepted!"}
-        if isinstance(texts, dict):
-            self.texts.update(texts)
-        self.close_cb = close_cb
-        self.setEncoding(FormPanel.ENCODING_MULTIPART)
-        self.setMethod(FormPanel.METHOD_POST)
-        self.setAction(action_url)
-        self.vPanel = VerticalPanel()
-        self.message = HTML(self.texts['body'])
-        self.vPanel.add(self.message)
-
-        hPanel = HorizontalPanel()
-        hPanel.setSpacing(5)
-        hPanel.setStyleName('marginAuto')
-        self.file_upload = FilterFileUpload(input_id, max_size)
-        self.vPanel.add(self.file_upload)
-
-        self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick"))
-        hPanel.add(self.upload_btn)
-        hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick")))
-
-        self.status = Label()
-        hPanel.add(self.status)
-
-        self.vPanel.add(hPanel)
-
-        self.add(self.vPanel)
-        self.addFormHandler(self)
-
-    def setCloseCb(self, close_cb):
-        self.close_cb = close_cb
-
-    def onCloseBtnClick(self):
-        if self.close_cb:
-            self.close_cb()
-        else:
-            log.warning("no close method defined")
-
-    def onSubmitBtnClick(self):
-        if not self.file_upload.check():
-            return
-        self.message.setHTML(self.texts['submitting'])
-        self.upload_btn.setEnabled(False)
-        self.submit()
-
-    def onSubmit(self, event):
-        pass
-
-    def onSubmitComplete(self, event):
-        result = event.getResults()
-        if result != "OK":
-            Window.alert(self.texts['errback'])
-            self.message.setHTML(self.texts['body_errback'])
-            self.upload_btn.setEnabled(True)
-        else:
-            Window.alert(self.texts['callback'])
-            self.close_cb()
--- a/src/browser/html_tools.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +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/>.
-
-from sat_frontends.tools import xmltools
-
-from nativedom import NativeDOM
-import re
-
-dom = NativeDOM()
-
-
-def html_sanitize(html):
-    """Naive sanitization of HTML"""
-    return html.replace('<', '&lt;').replace('>', '&gt;')
-
-
-def html_strip(html):
-    """Strip leading/trailing white spaces, HTML line breaks and &nbsp; sequences."""
-    cleaned = re.sub(r"^(<br/?>|&nbsp;|\s)+", "", html)
-    cleaned = re.sub(r"(<br/?>|&nbsp;|\s)+$", "", cleaned)
-    return cleaned
-
-
-def inlineRoot(xhtml):
-    """ make root element inline """
-    doc = dom.parseString(xhtml)
-    return xmltools.inlineRoot(doc)
-
-
-def convertNewLinesToXHTML(text):
-    return text.replace('\n', '<br/>')
--- a/src/browser/jid.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +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/>.
-
-
-class JID(object):
-    """This class help manage JID (Node@Domaine/Resource)"""
-
-    def __init__(self, jid):
-        self.__raw = str(jid)
-        self.__parse()
-
-    def __parse(self):
-        """find node domaine and resource"""
-        node_end = self.__raw.find('@')
-        if node_end < 0:
-            node_end = 0
-        domain_end = self.__raw.find('/')
-        if domain_end < 1:
-            domain_end = len(self.__raw)
-        self.node = self.__raw[:node_end]
-        self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end]
-        self.resource = self.__raw[domain_end + 1:]
-        if not node_end:
-            self.bare = self.__raw
-        else:
-            self.bare = self.node + '@' + self.domain
-
-    def __str__(self):
-        return self.__raw.__str__()
-
-    def is_valid(self):
-        """return True if the jid is xmpp compliant"""
-        #FIXME: always return True for the moment
-        return True
-
-    def __eq__(self, other):
-        """Redefine equality operator to implement the naturally expected test"""
-        return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- a/src/browser/libervia_main.py	Mon Jun 09 20:37:28 2014 +0200
+++ b/src/browser/libervia_main.py	Mon Jun 09 22:15:26 2014 +0200
@@ -20,7 +20,7 @@
 import pyjd  # this is dummy in pyjs
 
 ### logging configuration ###
-import logging
+from sat_browser import logging
 logging.configure()
 from sat.core.log import getLogger
 log = getLogger(__name__)
@@ -37,18 +37,17 @@
 from pyjamas import Window, DOM
 from pyjamas.JSONService import JSONProxy
 
-from register import RegisterBox
-from contact import ContactPanel
-from base_widget import WidgetsPanel
-from panels import MicroblogItem
-import panels
-import dialog
-from jid import JID
-from xmlui import XMLUI
-from html_tools import html_sanitize
-from notification import Notification
+from sat_browser import register
+from sat_browser import contact
+from sat_browser import base_widget
+from sat_browser import panels
+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 constants import Const as C
+from sat_browser.constants import Const as C
 
 
 MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
@@ -56,7 +55,7 @@
 # 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 WidgetsPanel, or replaced to the expected
+# and added to new base_widget.WidgetsPanel, or replaced to the expected
 # position if the previous and the new parent are the same.
 REUSE_EXISTING_LIBERVIA_WIDGETS = True
 
@@ -178,7 +177,7 @@
         self.bridge_signals = BridgeSignals(self)
         self.uni_box = None
         self.status_panel = HTML('<br />')
-        self.contact_panel = ContactPanel(self)
+        self.contact_panel = contact.ContactPanel(self)
         self.panel = panels.MainPanel(self)
         self.discuss_panel = self.panel.discuss_panel
         self.tab_panel = self.panel.tab_panel
@@ -189,7 +188,7 @@
         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()
+        self.notification = notification.Notification()
         DOM.addEventPreview(self)
         self._register = RegisterCall()
         self._register.call('getMenus', self.panel.menu.createMenus)
@@ -204,15 +203,15 @@
 
     def getSelected(self):
         wid = self.tab_panel.getCurrentPanel()
-        if not isinstance(wid, WidgetsPanel):
-            log.error("Tab widget is not a WidgetsPanel, can't get selected widget")
+        if not isinstance(wid, base_widget.WidgetsPanel):
+            log.error("Tab widget is not a base_widget.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, WidgetsPanel):
+        if not isinstance(widgets_panel, base_widget.WidgetsPanel):
             return
 
         selected = widgets_panel.selected
@@ -293,7 +292,7 @@
         @param wid: LiberviaWidget to add
         @param select: True to select the added tab
         """
-        widgets_panel = WidgetsPanel(self)
+        widgets_panel = base_widget.WidgetsPanel(self)
         self.tab_panel.add(widgets_panel, label)
         widgets_panel.addWidget(wid)
         if select:
@@ -316,7 +315,7 @@
     def _isRegisteredCB(self, result):
         registered, warning = result
         if not registered:
-            self._register_box = RegisterBox(self.logged)
+            self._register_box = register.RegisterBox(self.logged)
             self._register_box.centerBox()
             self._register_box.show()
             if warning:
@@ -395,7 +394,7 @@
             # action was a one shot, nothing to do
             pass
         elif "xmlui" in data:
-            ui = XMLUI(self, xml_data=data['xmlui'])
+            ui = xmlui.XMLUI(self, xml_data=data['xmlui'])
             options = ['NO_CLOSE'] if ui.type == 'form' else []
             _dialog = dialog.GenericDialog(ui.title, ui, options=options)
             ui.setCloseCb(_dialog.close)
@@ -420,8 +419,8 @@
         self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data)
 
     def _getContactsCB(self, contacts_data):
-        for contact in contacts_data:
-            jid, attributes, groups = contact
+        for contact_ in contacts_data:
+            jid, attributes, groups = contact_
             self._newContactCb(jid, attributes, groups)
 
     def _getSignalsCB(self, signal_data):
@@ -487,9 +486,9 @@
         elif name == 'chatStateReceived':
             self._chatStateReceivedCb(*args)
 
-    def _getParamsUICB(self, xmlui):
+    def _getParamsUICB(self, xml_ui):
         """Hide the parameters item if there's nothing to display"""
-        if not xmlui:
+        if not xml_ui:
             self.panel.menu.removeItemParams()
 
     def _ownBlogsFills(self, mblogs):
@@ -503,7 +502,7 @@
                     _groups = set(mblog['groups'].split() if mblog['groups'] else [])
                 else:
                     _groups = None
-                mblog_entry = MicroblogItem(mblog)
+                mblog_entry = panels.MicroblogItem(mblog)
                 self.mblog_cache.append((_groups, mblog_entry))
 
         if len(self.mblog_cache) > MAX_MBLOG_CACHE:
@@ -517,7 +516,7 @@
         del self.init_cache
 
     def _getProfileJidCB(self, jid):
-        self.whoami = JID(jid)
+        self.whoami = jid.JID(jid)
         #we can now ask our status
         self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb)
         #the rooms where we are
@@ -541,7 +540,7 @@
         if not self.initialised:
             self.init_cache.append((sender, event_type, data))
             return
-        sender = JID(sender).bare
+        sender = jid.JID(sender).bare
         if event_type == "MICROBLOG":
             if not 'content' in data:
                 log.warning("No content found in microblog data")
@@ -550,7 +549,7 @@
                 _groups = set(data['groups'].split() if data['groups'] else [])
             else:
                 _groups = None
-            mblog_entry = MicroblogItem(data)
+            mblog_entry = panels.MicroblogItem(data)
 
             for lib_wid in self.libervia_widgets:
                 if isinstance(lib_wid, panels.MicroblogPanel):
@@ -588,7 +587,7 @@
         @param mblog_panel: MicroblogPanel instance
         @param sender: jid of the entry sender
         @param _groups: groups which can receive this entry
-        @param mblog_entry: MicroblogItem instance"""
+        @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)
@@ -669,8 +668,8 @@
         return lib_wid
 
     def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
-        _from = JID(from_jid)
-        _to = JID(to_jid)
+        _from = jid.JID(from_jid)
+        _to = jid.JID(to_jid)
         other = _to if _from.bare == self.whoami.bare else _from
         lib_wid = self.getLiberviaWidget(panels.ChatPanel, other, ignoreOtherTabs=False)
         self.displayNotification(_from, msg)
@@ -691,7 +690,7 @@
             self.contact_panel.setContactMessageWaiting(other.bare, True)
 
     def _presenceUpdateCb(self, entity, show, priority, statuses):
-        entity_jid = JID(entity)
+        entity_jid = jid.JID(entity)
         if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
             self.status_panel.setPresence(show)
             if statuses:
@@ -700,7 +699,7 @@
             self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
 
     def _roomJoinedCb(self, room_jid, room_nicks, user_nick):
-        _target = JID(room_jid)
+        _target = jid.JID(room_jid)
         if _target not in self.room_list:
             self.room_list.append(_target)
         chat_panel = panels.ChatPanel(self, _target, type_='group')
@@ -716,8 +715,8 @@
         chat_panel.refresh()
 
     def _roomLeftCb(self, room_jid, room_nicks, user_nick):
-        # FIXME: room_list contains JID instances so why MUST we do
-        # 'remove(room_jid)' and not 'remove(JID(room_jid))' ????!!
+        # FIXME: room_list contains jid.JID instances so why MUST we do
+        # 'remove(room_jid)' and not 'remove(jid.JID(room_jid))' ????!!
         # This looks like a pyjamas bug --> check/report
         try:
             self.room_list.remove(room_jid)
@@ -776,17 +775,17 @@
 
     def _subscribeCb(self, sub_type, entity):
         if sub_type == 'subscribed':
-            dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show()
+            dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_tools.html_sanitize(entity)).show()
             self.getEntityMBlog(entity)
 
         elif sub_type == 'unsubscribed':
-            dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show()
+            dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_tools.html_sanitize(entity)).show()
             #TODO: remove microblogs from panels
 
         elif sub_type == 'subscribe':
             #The user want to subscribe to our presence
             _dialog = None
-            msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity))
+            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())
@@ -824,7 +823,7 @@
             target = '@ALL@'
             nick = C.ALL_OCCUPANTS
         else:
-            from_jid = JID(from_jid_s)
+            from_jid = jid.JID(from_jid_s)
             target = from_jid.bare
             nick = from_jid.resource
 
--- a/src/browser/list_manager.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,608 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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 pyjamas.ui.Grid import Grid
-from pyjamas.ui.Button import Button
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.FlowPanel import FlowPanel
-from pyjamas.ui.AutoComplete import AutoCompleteTextBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui.KeyboardListener import KEY_ENTER
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.DropWidget import DropWidget
-from pyjamas.Timer import Timer
-from pyjamas import DOM
-
-from base_panels import PopupMenuPanel
-from base_widget import DragLabel
-
-# HTML content for the removal button (image or text)
-REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>'
-
-# Item to be considered for an empty list box selection.
-# Could be whatever which doesn't look like a JID or a group name.
-EMPTY_SELECTION_ITEM = ""
-
-
-class ListManager():
-    """A manager for sub-panels to assign elements to lists."""
-
-    def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}):
-        """
-        @param parent: FlexTable parent widget for the manager
-        @param keys_dict: dict with the contact keys mapped to data
-        @param contact_list: list of string (the contact JID userhosts)
-        @param offsets: dict to set widget positions offset within parent
-        - "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
-        """
-        self._parent = parent
-        if isinstance(keys_dict, set) or isinstance(keys_dict, list):
-            tmp = {}
-            for key in keys_dict:
-                tmp[key] = {}
-            keys_dict = tmp
-        self.__keys_dict = keys_dict
-        if isinstance(contact_list, set):
-            contact_list = list(contact_list)
-        self.__list = contact_list
-        self.__list.sort()
-        # store the list of contacts that are not assigned yet
-        self.__remaining_list = []
-        self.__remaining_list.extend(self.__list)
-        # mark a change to sort the list before it's used
-        self.__remaining_list_sorted = True
-
-        self.offsets = {"x_first": 0, "x": 0, "y": 0}
-        if "x" in offsets and not "x_first" in offsets:
-            offsets["x_first"] = offsets["x"]
-        self.offsets.update(offsets)
-
-        self.style = {
-           "keyItem": "recipientTypeItem",
-           "popupMenuItem": "recipientTypeItem",
-           "buttonCell": "recipientButtonCell",
-           "dragoverPanel": "dragover-recipientPanel",
-           "keyPanel": "recipientPanel",
-           "textBox": "recipientTextBox",
-           "textBox-invalid": "recipientTextBox-invalid",
-           "removeButton": "recipientRemoveButton",
-        }
-        self.style.update(style)
-
-    def createWidgets(self, title_format="%s"):
-        """Fill the parent grid with all the widgets (some may be hidden during the initialization)."""
-        self.__children = {}
-        for key in self.__keys_dict:
-            self.addContactKey(key, title_format=title_format)
-
-    def addContactKey(self, key, dict_={}, title_format="%s"):
-        if key not in self.__keys_dict:
-            self.__keys_dict[key] = dict_
-        # copy the key to its associated sub-map
-        self.__keys_dict[key]["title"] = key
-        self._addChild(self.__keys_dict[key], title_format)
-
-    def removeContactKey(self, key):
-        """Remove a list panel and all its associated data."""
-        contacts = self.__children[key]["panel"].getContacts()
-        (y, x) = self._parent.getIndex(self.__children[key]["button"])
-        self._parent.removeRow(y)
-        del self.__children[key]
-        del self.__keys_dict[key]
-        self.addToRemainingList(contacts)
-
-    def _addChild(self, entry, title_format):
-        """Add a button and FlowPanel for the corresponding map entry."""
-        button = Button(title_format % entry["title"])
-        button.setStyleName(self.style["keyItem"])
-        if hasattr(entry, "desc"):
-            button.setTitle(entry["desc"])
-        if not "optional" in entry:
-            entry["optional"] = False
-        button.setVisible(not entry["optional"])
-        y = len(self.__children) + self.offsets["y"]
-        x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
-
-        self._parent.insertRow(y)
-        self._parent.setWidget(y, x, button)
-        self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
-
-        _child = ListPanel(self, entry, self.style)
-        self._parent.setWidget(y, x + 1, _child)
-
-        self.__children[entry["title"]] = {}
-        self.__children[entry["title"]]["button"] = button
-        self.__children[entry["title"]]["panel"] = _child
-
-        if hasattr(self, "popup_menu"):
-            # this is done if self.registerPopupMenuPanel has been called yet
-            self.popup_menu.registerClickSender(button)
-
-    def _refresh(self, visible=True):
-        """Set visible the sub-panels that are non optional or non empty, hide the rest."""
-        for key in self.__children:
-            self.setContactPanelVisible(key, False)
-        if not visible:
-            return
-        _map = self.getContacts()
-        for key in _map:
-            if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]:
-                self.setContactPanelVisible(key, True)
-
-    def setVisible(self, visible):
-        self._refresh(visible)
-
-    def setContactPanelVisible(self, key, visible=True, sender=None):
-        """Do not remove the "sender" param as it is needed for the context menu."""
-        self.__children[key]["button"].setVisible(visible)
-        self.__children[key]["panel"].setVisible(visible)
-
-    @property
-    def list(self):
-        """Return the full list of potential contacts."""
-        return self.__list
-
-    @property
-    def keys(self):
-        return self.__keys_dict.keys()
-
-    @property
-    def keys_dict(self):
-        return self.__keys_dict
-
-    @property
-    def remaining_list(self):
-        """Return the contacts that have not been selected yet."""
-        if not self.__remaining_list_sorted:
-            self.__remaining_list_sorted = True
-            self.__remaining_list.sort()
-        return self.__remaining_list
-
-    def setRemainingListUnsorted(self):
-        """Mark a change (deletion) so the list will be sorted before it's used."""
-        self.__remaining_list_sorted = False
-
-    def removeFromRemainingList(self, contacts):
-        """Remove contacts after they have been added to a sub-panel."""
-        if not isinstance(contacts, list):
-            contacts = [contacts]
-        for contact_ in contacts:
-            if contact_ in self.__remaining_list:
-                self.__remaining_list.remove(contact_)
-
-    def addToRemainingList(self, contacts, ignore_key=None):
-        """Add contacts after they have been removed from a sub-panel."""
-        if not isinstance(contacts, list):
-            contacts = [contacts]
-        assigned_contacts = set()
-        assigned_map = self.getContacts()
-        for key_ in assigned_map.keys():
-            if ignore_key is not None and key_ == ignore_key:
-                continue
-            assigned_contacts.update(assigned_map[key_])
-        for contact_ in contacts:
-            if contact_ not in self.__list or contact_ in self.__remaining_list:
-                continue
-            if contact_ in assigned_contacts:
-                continue  # the contact is assigned somewhere else
-            self.__remaining_list.append(contact_)
-            self.setRemainingListUnsorted()
-
-    def setContacts(self, _map={}):
-        """Set the contacts for each contact key."""
-        for key in self.__keys_dict:
-            if key in _map:
-                self.__children[key]["panel"].setContacts(_map[key])
-            else:
-                self.__children[key]["panel"].setContacts([])
-        self._refresh()
-
-    def getContacts(self):
-        """Get the contacts for all the lists.
-        @return: a mapping between keys and contact lists."""
-        _map = {}
-        for key in self.__children:
-            _map[key] = self.__children[key]["panel"].getContacts()
-        return _map
-
-    @property
-    def target_drop_cell(self):
-        """@return: the panel where something has been dropped."""
-        return self._target_drop_cell
-
-    def setTargetDropCell(self, target_drop_cell):
-        """@param: target_drop_cell: the panel where something has been dropped."""
-        self._target_drop_cell = target_drop_cell
-
-    def registerPopupMenuPanel(self, entries, hide, callback):
-        "Register a popup menu panel that will be bound to all contact keys elements."
-        self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]})
-
-
-class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler):
-    """A draggable AutoCompleteTextBox which is used for representing a contact.
-    This class is NOT generic because of the onDragEnd method which call methods
-    from ListPanel. It's probably not reusable for another scenario.
-    """
-
-    def __init__(self, parent, event_cbs, style):
-        AutoCompleteTextBox.__init__(self)
-        DragLabel.__init__(self, '', 'CONTACT_TEXTBOX')  # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
-        self._parent = parent
-        self.event_cbs = event_cbs
-        self.style = style
-        self.addMouseListener(self)
-        self.addFocusListener(self)
-        self.addChangeListener(self)
-        self.addStyleName(style["textBox"])
-        self.reset()
-
-    def reset(self):
-        self.setText("")
-        self.setValid()
-
-    def setValid(self, valid=True):
-        if self.getText() == "":
-            valid = True
-        if valid:
-            self.removeStyleName(self.style["textBox-invalid"])
-        else:
-            self.addStyleName(self.style["textBox-invalid"])
-        self.valid = valid
-
-    def onDragStart(self, event):
-        self._text = self.getText()
-        DragLabel.onDragStart(self, event)
-        self._parent.setTargetDropCell(None)
-        self.setSelectionRange(len(self.getText()), 0)
-
-    def onDragEnd(self, event):
-        target = self._parent.target_drop_cell  # parent or another ListPanel
-        if self.getText() == "" or target is None:
-            return
-        self.event_cbs["drop"](self, target)
-
-    def setRemoveButton(self):
-
-        def remove_cb(sender):
-            """Callback for the button to remove this contact."""
-            self._parent.remove(self)
-            self._parent.remove(self.remove_btn)
-            self.event_cbs["remove"](self)
-
-        self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
-        self.remove_btn.setStyleName(self.style["removeButton"])
-        self._parent.add(self.remove_btn)
-
-    def removeOrReset(self):
-        if hasattr(self, "remove_btn"):
-            self.remove_btn.click()
-        else:
-            self.reset()
-
-    def onMouseMove(self, sender):
-        """Mouse enters the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            sender.remove_btn.setVisible(True)
-
-    def onMouseLeave(self, sender):
-        """Mouse leaves the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
-
-    def onFocus(self, sender):
-        sender.setSelectionRange(0, len(self.getText()))
-        self.event_cbs["focus"](sender)
-
-    def validate(self):
-        self.setSelectionRange(len(self.getText()), 0)
-        self.event_cbs["validate"](self)
-
-    def onChange(self, sender):
-        """The textbox or list selection is changed"""
-        if isinstance(sender, ListBox):
-            AutoCompleteTextBox.onChange(self, sender)
-        self.validate()
-
-    def onClick(self, sender):
-        """The list is clicked"""
-        AutoCompleteTextBox.onClick(self, sender)
-        self.validate()
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        """Listen for ENTER key stroke"""
-        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
-        if keycode == KEY_ENTER:
-            self.validate()
-
-
-class DropCell(DropWidget):
-    """A cell where you can drop widgets. This class is NOT generic because of
-    onDrop which uses methods from ListPanel. It has been created to
-    separate the drag and drop methods from the others and add a bit of
-    lisibility, but it's probably not reusable for another scenario.
-    """
-
-    def __init__(self, drop_cbs):
-        DropWidget.__init__(self)
-        self.drop_cbs = drop_cbs
-
-    def onDragEnter(self, event):
-        self.addStyleName(self.style["dragoverPanel"])
-        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, we don't want the style to be removed
-            self.removeStyleName(self.style["dragoverPanel"])
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        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
-        if item_type in self.drop_cbs.keys():
-            self.drop_cbs[item_type](self, item)
-        self.removeStyleName(self.style["dragoverPanel"])
-
-
-VALID = 1
-INVALID = 2
-DELETE = 3
-
-
-class ListPanel(FlowPanel, DropCell):
-    """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel
-    is not fully implemented yet and can not be used with pyjamas.ui.Label."""
-
-    def __init__(self, parent, entry, style={}):
-        """Initialization with a button and a DragAutoCompleteTextBox."""
-        FlowPanel.__init__(self, Visible=(False if entry["optional"] else True))
-        drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item),
-                    "CONTACT": lambda panel, item: self.addContact(item),
-                    "CONTACT_TITLE": lambda panel, item: self.addContact('@@'),
-                    "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel)
-                    }
-        DropCell.__init__(self, drop_cbs)
-        self.style = style
-        self.addStyleName(self.style["keyPanel"])
-        self._parent = parent
-        self.key = entry["title"]
-        self._addTextBox()
-
-    def _addTextBox(self, switchPrevious=False):
-        """Add a text box to the last position. If switchPrevious is True, simulate
-        an insertion before the current last textbox by copying the text and valid state.
-        @return: the created textbox or the previous one if switchPrevious is True.
-        """
-        if hasattr(self, "_last_textbox"):
-            if self._last_textbox.getText() == "":
-                return
-            self._last_textbox.setRemoveButton()
-        else:
-            switchPrevious = False
-
-        def focus_cb(sender):
-            if sender != self._last_textbox:
-                # save the current value before it's being modified
-                self._parent.addToRemainingList(sender.getText(), ignore_key=self.key)
-            sender.setCompletionItems(self._parent.remaining_list)
-
-        def remove_cb(sender):
-            """Callback for the button to remove this contact."""
-            self._parent.addToRemainingList(sender.getText())
-            self._parent.setRemainingListUnsorted()
-            self._last_textbox.setFocus(True)
-
-        def drop_cb(sender, target):
-            """Callback when the textbox is drag-n-dropped."""
-            parent = sender._parent
-            if target != parent and target.addContact(sender.getText()):
-                sender.removeOrReset()
-            else:
-                parent._parent.removeFromRemainingList(sender.getText())
-
-        events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb}
-        textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
-        self.add(textbox)
-        if switchPrevious:
-            textbox.setText(self._last_textbox.getText())
-            textbox.setValid(self._last_textbox.valid)
-            self._last_textbox.reset()
-            previous = self._last_textbox
-        self._last_textbox = textbox
-        return previous if switchPrevious else textbox
-
-    def _checkContact(self, contact, modify):
-        """
-        @param contact: the contact to check
-        @param modify: True if the contact is being modified
-        @return:
-        - VALID if the contact is valid
-        - INVALID if the contact is not valid but can be displayed
-        - DELETE if the contact should not be displayed at all
-        """
-        def countItemInList(list_, item):
-            """For some reason the built-in count function doesn't work..."""
-            count = 0
-            for elem in list_:
-                if elem == item:
-                    count += 1
-            return count
-        if contact is None or contact == "":
-            return DELETE
-        if countItemInList(self.getContacts(), contact) > (1 if modify else 0):
-            return DELETE
-        return VALID if contact in self._parent.list else INVALID
-
-    def addContact(self, contact, sender=None):
-        """The first parameter type is checked, so it is also possible to call addContact(sender).
-        If contact is not defined, sender.getText() is used. If sender is not defined, contact will
-        be written to the last textbox and a new textbox is added afterward.
-        @param contact: unicode
-        @param sender: DragAutoCompleteTextBox instance
-        """
-        if isinstance(contact, DragAutoCompleteTextBox):
-            sender = contact
-            contact = sender.getText()
-        valid = self._checkContact(contact, sender is not None)
-        if sender is None:
-            # method has been called to modify but to add a contact
-            if valid == VALID:
-                # eventually insert before the last textbox if it's not empty
-                sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
-                sender.setText(contact)
-        else:
-            sender.setValid(valid == VALID)
-        if valid != VALID:
-            if sender is not None and valid == DELETE:
-                sender.removeOrReset()
-            return False
-        if sender == self._last_textbox:
-            self._addTextBox()
-        try:
-            sender.setVisibleLength(len(contact))
-        except:
-            # IndexSizeError: Index or size is negative or greater than the allowed amount
-            log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)))
-        self._parent.removeFromRemainingList(contact)
-        self._last_textbox.setFocus(True)
-        return True
-
-    def emptyContacts(self):
-        """Empty the list of contacts."""
-        for child in self.getChildren():
-            if hasattr(child, "remove_btn"):
-                child.remove_btn.click()
-
-    def setContacts(self, tab):
-        """Set the contacts."""
-        self.emptyContacts()
-        if isinstance(tab, set):
-            tab = list(tab)
-        tab.sort()
-        for contact in tab:
-            self.addContact(contact)
-
-    def getContacts(self):
-        """Get the contacts
-        @return: an array of string"""
-        tab = []
-        for widget in self.getChildren():
-            if isinstance(widget, DragAutoCompleteTextBox):
-                # not to be mixed with EMPTY_SELECTION_ITEM
-                if widget.getText() != "":
-                    tab.append(widget.getText())
-        return tab
-
-    @property
-    def target_drop_cell(self):
-        """@return: the panel where something has been dropped."""
-        return self._parent.target_drop_cell
-
-    def setTargetDropCell(self, target_drop_cell):
-        """
-        XXX: Property setter here would not make it, you need a proper method!
-        @param target_drop_cell: the panel where something has been dropped."""
-        self._parent.setTargetDropCell(target_drop_cell)
-
-
-class ContactChooserPanel(DialogBox):
-    """Display the contacts chooser dialog. This has been implemented while
-    prototyping and is currently not used. Left for an eventual later use.
-    Replaced by the popup menu which allows to add a panel for Cc or Bcc.
-    """
-
-    def __init__(self, manager, **kwargs):
-        """Display a listbox for each contact key"""
-        DialogBox.__init__(self, autoHide=False, centered=True, **kwargs)
-        self.setHTML("Select contacts")
-        self.manager = manager
-        self.listboxes = {}
-        self.contacts = manager.getContacts()
-
-        container = VerticalPanel(Visible=True)
-        container.addStyleName("marginAuto")
-
-        grid = Grid(2, len(self.manager.keys_dict))
-        index = -1
-        for key in self.manager.keys_dict:
-            index += 1
-            grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index)
-            listbox = ListBox()
-            listbox.setMultipleSelect(True)
-            listbox.setVisibleItemCount(15)
-            listbox.addItem(EMPTY_SELECTION_ITEM)
-            for element in manager.list:
-                listbox.addItem(element)
-            self.listboxes[key] = listbox
-            grid.add(listbox, 1, index)
-        self._reset()
-
-        buttons = HorizontalPanel()
-        buttons.addStyleName("marginAuto")
-        btn_close = Button("Cancel", self.hide)
-        buttons.add(btn_close)
-        btn_reset = Button("Reset", self._reset)
-        buttons.add(btn_reset)
-        btn_ok = Button("OK", self._validate)
-        buttons.add(btn_ok)
-
-        container.add(grid)
-        container.add(buttons)
-
-        self.add(container)
-        self.center()
-
-    def _reset(self):
-        """Reset the selections."""
-        for key in self.manager.keys_dict:
-            listbox = self.listboxes[key]
-            for i in xrange(0, listbox.getItemCount()):
-                if listbox.getItemText(i) in self.contacts[key]:
-                    listbox.setItemSelected(i, "selected")
-                else:
-                    listbox.setItemSelected(i, "")
-
-    def _validate(self):
-        """Sets back the selected contacts to the good sub-panels."""
-        _map = {}
-        for key in self.manager.keys_dict:
-            selections = self.listboxes[key].getSelectedItemText()
-            if EMPTY_SELECTION_ITEM in selections:
-                selections.remove(EMPTY_SELECTION_ITEM)
-            _map[key] = selections
-        self.manager.setContacts(_map)
-        self.hide()
--- a/src/browser/logging.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +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/>.
-"""This module configure logs for Libervia browser side"""
-
-from __pyjamas__ import console
-from constants import Const as C
-from sat.core import log  # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas
-
-
-class LiberviaLogger(log.Logger):
-
-    def out(self, message, level=None):
-        if level == C.LOG_LVL_DEBUG:
-            console.debug(message)
-        elif level == C.LOG_LVL_INFO:
-            console.info(message)
-        elif level == C.LOG_LVL_WARNING:
-            console.warn(message)
-        else:
-            console.error(message)
-
-
-def configure():
-    fmt = '[%(name)s] %(message)s'
-    log.configure(C.LOG_BACKEND_CUSTOM,
-                  logger_class = LiberviaLogger,
-                  level = C.LOG_LVL_DEBUG,
-                  fmt = fmt,
-                  output = None,
-                  logger = None,
-                  colors = False,
-                  force_colors = False)
-    # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed
-    LiberviaLogger.fmt = fmt
--- a/src/browser/menu.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,271 +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 pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.MenuBar import MenuBar
-from pyjamas.ui.MenuItem import MenuItem
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Frame import Frame
-from pyjamas import Window
-
-from jid import JID
-
-from file_tools import FileUploadPanel
-from xmlui import XMLUI
-import panels
-import dialog
-from contact_group import ContactGroupEditor
-
-
-class MenuCmd:
-
-    def __init__(self, object_, handler):
-        self._object = object_
-        self._handler = handler
-
-    def execute(self):
-        handler = getattr(self._object, self._handler)
-        handler()
-
-
-class PluginMenuCmd:
-
-    def __init__(self, host, action_id):
-        self.host = host
-        self.action_id = action_id
-
-    def execute(self):
-        self.host.launchAction(self.action_id, None)
-
-
-class LiberviaMenuBar(MenuBar):
-
-    def __init__(self):
-        MenuBar.__init__(self, vertical=False)
-        self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal")
-                                                    # TODO: properly adapt CSS to the new class name
-
-    def doItemAction(self, item, fireCommand):
-        MenuBar.doItemAction(self, item, fireCommand)
-        if item == self.items[-1] and self.popup:
-            self.popup.setPopupPosition(Window.getClientWidth() -
-                                        self.popup.getOffsetWidth() - 22,
-                                        self.getAbsoluteTop() +
-                                        self.getOffsetHeight() - 1)
-            self.popup.addStyleName('menuLastPopup')
-
-
-class AvatarUpload(FileUploadPanel):
-    def __init__(self):
-        texts = {'ok_button': 'Upload avatar',
-                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
-                 'errback': "Can't open image... did you actually submit an image?",
-                 'body_errback': 'Please select another image file.',
-                 'callback': "Your new profile picture has been set!"}
-        FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
-
-
-class Menu(SimplePanel):
-
-    def __init__(self, host):
-        self.host = host
-        SimplePanel.__init__(self)
-        self.setStyleName('menuContainer')
-
-    def createMenus(self, add_menus):
-        _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s"
-        menus_dict = {}
-        menus_order = []
-
-        def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd):
-            """ add a menu to menu_dict """
-            log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd))
-            try:
-                menu_bar = menus_dict[menu_name]
-            except KeyError:
-                menu_bar = menus_dict[menu_name] = MenuBar(vertical=True)
-                menus_order.append((menu_name, menu_name_i18n, icon))
-            if item_name_i18n and menu_cmd:
-                menu_bar.addItem(item_name_i18n, menu_cmd)
-
-        addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget"))
-        addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect"))
-        addMenu("Contacts", _("Contacts"), None, 'social', None)
-        addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom"))
-        addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio"))
-        addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame"))
-        addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame"))
-
-        # additional menus
-        for action_id, type_, path, path_i18n in add_menus:
-            if not path:
-                log.warning("skipping menu without path")
-                continue
-            if len(path) != len(path_i18n):
-                log.error("inconsistency between menu paths")
-                continue
-            menu_name = path[0]
-            menu_name_i18n = path_i18n[0]
-            item_name = path[1:]
-            if not item_name:
-                log.warning("skipping menu with a path of lenght 1 [%s]" % path[0])
-                continue
-            item_name_i18n = ' | '.join(path_i18n[1:])
-            addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id))
-
-        # menu items that should be displayed after the automatically added ones
-        addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
-
-        menus_order.append(None)  # we add separator
-
-        addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract"))
-        addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout"))
-        addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount"))
-        addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters"))
-
-        # XXX: temporary, will change when a full profile will be managed in SàT
-        addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
-
-        menubar = LiberviaMenuBar()
-
-        for menu_data in menus_order:
-            if menu_data is None:
-                _separator = MenuItem('', None)
-                _separator.setStyleName('menuSeparator')
-                menubar.addItem(_separator, None)
-            else:
-                menu_name, menu_name_i18n, icon = menu_data
-                menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name]))
-
-        self.add(menubar)
-
-    #General menu
-    def onWebWidget(self):
-        web_panel = panels.WebPanel(self.host, "http://www.goffi.org")
-        self.host.addWidget(web_panel)
-        self.host.setSelected(web_panel)
-
-    def onDisconnect(self):
-        def confirm_cb(answer):
-            if answer:
-                log.info("disconnection")
-                self.host.bridge.call('disconnect', None)
-        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?")
-        _dialog.show()
-
-    def onSocialContract(self):
-        _frame = Frame('contrat_social.html')
-        _frame.setStyleName('infoFrame')
-        _dialog = dialog.GenericDialog("Contrat Social", _frame)
-        _dialog.setSize('80%', '80%')
-        _dialog.show()
-
-    def onAbout(self):
-        _about = HTML("""<b>Libervia</b>, a Salut &agrave; Toi project<br />
-<br />
-You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br />
-Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br />
-Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br />
-<br />
-Any help welcome :)
-<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p>
-""")
-        _dialog = dialog.GenericDialog("About", _about)
-        _dialog.show()
-
-    #Contact menu
-    def onManageContactGroups(self):
-        """Open the contact groups manager."""
-
-        def onCloseCallback():
-            pass
-
-        ContactGroupEditor(self.host, None, onCloseCallback)
-
-    #Group menu
-    def onJoinRoom(self):
-
-        def invite(room_jid, contacts):
-            for contact in contacts:
-                self.host.bridge.call('inviteMUC', None, contact, room_jid)
-
-        def join(room_jid, contacts):
-            if self.host.whoami:
-                nick = self.host.whoami.node
-                if room_jid not in [room.bare for room in self.host.room_list]:
-                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick)
-                else:
-                    self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare)
-                    invite(room_jid, contacts)
-
-        dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
-
-    def onCollectiveRadio(self):
-        def callback(room_jid, contacts):
-            self.host.bridge.call('launchRadioCollective', None, contacts, room_jid)
-        dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
-
-    #Game menu
-    def onTarotGame(self):
-        def onPlayersSelected(room_jid, other_players):
-            self.host.bridge.call('launchTarotGame', None, other_players, room_jid)
-        dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True))
-
-    def onXiangqiGame(self):
-        Window.alert("A Xiangqi game is planed, but not available yet")
-
-    #Settings menu
-
-    def onAccount(self):
-        def gotUI(xmlui):
-            if not xmlui:
-                return
-            body = XMLUI(self.host, xmlui)
-            _dialog = dialog.GenericDialog("Manage your account", body, options=['NO_CLOSE'])
-            body.setCloseCb(_dialog.close)
-            _dialog.show()
-        self.host.bridge.call('getAccountDialogUI', gotUI)
-
-    def onParameters(self):
-        def gotParams(xmlui):
-            if not xmlui:
-                return
-            body = XMLUI(self.host, xmlui)
-            _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE'])
-            body.setCloseCb(_dialog.close)
-            _dialog.setSize('80%', '80%')
-            _dialog.show()
-        self.host.bridge.call('getParamsUI', gotParams)
-
-    def removeItemParams(self):
-        """Remove the Parameters item from the Settings menu bar."""
-        self.menu_settings.removeItem(self.item_params)
-
-    def onAvatarUpload(self):
-        body = AvatarUpload()
-        _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.setWidth('40%')
-        _dialog.show()
--- a/src/browser/nativedom.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +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/>.
-
-"""
-This class provide basic DOM parsing based on native javascript parser
-__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989
-"""
-
-from __pyjamas__ import JS
-
-
-class Node():
-
-    def __init__(self, js_node):
-        self._node = js_node
-
-    def _jsNodesList2List(self, js_nodes_list):
-        ret = []
-        for i in range(len(js_nodes_list)):
-            #ret.append(Element(js_nodes_list.item(i)))
-            ret.append(self.__class__(js_nodes_list.item(i)))  # XXX: Ugly, but used to word around a Pyjamas's bug
-        return ret
-
-    @property
-    def nodeName(self):
-        return self._node.nodeName
-
-    @property
-    def wholeText(self):
-        return self._node.wholeText
-
-    @property
-    def childNodes(self):
-        return self._jsNodesList2List(self._node.childNodes)
-
-    def getAttribute(self, attr):
-        return self._node.getAttribute(attr)
-
-    def setAttribute(self, attr, value):
-        return self._node.setAttribute(attr, value)
-
-    def hasAttribute(self, attr):
-        return self._node.hasAttribute(attr)
-
-    def toxml(self):
-        return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""")
-
-
-class Element(Node):
-
-    def __init__(self, js_node):
-        Node.__init__(self, js_node)
-
-    def getElementsByTagName(self, tagName):
-        return self._jsNodesList2List(self._node.getElementsByTagName(tagName))
-
-
-class Document(Node):
-
-    def __init__(self, js_document):
-        Node.__init__(self, js_document)
-
-    @property
-    def documentElement(self):
-        return Element(self._node.documentElement)
-
-
-class NativeDOM:
-
-    def __init__(self):
-        JS("""
-
-        if (typeof window.DOMParser != "undefined") {
-            this.parseXml = function(xmlStr) {
-                return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml");
-            };
-        } else if (typeof window.ActiveXObject != "undefined" &&
-               new window.ActiveXObject("Microsoft.XMLDOM")) {
-            this.parseXml = function(xmlStr) {
-                var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
-                xmlDoc.async = "false";
-                xmlDoc.loadXML(xmlStr);
-                return xmlDoc;
-            };
-        } else {
-            throw new Error("No XML parser found");
-        }
-        """)
-
-    def parseString(self, xml):
-        return Document(self.parseXml(xml))
--- a/src/browser/notification.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-from __pyjamas__ import JS, wnd
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat.core.i18n import _
-
-from pyjamas import Window
-from pyjamas.Timer import Timer
-
-import dialog
-
-TIMER_DELAY = 5000
-
-
-class Notification(object):
-    """
-    If the browser supports it, the user allowed it to and the tab is in the
-    background, send desktop notifications on messages.
-
-    Requires both Web Notifications and Page Visibility API.
-    """
-
-    def __init__(self):
-        self.enabled = False
-        user_agent = None
-        notif_permission = None
-        JS("""
-        if (!('hidden' in document))
-            document.hidden = false;
-
-        user_agent = navigator.userAgent
-
-        if (!('Notification' in window))
-            return;
-
-        notif_permission = Notification.permission
-
-        if (Notification.permission === 'granted')
-            this.enabled = true;
-
-        else if (Notification.permission === 'default') {
-            Notification.requestPermission(function(permission){
-                if (permission !== 'granted')
-                    return;
-
-                self.enabled = true; //need to use self instead of this
-            });
-        }
-        """)
-
-        if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']:
-            self.user_agent = user_agent
-            self._installChromiumWorkaround()
-
-        wnd().onfocus = self.onFocus
-        # wnd().onblur = self.onBlur
-        self._notif_count = 0
-        self._orig_title = Window.getTitle()
-
-    def _installChromiumWorkaround(self):
-        # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event
-        # see https://code.google.com/p/chromium/issues/detail?id=274284
-        # FIXME: need to be removed if Chromium behaviour changes
-        try:
-            version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1]
-            version = int(version_full.split('.')[0])
-        except (IndexError, ValueError):
-            log.warning("Can't find Chromium version")
-            version = 0
-        log.info("Chromium version: %d" % (version,))
-        if version < 22:
-            log.info("Notification use the old prefixed version or are unmanaged")
-            return
-        if version < 32:
-            dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show()
-            return
-
-        log.info("==> Installing Chromium notifications request workaround <==")
-        self._old_click = wnd().onclick
-        wnd().onclick = self._chromiumWorkaround
-
-    def _chromiumWorkaround(self):
-        log.info("Activating workaround")
-        JS("""
-            Notification.requestPermission(function(permission){
-                if (permission !== 'granted')
-                    return;
-                self.enabled = true; //need to use self instead of this
-            });
-        """)
-        wnd().onclick = self._old_click
-
-    def onFocus(self):
-        Window.setTitle(self._orig_title)
-        self._notif_count = 0
-
-    # def onBlur(self):
-    #     pass
-
-    def isHidden(self):
-        JS("""return document.hidden;""")
-
-    def _notify(self, title, body, icon):
-        if not self.enabled:
-            return
-        notification = None
-        JS("""
-           notification = new Notification(title, {body: body, icon: icon});
-           // Probably won’t work, but it doesn’t hurt to try.
-           notification.addEventListener('click', function() {
-               window.focus();
-           });
-           """)
-        notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close())
-
-    def highlightTab(self):
-        self._notif_count += 1
-        Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count))
-
-    def notify(self, title, body, icon='/media/icons/apps/48/sat.png'):
-        if self.isHidden():
-            self._notify(title, body, icon)
-            self.highlightTab()
--- a/src/browser/panels.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1419 +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.strings import addURLToText
-from sat_frontends.tools.games import SYMBOLS
-from sat.core.i18n import _
-
-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.Frame import Frame
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Label import Label
-from pyjamas.ui.Button import Button
-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, KEY_UP, KEY_DOWN, KeyboardHandler
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.Timer import Timer
-from pyjamas import DOM
-from pyjamas import Window
-from __pyjamas__ import doc
-
-from datetime import datetime
-from time import time
-
-from jid import JID
-from html_tools import html_sanitize
-from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor
-from card_game import CardPanel
-from radiocol import RadioColPanel
-from menu import Menu
-import dialog
-import base_widget
-import richtext
-import contact
-from constants import Const as C
-from plugin_xep_0085 import ChatStateMachine
-
-
-# TODO: at some point we should decide which behaviors to keep and remove these two constants
-TOGGLE_EDITION_USE_ICON = False  # set to True to use an icon inside the "toggle syntax" button
-NEW_MESSAGE_USE_BUTTON = False  # set to True to display the "New message" button instead of an empty entry
-
-
-class UniBoxPanel(HorizontalPanel):
-    """Panel containing the UniBox"""
-
-    def __init__(self, host):
-        HorizontalPanel.__init__(self)
-        self.host = host
-        self.setStyleName('uniBoxPanel')
-        self.unibox = None
-
-    def refresh(self):
-        """Enable or disable this panel. Contained widgets are created when necessary."""
-        enable = self.host.getUIParam('unibox')
-        self.setVisible(enable)
-        if enable and not self.unibox:
-            self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
-            self.button.setTitle('Open the rich text editor')
-            self.button.addStyleName('uniBoxButton')
-            self.add(self.button)
-            self.unibox = UniBox(self.host)
-            self.add(self.unibox)
-            self.setCellWidth(self.unibox, '100%')
-            self.button.addClickListener(self.openRichMessageEditor)
-            self.unibox.addKey("@@: ")
-            self.unibox.onSelectedChange(self.host.getSelected())
-
-    def openRichMessageEditor(self):
-        """Open the rich text editor."""
-        self.button.setVisible(False)
-        self.unibox.setVisible(False)
-        self.setCellWidth(self.unibox, '0px')
-        self.host.panel._contactsMove(self)
-
-        def afterEditCb():
-            Window.removeWindowResizeListener(self)
-            self.host.panel._contactsMove(self.host.panel._hpanel)
-            self.setCellWidth(self.unibox, '100%')
-            self.button.setVisible(True)
-            self.unibox.setVisible(True)
-            self.host.resize()
-
-        richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
-        Window.addWindowResizeListener(self)
-        self.host.resize()
-
-    def onWindowResized(self, width, height):
-        right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
-        left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth()
-        ideal_width = right - left - 40
-        self.host.richtext.setWidth("%spx" % ideal_width)
-
-
-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)
-        self._selected_cache = None
-
-    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._selected_cache.onTextEntered(_txt)
-                self.host._updateInputHistory(_txt)
-            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."""
-        if hasattr(self._selected_cache, "target"):
-            self._selected_cache.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 UniBox(MessageBox, MouseHandler):  # AutoCompleteTextBox):
-    """This text box is used as a main typing point, for message, microblog, etc"""
-
-    def __init__(self, host):
-        MessageBox.__init__(self, host)
-        #AutoCompleteTextBox.__init__(self)
-        self.setStyleName('uniBox')
-        host.addSelectedListener(self.onSelectedChange)
-
-    def addKey(self, key):
-        return
-        #self.getCompletionItems().completions.append(key)
-
-    def removeKey(self, key):
-        return
-        # TODO: investigate why AutoCompleteTextBox doesn't work here,
-        # maybe it can work on a TextBox but no TextArea. Remove addKey
-        # and removeKey methods if they don't serve anymore.
-        try:
-            self.getCompletionItems().completions.remove(key)
-        except KeyError:
-            log.warning("trying to remove an unknown key")
-
-    def _getTarget(self, txt):
-        """ Say who will receive the messsage
-        @return: a tuple (selected, target_type, target info) with:
-            - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None))
-            - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
-            - msg: HTML message which will appear in the privacy warning banner """
-        target = self._selected_cache
-
-        def getSelectedOrStatus():
-            if target and target.isSelectable():
-                _type, msg = target.getWarningData()
-                target_hook = None  # we use the selected widget, not a hook
-            else:
-                _type, msg = "STATUS", "This will be your new status message"
-                target_hook = (txt, None)
-            return (target_hook, _type, msg)
-
-        if not txt.startswith('@'):
-            target_hook, _type, msg = getSelectedOrStatus()
-        elif txt.startswith('@@: '):
-            _type = "PUBLIC"
-            msg = MicroblogPanel.warning_msg_public
-            target_hook = (txt[4:], None)
-        elif txt.startswith('@'):
-            _end = txt.find(': ')
-            if _end == -1:
-                target_hook, _type, msg = getSelectedOrStatus()
-            else:
-                group = txt[1:_end]  # only one target group is managed for the moment
-                if not group or not group in self.host.contact_panel.getGroups():
-                    # the group doesn't exists, we ignore the key
-                    group = None
-                    target_hook, _type, msg = getSelectedOrStatus()
-                else:
-                    _type = "GROUP"
-                    msg = MicroblogPanel.warning_msg_group % group
-                    target_hook = (txt[_end + 2:], group)
-        else:
-            log.error("Unknown target")
-            target_hook, _type, msg = getSelectedOrStatus()
-
-        return (target_hook, _type, msg)
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        _txt = self.getText()
-        target_hook, type_, msg = self._getTarget(_txt)
-
-        if keycode == KEY_ENTER:
-            if _txt:
-                if target_hook:
-                    parsed_txt, data = target_hook
-                    self.host.send([(type_, data)], parsed_txt)
-                    self.host._updateInputHistory(_txt)
-                    self.setText('')
-            self.host.showWarning(None, None)
-        else:
-            self.host.showWarning(type_, msg)
-        MessageBox.onKeyPress(self, sender, keycode, modifiers)
-
-    def getTargetAndData(self):
-        """For external use, to get information about the (hypothetical) message
-        that would be sent if we press Enter right now in the unibox.
-        @return a tuple (target, data) with:
-          - data: what would be the content of the message (body)
-          - target: JID, group with the prefix "@" or the public entity "@@"
-        """
-        _txt = self.getText()
-        target_hook, _type, _msg = self._getTarget(_txt)
-        if target_hook:
-            data, target = target_hook
-            if target is None:
-                return target_hook
-            return (data, "@%s" % (target if target != "" else "@"))
-        if isinstance(self._selected_cache, MicroblogPanel):
-            groups = self._selected_cache.accepted_groups
-            target = "@%s" % (groups[0] if len(groups) > 0 else "@")
-            if len(groups) > 1:
-                Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0])
-        elif isinstance(self._selected_cache, ChatPanel):
-            target = self._selected_cache.target
-        else:
-            target = None
-        return (_txt, target)
-
-    def onWidgetClosed(self, lib_wid):
-        """Called when a libervia widget is closed"""
-        if self._selected_cache == lib_wid:
-            self.onSelectedChange(None)
-
-    """def complete(self):
-
-        #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
-        #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
-        return AutoCompleteTextBox.complete(self)"""
-
-
-class WarningPopup():
-
-    def __init__(self):
-        self._popup = None
-        self._timer = Timer(notify=self._timeCb)
-
-    def showWarning(self, type_=None, msg=None, duration=2000):
-        """Display a popup information message, e.g. to notify the recipient of a message being composed.
-        If type_ is None, a popup being currently displayed will be hidden.
-        @type_: a type determining the CSS style to be applied (see __showWarning)
-        @msg: message to be displayed
-        """
-        if type_ is None:
-            self.__removeWarning()
-            return
-        if not self._popup:
-            self.__showWarning(type_, msg)
-        elif (type_, msg) != self._popup.target_data:
-            self._timeCb(None)  # we remove the popup
-            self.__showWarning(type_, msg)
-
-        self._timer.schedule(duration)
-
-    def __showWarning(self, type_, msg):
-        """Display a popup information message, e.g. to notify the recipient of a message being composed.
-        @type_: a type determining the CSS style to be applied. For now the defined styles are
-        "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE".
-        @msg: message to be displayed
-        """
-        if type_ == "NONE":
-            return
-        if not msg:
-            log.warning("no msg set uniBox warning")
-            return
-        if type_ == "PUBLIC":
-            style = "targetPublic"
-        elif type_ == "GROUP":
-            style = "targetGroup"
-        elif type_ == "STATUS":
-            style = "targetStatus"
-        elif type_ == "ONE2ONE":
-            style = "targetOne2One"
-        else:
-            log.error("unknown message type")
-            return
-        contents = HTML(msg)
-
-        self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
-        self._popup.target_data = (type_, msg)
-        self._popup.add(contents)
-        self._popup.setStyleName("warningPopup")
-        if style:
-            self._popup.addStyleName(style)
-
-        left = 0
-        top = 0  # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
-        self._popup.setPopupPosition(left, top)
-        self._popup.show()
-
-    def _timeCb(self, timer):
-        if self._popup:
-            self._popup.hide()
-            del self._popup
-            self._popup = None
-
-    def __removeWarning(self):
-        """Remove the popup"""
-        self._timeCb(None)
-
-
-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 = 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 = HTMLPanel('')
-        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')
-        self.avatar = Image(self._blog_panel.host.getAvatar(self.author))
-        entry_avatar.add(self.avatar)
-        self.panel.add(entry_avatar)
-
-        if TOGGLE_EDITION_USE_ICON:
-            self.entry_dialog = HorizontalPanel()
-        else:
-            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.setHTML("""<div class='mb_entry_header'>
-                                   <span class='mb_entry_author'>%(author)s</span> on
-                                   <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
-                               </div>""" % {'author': html_sanitize(self.author),
-                                            'published': datetime.fromtimestamp(self.published),
-                                            'updated': update_text if self.published != self.updated else ''
-                                            }
-                            )
-
-    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 str(self.node).endswith(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:
-            try:  # prevent re-selection of the main entry after a comment has been focused
-                if self.__ignoreNextEvent:
-                    self.__ignoreNextEvent = False
-                    return
-            except AttributeError:
-                pass
-            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.__ignoreNextEvent = True
-            self._comment()
-
-    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': str(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, 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)
-            if self.type == 'main_item':  # restore the "New message" button
-                self._blog_panel.refresh()
-            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 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 = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
-        self.bubble.addStyleName("bubble")
-        try:
-            self.toggle_syntax_button.removeFromParent()
-        except 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)
-            return
-        data = {'id': str(time()),
-                'new': True,
-                'type': 'comment',
-                'author': self._blog_panel.host.whoami.bare,
-                'service': self.comments_service,
-                'node': self.comments_node
-                }
-        entry = self._blog_panel.addEntry(data)
-        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)
-
-    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 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')
-            if TOGGLE_EDITION_USE_ICON:
-                entry.entry_dialog.setWidth('80%')
-                entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax)
-                entry.toggle_syntax_button.setTitle(title)
-                entry.entry_dialog.add(entry.toggle_syntax_button)
-            else:
-                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(base_widget.LiberviaWidget):
-    warning_msg_public = "This message will be PUBLIC 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 group <span class='warningTarget'>%s</span>"
-
-    def __init__(self, host, accepted_groups):
-        """Panel used to show microblog
-        @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
-        """
-        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
-        self.setAcceptedGroup(accepted_groups)
-        self.host = host
-        self.entries = {}
-        self.comments = {}
-        self.selected_entry = None
-        self.vpanel = VerticalPanel()
-        self.vpanel.setStyleName('microblogPanel')
-        self.setWidget(self.vpanel)
-
-    def refresh(self):
-        """Refresh the display of this widget. If the unibox is disabled,
-        display the 'New message' button or an empty bubble on top of the panel"""
-        if hasattr(self, 'new_button'):
-            self.new_button.setVisible(self.host.uni_box is None)
-            return
-        if self.host.uni_box is None:
-            def addBox():
-                if hasattr(self, 'new_button'):
-                    self.new_button.setVisible(False)
-                data = {'id': str(time()),
-                        'new': True,
-                        'author': self.host.whoami.bare,
-                        }
-                entry = self.addEntry(data)
-                entry.edit(True)
-            if NEW_MESSAGE_USE_BUTTON:
-                self.new_button = Button("New message", listener=addBox)
-                self.new_button.setStyleName("microblogNewButton")
-                self.vpanel.insert(self.new_button, 0)
-            elif not self.getNewMainEntry():
-                addBox()
-
-    def getNewMainEntry(self):
-        """Get the new entry being edited, or None if it doesn't exists.
-
-        @return (MicroblogEntry): the new entry being edited.
-        """
-        try:
-            first = self.vpanel.children[0]
-        except IndexError:
-            return None
-        assert(first.type == 'main_item')
-        return first if first.empty else None
-
-    @classmethod
-    def registerClass(cls):
-        base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel)
-        base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel)
-
-    @classmethod
-    def createPanel(cls, host, item):
-        """Generic panel creation for one, several or all groups (meta).
-        @parem host: the SatWebFrontend instance
-        @param item: single group as a string, list of groups
-         (as an array) or None (for the meta group = "all groups")
-        @return: the created MicroblogPanel
-        """
-        _items = item if isinstance(item, list) else ([] if item is None else [item])
-        _type = 'ALL' if _items == [] else 'GROUP'
-        # XXX: pyjamas doesn't support use of cls directly
-        _new_panel = MicroblogPanel(host, _items)
-        host.FillMicroblogPanel(_new_panel)
-        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
-        host.setSelected(_new_panel)
-        _new_panel.refresh()
-        return _new_panel
-
-    @classmethod
-    def createMetaPanel(cls, host, item):
-        """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group"""
-        return MicroblogPanel.createPanel(host, None)
-
-    @property
-    def accepted_groups(self):
-        return self._accepted_groups
-
-    def matchEntity(self, entity):
-        """
-        @param entity: single group as a string, list of groups
-        (as an array) or None (for the meta group = "all groups")
-        @return: True if self matches the given entity
-        """
-        entity = entity if isinstance(entity, list) else ([] if entity is None else [entity])
-        entity.sort()  # sort() do not return the sorted list: do it here, not on the "return" line
-        return self.accepted_groups == entity
-
-    def getWarningData(self, comment=None):
-        """
-        @param comment: True if the composed message is a comment. If None, consider we are
-        composing from the unibox and guess the message type from self.selected_entry
-        @return: a couple (type, msg) for calling self.host.showWarning"""
-        if comment is None:  # composing from the unibox
-            if self.selected_entry and not self.selected_entry.comments:
-                log.error("an item without comment is selected")
-                return ("NONE", None)
-            comment = self.selected_entry is not None
-        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:
-            # we only accept one group at the moment
-            # FIXME: manage several groups
-            return ("GROUP", self.warning_msg_group % self._accepted_groups[0])
-
-    def onTextEntered(self, text):
-        if self.selected_entry:
-            # we are entering a comment
-            comments_url = self.selected_entry.comments
-            if not comments_url:
-                raise Exception("ERROR: the comments URL is empty")
-            target = ("COMMENT", comments_url)
-        elif not self._accepted_groups:
-            # we are entering a public microblog
-            target = ("PUBLIC", None)
-        else:
-            # we are entering a microblog restricted to a group
-            # FIXME: manage several groups
-            target = ("GROUP", self._accepted_groups[0])
-        self.host.send([target], 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: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
-        """
-        log.debug("Massive insertion of %d microblogs" % len(mblogs))
-        for publisher in mblogs:
-            log.debug("adding blogs for [%s]" % publisher)
-            for mblog in mblogs[publisher]:
-                if not "content" in mblog:
-                    log.warning("No content found in microblog [%s]", mblog)
-                    continue
-                self.addEntry(mblog)
-
-    def mblogsInsert(self, mblogs):
-        """ Insert several microblogs at once
-        @param mblogs: list of microblogs
-        """
-        for mblog in mblogs:
-            if not "content" in mblog:
-                log.warning("No content found in microblog [%s]", mblog)
-                continue
-            self.addEntry(mblog)
-
-    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"""
-        assert(isinstance(reverse, bool))
-        if entry.empty:
-            entry.published = 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:
-            if not isinstance(child, MicroblogEntry):
-                idx += 1
-                continue
-            condition_to_stop = child.empty or (child.published > entry.published)
-            if condition_to_stop != reverse:  # != is XOR
-                break
-            idx += 1
-
-        vpanel.insert(entry, idx)
-
-    def addEntry(self, data):
-        """Add an entry to the panel
-        @param data: dict containing the item data
-        @return: the added entry, or None
-        """
-        _entry = MicroblogEntry(self, data)
-        if _entry.type == "comment":
-            comments_hash = (_entry.service, _entry.node)
-            if not comments_hash 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)
-            return _entry
-
-        if _entry.id in self.entries:  # update
-            idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
-            self.vpanel.remove(self.entries[_entry.id])
-            self.vpanel.insert(_entry, idx)
-        else:  # new entry
-            self._chronoInsert(self.vpanel, _entry)
-        self.entries[_entry.id] = _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)
-
-        return _entry
-
-    def removeEntry(self, type_, id_):
-        """Remove an entry from the panel
-        @param type_: entry type ('main_item' or 'comment')
-        @param id_: entry id
-        """
-        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()
-                    self.selected_entry = None
-                    break
-            elif isinstance(child, VerticalPanel) and type_ == 'comment':
-                for comment in child.getChildren():
-                    if comment.id == id_:
-                        comment.removeFromParent()
-                        self.selected_entry = None
-                        break
-
-    def setSelectedEntry(self, 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!")
-        removeStyle = lambda entry: entry.removeStyleName('selected_entry')
-        if not self.host.uni_box or not entry.comments:
-            entry.addStyleName('selected_entry')  # blink the clicked entry
-            clicked_entry = entry  # entry may be None when the timer is done
-            Timer(500, lambda timer: removeStyle(clicked_entry))
-        if not self.host.uni_box:
-            return  # unibox is disabled
-        # from here the previous behavior (toggle main item selection) is conserved
-        entry = entry if entry.comments else None
-        if self.selected_entry == entry:
-            entry = None
-        if self.selected_entry:
-            removeStyle(self.selected_entry)
-        if entry:
-            log.debug("microblog entry selected (author=%s)" % entry.author)
-            entry.addStyleName('selected_entry')
-        self.selected_entry = entry
-
-    def updateValue(self, type_, jid, value):
-        """Update a jid value in entries
-        @param type_: one of 'avatar', 'nick'
-        @param jid: jid concerned
-        @param value: new value"""
-        def updateVPanel(vpanel):
-            for child in vpanel.children:
-                if isinstance(child, MicroblogEntry) and child.author == jid:
-                    child.updateAvatar(value)
-                elif isinstance(child, VerticalPanel):
-                    updateVPanel(child)
-        if type_ == 'avatar':
-            updateVPanel(self.vpanel)
-
-    def setAcceptedGroup(self, group):
-        """Add one or more group(s) which can be displayed in this panel.
-        Prevent from duplicate values and keep the list sorted.
-        @param group: string of the group, or list of string
-        """
-        if not hasattr(self, "_accepted_groups"):
-            self._accepted_groups = []
-        groups = group if isinstance(group, list) else [group]
-        for _group in groups:
-            if _group not in self._accepted_groups:
-                self._accepted_groups.append(_group)
-        self._accepted_groups.sort()
-
-    def isJidAccepted(self, jid):
-        """Tell if a jid is actepted and shown in this panel
-        @param jid: jid
-        @return: True if the jid is accepted"""
-        if self.accept_all():
-            return True
-        for group in self._accepted_groups:
-            if self.host.contact_panel.isContactInGroup(group, jid):
-                return True
-        return False
-
-
-class StatusPanel(HTMLTextEditor):
-
-    EMPTY_STATUS = '&lt;click to set a status&gt;'
-
-    def __init__(self, host, status=''):
-        self.host = host
-        modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
-        HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True})
-        self.edit(False)
-        self.setStyleName('statusPanel')
-
-    @property
-    def status(self):
-        return self._original_content['text']
-
-    def __cleanContent(self, content):
-        status = content['text']
-        if status == self.EMPTY_STATUS or status in C.PRESENCE.values():
-            content['text'] = ''
-        return content
-
-    def getContent(self):
-        return self.__cleanContent(HTMLTextEditor.getContent(self))
-
-    def setContent(self, content):
-        content = self.__cleanContent(content)
-        BaseTextEditor.setContent(self, content)
-
-    def setDisplayContent(self):
-        status = self._original_content['text']
-        try:
-            presence = self.host.status_panel.presence
-        except AttributeError:  # during initialization
-            presence = None
-        if not status:
-            if presence and presence in C.PRESENCE:
-                status = C.PRESENCE[presence]
-            else:
-                status = self.EMPTY_STATUS
-        self.display.setHTML(addURLToText(status))
-
-
-class PresenceStatusPanel(HorizontalPanel, ClickHandler):
-
-    def __init__(self, host, presence="", status=""):
-        self.host = host
-        HorizontalPanel.__init__(self, Width='100%')
-        self.presence_button = Label(u"◉")
-        self.presence_button.setStyleName("presence-button")
-        self.status_panel = StatusPanel(host, status=status)
-        self.setPresence(presence)
-        entries = {}
-        for value in C.PRESENCE.keys():
-            entries.update({C.PRESENCE[value]: {"value": value}})
-
-        def callback(sender, key):
-            self.setPresence(entries[key]["value"])  # order matters
-            self.host.send([("STATUS", None)], self.status_panel.status)
-
-        self.presence_list = PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"})
-        self.presence_list.registerClickSender(self.presence_button)
-
-        panel = HorizontalPanel()
-        panel.add(self.presence_button)
-        panel.add(self.status_panel)
-        panel.setCellVerticalAlignment(self.presence_button, 'baseline')
-        panel.setCellVerticalAlignment(self.status_panel, 'baseline')
-        panel.setStyleName("marginAuto")
-        self.add(panel)
-
-        self.status_panel.edit(False)
-
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    @property
-    def presence(self):
-        return self._presence
-
-    @property
-    def status(self):
-        return self.status_panel._original_content['text']
-
-    def setPresence(self, presence):
-        self._presence = presence
-        contact.setPresenceStyle(self.presence_button, self._presence)
-
-    def setStatus(self, status):
-        self.status_panel.setContent({'text': status})
-        self.status_panel.setDisplayContent()
-
-    def onClick(self, sender):
-        # As status is the default target of uniBar, we don't want to select anything if click on it
-        self.host.setSelected(None)
-
-
-class ChatPanel(base_widget.LiberviaWidget):
-
-    def __init__(self, host, target, type_='one2one'):
-        """Panel used for conversation (one 2 one or group chat)
-        @param host: SatWebFrontend instance
-        @param target: entity (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"""
-        base_widget.LiberviaWidget.__init__(self, host, title=target.bare, selectable=True)
-        self.vpanel = VerticalPanel()
-        self.vpanel.setSize('100%', '100%')
-        self.type = type_
-        self.nick = None
-        if not target:
-            log.error("Empty target !")
-            return
-        self.target = target
-        self.__body = AbsolutePanel()
-        self.__body.setStyleName('chatPanel_body')
-        chat_area = HorizontalPanel()
-        chat_area.setStyleName('chatArea')
-        if type_ == 'group':
-            self.occupants_list = OccupantsList()
-            chat_area.add(self.occupants_list)
-        self.__body.add(chat_area)
-        self.content = AbsolutePanel()
-        self.content.setStyleName('chatContent')
-        self.content_scroll = base_widget.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 = ChatStateMachine(self.host, str(self.target))
-        self._state = None
-
-    @classmethod
-    def registerClass(cls):
-        base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
-
-    @classmethod
-    def createPanel(cls, host, item):
-        _contact = item if isinstance(item, JID) else JID(item)
-        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
-        _new_panel = ChatPanel(host, _contact)  # 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"""
-        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)
-            return
-        if enable_box:
-            self.message_box = MessageBox(self.host)
-            self.message_box.onSelectedChange(self)
-            self.vpanel.add(self.message_box)
-
-    def matchEntity(self, entity):
-        """
-        @param entity: target jid as a string or JID instance.
-        Could also be a couple with a type in the second element.
-        @return: True if self matches the given entity
-        """
-        if isinstance(entity, tuple):
-            entity, type_ = entity if len(entity) > 1 else (entity[0], self.type)
-        else:
-            type_ = self.type
-        entity = entity if isinstance(entity, JID) else JID(entity)
-        try:
-            return self.target.bare == entity.bare and self.type == type_
-        except AttributeError as e:
-            e.include_traceback()
-            return False
-
-    def getWarningData(self):
-        if self.type not in ["one2one", "group"]:
-            raise Exception("Unmanaged type !")
-        if self.type == "one2one":
-            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
-        elif self.type == "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 == "one2one" else "GROUP", msg)
-
-    def onTextEntered(self, text):
-        self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text)
-        self.state_machine._onEvent("active")
-
-    def onQuit(self):
-        base_widget.LiberviaWidget.onQuit(self)
-        if self.type == 'group':
-            self.host.bridge.call('mucLeave', None, 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 users presents in this room
-        @param occupants: list of nicks (string)"""
-        self.occupants_list.clear()
-        for nick in nicks:
-            self.occupants_list.addOccupant(nick)
-
-    def userJoined(self, nick, data):
-        self.occupants_list.addOccupant(nick)
-        self.printInfo("=> %s has joined the room" % nick)
-
-    def userLeft(self, nick, data):
-        self.occupants_list.removeOccupant(nick)
-        self.printInfo("<= %s has left the room" % nick)
-
-    def changeUserNick(self, old_nick, new_nick):
-        assert(self.type == "group")
-        self.occupants_list.removeOccupant(old_nick)
-        self.occupants_list.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=20):
-        """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, to_jid, 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(from_jid, message, extra, timestamp)
-        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
-
-    def printInfo(self, msg, type_='normal', link_cb=None):
-        """Print general info
-        @param msg: message to print
-        @param type_: one of:
-            "normal": general info like "toto has joined the room"
-            "link": general info that is clickable like "click here to join the main room"
-            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
-        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
-        """
-        _wid = HTML(msg) if type_ == 'link' else Label(msg)
-        if type_ == 'normal':
-            _wid.setStyleName('chatTextInfo')
-        elif type_ == 'link':
-            _wid.setStyleName('chatTextInfo-link')
-            if link_cb:
-                _wid.addClickListener(link_cb)
-        elif type_ == 'me':
-            _wid.setStyleName('chatTextMe')
-        else:
-            _wid.setStyleName('chatTextInfo')
-        self.content.add(_wid)
-
-    def printMessage(self, from_jid, msg, extra, timestamp=None):
-        """Print message in chat window. Must be implemented by child class"""
-        _jid = JID(from_jid)
-        nick = _jid.node if self.type == 'one2one' else _jid.resource
-        mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare  # mymess = True if message comes from local user
-        if msg.startswith('/me '):
-            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me')
-            return
-        self.content.add(ChatText(timestamp, nick, mymess, msg, extra.get('xhtml')))
-        self.content_scroll.scrollToBottom()
-
-    def startGame(self, game_type, waiting, referee, players, *args):
-        """Configure the chat window to start a game"""
-        classes = {"Tarot": CardPanel, "RadioCol": RadioColPanel}
-        if game_type not in classes.keys():
-            return  # unknown game
-        attr = game_type.lower()
-        self.occupants_list.updateSpecials(players, SYMBOLS[attr])
-        if waiting or not self.nick in players:
-            return  # waiting for player or not playing
-        attr = "%s_panel" % attr
-        if hasattr(self, attr):
-            return
-        log.info("%s Game Started \o/" % game_type)
-        panel = classes[game_type](self, referee, self.nick, players, *args)
-        setattr(self, attr, panel)
-        self.vpanel.insert(panel, 0)
-        self.vpanel.setCellHeight(panel, panel.getHeight())
-
-    def getGame(self, game_type):
-        """Return class managing the game type"""
-        # TODO: check that the game is launched, and manage errors
-        if game_type == "Tarot":
-            return self.tarot_panel
-        elif game_type == "RadioCol":
-            return self.radiocol_panel
-
-    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: None for one2one, the MUC user nick or ALL_OCCUPANTS
-        """
-        if nick:
-            assert(self.type == 'group')
-            occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick]
-            for occupant in occupants:
-                self.occupants_list.occupants_list[occupant].setState(state)
-        else:
-            assert(self.type == 'one2one')
-            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 ChatPanel dialog"""
-        if self._state:
-            self.setTitle(self.target.bare + " (" + self._state + ")")
-        else:
-            self.setTitle(self.target.bare)
-
-
-class WebPanel(base_widget.LiberviaWidget):
-    """ (mini)browser like widget """
-
-    def __init__(self, host, url=None):
-        """
-        @param host: SatWebFrontend instance
-        """
-        base_widget.LiberviaWidget.__init__(self, host)
-        self._vpanel = VerticalPanel()
-        self._vpanel.setSize('100%', '100%')
-        self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
-        self._url.setText(url or "")
-        self._url.setWidth('100%')
-        hpanel = HorizontalPanel()
-        hpanel.add(self._url)
-        btn = Button("Go", self.onUrlClick)
-        hpanel.setCellWidth(self._url, "100%")
-        #self.setCellWidth(btn, "10%")
-        hpanel.add(self._url)
-        hpanel.add(btn)
-        self._vpanel.add(hpanel)
-        self._vpanel.setCellHeight(hpanel, '20px')
-        self._frame = Frame(url or "")
-        self._frame.setSize('100%', '100%')
-        DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
-        self._vpanel.add(self._frame)
-        self.setWidget(self._vpanel)
-
-    def onUrlClick(self, sender):
-        self._frame.setUrl(self._url.getText())
-
-
-class MainPanel(AbsolutePanel):
-
-    def __init__(self, host):
-        self.host = host
-        AbsolutePanel.__init__(self)
-
-        # menu
-        self.menu = Menu(host)
-
-        # unibox
-        self.unibox_panel = UniBoxPanel(host)
-        self.unibox_panel.setVisible(False)
-
-        # contacts
-        self._contacts = HorizontalPanel()
-        self._contacts.addStyleName('globalLeftArea')
-        self.contacts_switch = Button(u'«', self._contactsSwitch)
-        self.contacts_switch.addStyleName('contactsSwitch')
-        self._contacts.add(self.contacts_switch)
-        self._contacts.add(self.host.contact_panel)
-
-        # tabs
-        self.tab_panel = base_widget.MainTabPanel(host)
-        self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
-        self.tab_panel.add(self.discuss_panel, "Discussions")
-        self.tab_panel.selectTab(0)
-
-        self.header = AbsolutePanel()
-        self.header.add(self.menu)
-        self.header.add(self.unibox_panel)
-        self.header.add(self.host.status_panel)
-        self.header.setStyleName('header')
-        self.add(self.header)
-
-        self._hpanel = HorizontalPanel()
-        self._hpanel.add(self._contacts)
-        self._hpanel.add(self.tab_panel)
-        self.add(self._hpanel)
-
-        self.setWidth("100%")
-        Window.addWindowResizeListener(self)
-
-    def _contactsSwitch(self, btn=None):
-        """ (Un)hide contacts panel """
-        if btn is None:
-            btn = self.contacts_switch
-        cpanel = self.host.contact_panel
-        cpanel.setVisible(not cpanel.getVisible())
-        btn.setText(u"«" if cpanel.getVisible() else u"»")
-        self.host.resize()
-
-    def _contactsMove(self, parent):
-        """Move the contacts container (containing the contact list and
-        the "hide/show" button) to another parent, but always as the
-        first child position (insert at index 0).
-        """
-        if self._contacts.getParent():
-            if self._contacts.getParent() == parent:
-                return
-            self._contacts.removeFromParent()
-        parent.insert(self._contacts, 0)
-
-    def onWindowResized(self, width, height):
-        _elts = doc().getElementsByClassName('gwt-TabBar')
-        if not _elts.length:
-            tab_bar_h = 0
-        else:
-            tab_bar_h = _elts.item(0).offsetHeight
-        ideal_height = Window.getClientHeight() - tab_bar_h
-        self.setHeight("%s%s" % (ideal_height, "px"))
-
-    def refresh(self):
-        """Refresh the main panel"""
-        self.unibox_panel.refresh()
--- a/src/browser/plugin_xep_0085.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# SAT plugin for Chat State Notifications Protocol (xep-0085)
-# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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 pyjamas.Timer import Timer
-
-
-# Copy of the map from sat/src/plugins/plugin_xep_0085
-TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120},
-               "inactive": {"next_state": "gone", "delay": 480},
-               "gone": {"next_state": "", "delay": 0},
-               "composing": {"next_state": "paused", "delay": 30},
-               "paused": {"next_state": "inactive", "delay": 450}
-               }
-
-
-class ChatStateMachine:
-    """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085
-    which manage a timer on the web browser and keep it synchronized with the timer that runs
-    on the backend. This is only needed to avoid calling the bridge method chatStateComposing
-    too often ; accuracy is not needed so we can ignore the delay of the communication between
-    the web browser and the backend (the timer on the web browser always starts a bit before).
-    /!\ Keep this file up to date if you modify the one in the sat plugins directory.
-    """
-    def __init__(self, host, target_s):
-
-        self.host = host
-        self.target_s = target_s
-        self.started = False
-        self.state = None
-        self.timer = None
-
-    def _onEvent(self, state):
-        """Pyjamas callback takes no extra argument so we need this trick"""
-        # Here we should check the value of the parameter "Send chat state notifications"
-        # but this costs two messages. It's even better to call chatStateComposing
-        # with a doubt, it will be checked by the back-end anyway before sending
-        # the actual notifications to the other client.
-        if state == "composing" and not self.started:
-            return
-        self.started = True
-        self.next_state = state
-        self.__onEvent(None)
-
-    def __onEvent(self, timer):
-        # print "on event %s" % self.next_state
-        state = self.next_state
-        self.next_state = ""
-        if state != self.state and state == "composing":
-            self.host.bridge.call('chatStateComposing', None, self.target_s)
-        self.state = state
-        if not self.timer is None:
-            self.timer.cancel()
-
-        if not state in TRANSITIONS:
-            return
-        if not "next_state" in TRANSITIONS[state]:
-            return
-        if not "delay" in TRANSITIONS[state]:
-            return
-        next_state = TRANSITIONS[state]["next_state"]
-        delay = TRANSITIONS[state]["delay"]
-        if next_state == "" or delay < 0:
-            return
-        self.next_state = next_state
-        # pyjamas timer in milliseconds
-        self.timer = Timer(delay * 1000, self.__onEvent)
--- a/src/browser/radiocol.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,321 +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.misc import DEFAULT_MUC
-from sat.core.i18n import _
-
-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
-
-from html_tools import html_sanitize
-from file_tools import FilterFileUpload
-
-
-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 = 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 == "OK":
-            # the song can still be rejected (not readable, full queue...)
-            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
-        elif result == "KO":
-            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
-            self._parent.radiocolSongRejected(_("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_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, player_nick, players, queue_data):
-        """
-        @param parent
-        @param referee
-        @param player_nick
-        @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 radiocolPreload(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 radiocolPlay(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 radiocolNoUpload(self):
-        self.control_panel.blockUpload()
-
-    def radiocolUploadOk(self):
-        self.control_panel.unblockUpload()
-
-    def radiocolSongRejected(self, reason):
-        Window.alert("Song rejected: %s" % reason)
--- a/src/browser/register.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,255 +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>
-# Copyright (C) 2011, 2012  Adrien Vigneron <adrienvigneron@mailoo.org>
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-#This page manage subscription and new account creation
-
-import pyjd  # this is dummy in pyjs
-from sat.core.i18n import _
-
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.TabBar import TabBar
-from pyjamas.ui.PasswordTextBox import PasswordTextBox
-from pyjamas.ui.TextBox import TextBox
-from pyjamas.ui.FormPanel import FormPanel
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.PopupPanel import PopupPanel
-from pyjamas.ui.Image import Image
-from pyjamas.ui.Hidden import Hidden
-from pyjamas import Window
-from pyjamas.ui.KeyboardListener import KEY_ENTER
-from pyjamas.Timer import Timer
-
-import re
-
-from constants import Const as C
-
-
-class RegisterPanel(FormPanel):
-
-    def __init__(self, callback):
-        """
-        @param callback: method to call if login successful
-        """
-        FormPanel.__init__(self)
-        self.setSize('600px', '350px')
-        self.callback = callback
-        self.setMethod(FormPanel.METHOD_POST)
-        main_panel = HorizontalPanel()
-        main_panel.setStyleName('registerPanel_main')
-        left_side = Image("media/libervia/register_left.png")
-        main_panel.add(left_side)
-
-        ##TabPanel##
-        tab_bar = TabBar()
-        tab_bar.setStyleName('registerPanel_tabs')
-        self.right_side = TabPanel(tab_bar)
-        self.right_side.setStyleName('registerPanel_right_side')
-        main_panel.add(self.right_side)
-        main_panel.setCellWidth(self.right_side, '100%')
-
-        ##Login tab##
-        login_tab = SimplePanel()
-        login_tab.setStyleName('registerPanel_content')
-        login_vpanel = VerticalPanel()
-        login_tab.setWidget(login_vpanel)
-
-        self.login_warning_msg = Label('')
-        self.login_warning_msg.setVisible(False)
-        self.login_warning_msg.setStyleName('formWarning')
-        login_vpanel.add(self.login_warning_msg)
-
-        login_label = Label('Login:')
-        self.login_box = TextBox()
-        self.login_box.setName("login")
-        self.login_box.addKeyboardListener(self)
-        login_pass_label = Label('Password:')
-        self.login_pass_box = PasswordTextBox()
-        self.login_pass_box.setName("login_password")
-        self.login_pass_box.addKeyboardListener(self)
-
-        login_vpanel.add(login_label)
-        login_vpanel.add(self.login_box)
-        login_vpanel.add(login_pass_label)
-        login_vpanel.add(self.login_pass_box)
-        login_but = Button("Log in", getattr(self, "onLogin"))
-        login_but.setStyleName('button')
-        login_but.addStyleName('red')
-        login_vpanel.add(login_but)
-
-        #The hidden submit_type field
-        self.submit_type = Hidden('submit_type')
-        login_vpanel.add(self.submit_type)
-
-        ##Register tab##
-        register_tab = SimplePanel()
-        register_tab.setStyleName('registerPanel_content')
-        register_vpanel = VerticalPanel()
-        register_tab.setWidget(register_vpanel)
-
-        self.register_warning_msg = HTML('')
-        self.register_warning_msg.setVisible(False)
-        self.register_warning_msg.setStyleName('formWarning')
-        register_vpanel.add(self.register_warning_msg)
-
-        register_login_label = Label('Login:')
-        self.register_login_box = TextBox()
-        self.register_login_box.setName("register_login")
-        self.register_login_box.addKeyboardListener(self)
-        email_label = Label('E-mail:')
-        self.email_box = TextBox()
-        self.email_box.setName("email")
-        self.email_box.addKeyboardListener(self)
-        register_pass_label = Label('Password:')
-        self.register_pass_box = PasswordTextBox()
-        self.register_pass_box.setName("register_password")
-        self.register_pass_box.addKeyboardListener(self)
-        register_vpanel.add(register_login_label)
-        register_vpanel.add(self.register_login_box)
-        register_vpanel.add(email_label)
-        register_vpanel.add(self.email_box)
-        register_vpanel.add(register_pass_label)
-        register_vpanel.add(self.register_pass_box)
-
-        register_but = Button("Register", getattr(self, "onRegister"))
-        register_but.setStyleName('button')
-        register_but.addStyleName('red')
-        register_vpanel.add(register_but)
-
-        self.right_side.add(login_tab, 'Login')
-        self.right_side.add(register_tab, 'Register')
-        self.right_side.addTabListener(self)
-        self.right_side.selectTab(1)
-        login_tab.setWidth(None)
-        register_tab.setWidth(None)
-
-        self.add(main_panel)
-        self.addFormHandler(self)
-        self.setAction('register_api/login')
-
-    def onBeforeTabSelected(self, sender, tabIndex):
-        return True
-
-    def onTabSelected(self, sender, tabIndex):
-        if tabIndex == 0:
-            self.login_box.setFocus(True)
-        elif tabIndex == 1:
-            self.register_login_box.setFocus(True)
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        if keycode == KEY_ENTER:
-            # Browsers offer an auto-completion feature to any
-            # text box, but the selected value is not set when
-            # the widget looses the focus. Using a timer with
-            # any delay value > 0 would do the trick.
-            if sender == self.login_box:
-                Timer(5, lambda timer: self.login_pass_box.setFocus(True))
-            elif sender == self.login_pass_box:
-                self.onLogin(None)
-            elif sender == self.register_login_box:
-                Timer(5, lambda timer: self.email_box.setFocus(True))
-            elif sender == self.email_box:
-                Timer(5, lambda timer: self.register_pass_box.setFocus(True))
-            elif sender == self.register_pass_box:
-                self.onRegister(None)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        pass
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        pass
-
-    def onLogin(self, button):
-        if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE):
-            self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -')
-            self.login_warning_msg.setVisible(True)
-        else:
-            self.submit_type.setValue('login')
-            self.submit()
-
-    def onRegister(self, button):
-        if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE):
-            self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -'))
-            self.register_warning_msg.setVisible(True)
-        elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE):
-            self.register_warning_msg.setHTML(_('Invalid email address'))
-            self.register_warning_msg.setVisible(True)
-        elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH:
-            self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH)
-            self.register_warning_msg.setVisible(True)
-        else:
-            self.register_warning_msg.setVisible(False)
-            self.submit_type.setValue('register')
-            self.submit()
-
-    def onSubmit(self, event):
-        pass
-
-    def onSubmitComplete(self, event):
-        result = event.getResults()
-        if result == "PROFILE AUTH ERROR":
-            Window.alert(_('Your login and/or password is incorrect. Please try again'))
-        elif result == "XMPP AUTH ERROR":
-            # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus
-            Window.alert(_(u'Your SàT profile has been authenticated but the associated XMPP account failed to connect. Please use another SàT frontend to set another XMPP password.'))
-        elif result == "LOGGED":
-            self.callback()
-        elif result == "SESSION_ACTIVE":
-            Window.alert(_('Session already active, this should not happen, please contact the author to fix it'))
-        elif result == "ALREADY EXISTS":
-            self.register_warning_msg.setHTML(_('This login already exists,<br>please choose another one'))
-            self.register_warning_msg.setVisible(True)
-        elif result == "INTERNAL":
-            self.register_warning_msg.setHTML(_('SERVER ERROR: something went wrong during registration process, please contact the server administrator'))
-            self.register_warning_msg.setVisible(True)
-        elif result == "REGISTRATION":
-            self.login_warning_msg.setVisible(False)
-            self.register_warning_msg.setVisible(False)
-            self.login_box.setText(self.register_login_box.getText())
-            self.login_pass_box.setText('')
-            self.register_login_box.setText('')
-            self.register_pass_box.setText('')
-            self.email_box.setText('')
-            self.right_side.selectTab(0)
-            self.login_pass_box.setFocus(True)
-            Window.alert(_('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO'))
-        else:
-            Window.alert(_('Submit error: %s' % result))
-
-
-class RegisterBox(PopupPanel):
-
-    def __init__(self, callback, *args, **kwargs):
-        PopupPanel.__init__(self, *args, **kwargs)
-        self._form = RegisterPanel(callback)
-        self.setWidget(self._form)
-
-    def onWindowResized(self, width, height):
-        super(RegisterBox, self).onWindowResized(width, height)
-        self.centerBox()
-
-    def show(self):
-        super(RegisterBox, self).show()
-        self.centerBox()
-        self._form.login_box.setFocus(True)
--- a/src/browser/richtext.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,536 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.org>
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat_frontends.tools import composition
-from sat.core.i18n import _
-
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Button import Button
-from pyjamas.ui.CheckBox import CheckBox
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas import Window
-from pyjamas.ui.KeyboardListener import KeyboardHandler
-from __pyjamas__ import doc
-
-from constants import Const as C
-from dialog import ConfirmDialog, InfoDialog
-from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor
-from list_manager import ListManager
-from html_tools import html_sanitize
-import panels
-
-
-class RichTextEditor(BaseTextEditor, FlexTable):
-    """Panel for the rich text editor."""
-
-    def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None):
-        """
-        @param host: the SatWebFrontend instance
-        @param content: dict with at least a 'text' key
-        @param modifiedCb: method to be called when the text has been modified
-        @param afterEditCb: method to be called when the edition is done
-        @param options: list of UI options (see self.readOptions)
-        """
-        self.host = host
-        self._debug = False  # TODO: don't forget to set  it False before commit
-        self.wysiwyg = False
-        self.__readOptions(options)
-        self.style = {'main': 'richTextEditor',
-                      'title': 'richTextTitle',
-                      'toolbar': 'richTextToolbar',
-                      'textarea': 'richTextArea'}
-        if isinstance(style, dict):
-            self.style.update(style)
-        self._prepareUI()
-        BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
-
-    def __readOptions(self, options):
-        """Set the internal flags according to the given options."""
-        if options is None:
-            options = []
-        self.read_only = 'read_only' in options
-        self.update_msg = 'update_msg' in options
-        self.no_title = 'no_title' in options or self.read_only
-        self.no_command = 'no_command' in options or self.read_only
-
-    def _prepareUI(self, y_offset=0):
-        """Prepare the UI to host title panel, toolbar, text area...
-        @param y_offset: Y offset to start from (extra rows on top)"""
-        if not self.read_only:
-            self.title_offset = y_offset
-            self.toolbar_offset = self.title_offset + (0 if self.no_title else 1)
-            self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1)
-            self.command_offset = self.content_offset + 1
-        else:
-            self.title_offset = self.toolbar_offset = self.content_offset = y_offset
-            self.command_offset = self.content_offset + 1
-        FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2)
-        self.addStyleName(self.style['main'])
-
-    def addEditListener(self, listener):
-        """Add a method to be called whenever the text is edited.
-        @param listener: method taking two arguments: sender, keycode"""
-        BaseTextEditor.addEditListener(self, listener)
-        if hasattr(self, 'display'):
-            self.display.addEditListener(listener)
-
-    def refresh(self, edit=None):
-        """Refresh the UI for edition/display mode
-        @param edit: set to True to display the edition mode"""
-        if edit is None:
-            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
-
-        for widget in ['title_panel', 'command']:
-            if hasattr(self, widget):
-                getattr(self, widget).setVisible(edit)
-
-        if hasattr(self, 'toolbar'):
-            self.toolbar.setVisible(False)
-        if not hasattr(self, 'display'):
-            self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False})  # for display mode
-            for listener in self.edit_listeners:
-                self.display.addEditListener(listener)
-        if not self.read_only and not hasattr(self, 'textarea'):
-            self.textarea = EditTextArea(self)  # for edition mode
-            self.textarea.addStyleName(self.style['textarea'])
-
-        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
-        if edit and not self.wysiwyg:
-            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
-            self.setWidget(self.content_offset, 0, self.textarea)
-        else:
-            self.setWidget(self.content_offset, 0, self.display)
-        if not edit:
-            return
-
-        if not self.no_title and not hasattr(self, 'title_panel'):
-            self.title_panel = TitlePanel()
-            self.title_panel.addStyleName(self.style['title'])
-            self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2)
-            self.setWidget(self.title_offset, 0, self.title_panel)
-
-        if not self.no_command and not hasattr(self, 'command'):
-            self.command = HorizontalPanel()
-            self.command.addStyleName("marginAuto")
-            self.command.add(Button("Cancel", lambda: self.edit(True, True)))
-            self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False)))
-            self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2)
-            self.setWidget(self.command_offset, 0, self.command)
-
-    def setToolBar(self, syntax):
-        """This method is called asynchronously after the parameter
-        holding the rich text syntax is retrieved. It is called at
-        each call of self.edit(True) because the user may
-        have change his setting since the last time."""
-        if syntax is None or syntax not in composition.RICH_SYNTAXES.keys():
-            syntax = composition.RICH_SYNTAXES.keys()[0]
-        if hasattr(self, "toolbar") and self.toolbar.syntax == syntax:
-            self.toolbar.setVisible(True)
-            return
-        count = 0
-        for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]:
-            self.toolbar = HorizontalPanel()
-            self.toolbar.syntax = syntax
-            self.toolbar.addStyleName(self.style['toolbar'])
-            for key in composition.RICH_SYNTAXES[syntax].keys():
-                self.addToolbarButton(syntax, key)
-            self.wysiwyg_button = CheckBox(_('WYSIWYG edition'))
-            wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked())
-            self.wysiwyg_button.addClickListener(wysiywgCb)
-            self.toolbar.add(self.wysiwyg_button)
-            self.syntax_label = Label(_("Syntax: %s") % syntax)
-            self.syntax_label.addStyleName("richTextSyntaxLabel")
-            self.toolbar.add(self.syntax_label)
-            self.toolbar.setCellWidth(self.syntax_label, "100%")
-            self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2)
-            self.setWidget(self.toolbar_offset + count, 0, self.toolbar)
-            count += 1
-
-    def setWysiwyg(self, wysiwyg, init=False):
-        """Toggle the edition mode between rich content syntax and wysiwyg.
-        @param wysiwyg: boolean value
-        @param init: set to True to re-init without switching the widgets."""
-        def setWysiwyg():
-            self.wysiwyg = wysiwyg
-            try:
-                self.wysiwyg_button.setChecked(wysiwyg)
-            except TypeError:
-                pass
-            try:
-                if wysiwyg:
-                    self.syntax_label.addStyleName('transparent')
-                else:
-                    self.syntax_label.removeStyleName('transparent')
-            except TypeError:
-                pass
-            if not wysiwyg:
-                self.display.removeStyleName('richTextWysiwyg')
-
-        if init:
-            setWysiwyg()
-            return
-
-        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
-        if wysiwyg:
-            def syntaxConvertCb(text):
-                self.display.setContent({'text': text})
-                self.textarea.removeFromParent()  # XXX: force as it is not always done...
-                self.setWidget(self.content_offset, 0, self.display)
-                self.display.addStyleName('richTextWysiwyg')
-                self.display.edit(True)
-            content = self.getContent()
-            if content['text'] and content['syntax'] != C.SYNTAX_XHTML:
-                self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML)
-            else:
-                syntaxConvertCb(content['text'])
-        else:
-            syntaxConvertCb = lambda text: self.textarea.setText(text)
-            text = self.display.getContent()['text']
-            if text and self.toolbar.syntax != C.SYNTAX_XHTML:
-                self.host.bridge.call('syntaxConvert', syntaxConvertCb, text)
-            else:
-                syntaxConvertCb(text)
-            self.setWidget(self.content_offset, 0, self.textarea)
-            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
-
-        setWysiwyg()  # do it in the end because it affects self.getContent
-
-    def addToolbarButton(self, syntax, key):
-        """Add a button with the defined parameters."""
-        button = Button('<img src="%s" class="richTextIcon" />' %
-                        composition.RICH_BUTTONS[key]["icon"])
-        button.setTitle(composition.RICH_BUTTONS[key]["tip"])
-        button.addStyleName('richTextToolButton')
-        self.toolbar.add(button)
-
-        def buttonCb():
-            """Generic callback for a toolbar button."""
-            text = self.textarea.getText()
-            cursor_pos = self.textarea.getCursorPos()
-            selection_length = self.textarea.getSelectionLength()
-            data = composition.RICH_SYNTAXES[syntax][key]
-            if selection_length == 0:
-                middle_text = data[1]
-            else:
-                middle_text = text[cursor_pos:cursor_pos + selection_length]
-            self.textarea.setText(text[:cursor_pos]
-                                  + data[0]
-                                  + middle_text
-                                  + data[2]
-                                  + text[cursor_pos + selection_length:])
-            self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text))
-            self.textarea.setFocus(True)
-            self.textarea.onKeyDown()
-
-        def wysiwygCb():
-            """Callback for a toolbar button while wysiwyg mode is enabled."""
-            data = composition.COMMANDS[key]
-
-            def execCommand(command, arg):
-                self.display.setFocus(True)
-                doc().execCommand(command, False, arg.strip() if arg else '')
-            # use Window.prompt instead of dialog.PromptDialog to not loose the focus
-            prompt = lambda command, text: execCommand(command, Window.prompt(text))
-            if isinstance(data, tuple) or isinstance(data, list):
-                if data[1]:
-                    prompt(data[0], data[1])
-                else:
-                    execCommand(data[0], data[2])
-            else:
-                execCommand(data, False, '')
-            self.textarea.onKeyDown()
-
-        button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb())
-
-    def getContent(self):
-        assert(hasattr(self, 'textarea'))
-        assert(hasattr(self, 'toolbar'))
-        if self.wysiwyg:
-            content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML}
-        else:
-            content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax}
-        if hasattr(self, 'title_panel'):
-            content.update({'title': self.strproc(self.title_panel.getText())})
-        return content
-
-    def edit(self, edit=False, 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 not (edit and abort):
-            self.refresh(edit)  # not when we are asking for a confirmation
-        BaseTextEditor.edit(self, edit, abort, sync)  # after the UI has been refreshed
-        if (edit and abort):
-            return  # self.abortEdition is called by BaseTextEditor.edit
-        self.setWysiwyg(False, init=True)  # after BaseTextEditor (it affects self.getContent)
-        if sync:
-            return
-        # the following must NOT be done at each UI refresh!
-        content = self._original_content
-        if edit:
-            def getParamCb(syntax):
-                # set the editable text in the current user-selected syntax
-                def syntaxConvertCb(text=None):
-                    if text is not None:
-                        # Important: this also update self._original_content
-                        content.update({'text': text})
-                    content.update({'syntax': syntax})
-                    self.textarea.setText(content['text'])
-                    if hasattr(self, 'title_panel') and 'title' in content:
-                        self.title_panel.setText(content['title'])
-                        self.title_panel.setStackVisible(0, content['title'] != '')
-                    self.setToolBar(syntax)
-                if content['text'] and content['syntax'] != syntax:
-                    self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'])
-                else:
-                    syntaxConvertCb()
-            self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION)
-        else:
-            if not self.initialized:
-                # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification
-                self.setDisplayContent()
-            self.display.edit(False)
-
-    def setDisplayContent(self):
-        """Set the content of the HTMLTextEditor which is used for display/wysiwyg"""
-        content = self._original_content
-        text = content['text']
-        if 'title' in content and content['title']:
-            text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text'])
-        self.display.setContent({'text': text})
-
-    def setFocus(self, focus):
-        self.textarea.setFocus(focus)
-
-    def abortEdition(self, content):
-        """Ask for confirmation before closing the dialog."""
-        def confirm_cb(answer):
-            if answer:
-                self.edit(False, True)
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message"))
-        _dialog.cancel_button.setText(_("No"))
-        _dialog.show()
-
-
-class RichMessageEditor(RichTextEditor):
-    """Use the rich text editor for sending messages with extended addressing.
-    Recipient panels are on top and data may be synchronized from/to the unibox."""
-
-    @classmethod
-    def getOrCreate(cls, host, parent=None, callback=None):
-        """Get or create the message editor associated to that host.
-        Add it to parent if parent is not None, otherwise display it
-        in a popup dialog.
-        @param host: the host
-        @param parent: parent panel (or None to display in a popup).
-        @return: the RichTextEditor instance if parent is not None,
-        otherwise a popup DialogBox containing the RichTextEditor.
-        """
-        if not hasattr(host, 'richtext'):
-            modifiedCb = lambda content: True
-
-            def afterEditCb(content):
-                if hasattr(host.richtext, 'popup'):
-                    host.richtext.popup.hide()
-                else:
-                    host.richtext.setVisible(False)
-                callback()
-            options = ['no_title']
-            style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'}
-            host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style)
-
-        def add(widget, parent):
-            if widget.getParent() is not None:
-                if widget.getParent() != parent:
-                    widget.removeFromParent()
-                    parent.add(widget)
-            else:
-                parent.add(widget)
-            widget.setVisible(True)
-            widget.initialized = False  # fake a new creation
-            widget.edit(True)
-
-        if parent is None:
-            if not hasattr(host.richtext, 'popup'):
-                host.richtext.popup = DialogBox(autoHide=False, centered=True)
-                host.richtext.popup.setHTML("Compose your message")
-                host.richtext.popup.add(host.richtext)
-            add(host.richtext, host.richtext.popup)
-            host.richtext.popup.center()
-        else:
-            add(host.richtext, parent)
-        return host.richtext.popup if parent is None else host.richtext
-
-    def _prepareUI(self, y_offset=0):
-        """Prepare the UI to host recipients panel, toolbar, text area...
-        @param y_offset: Y offset to start from (extra rows on top)"""
-        self.recipient_offset = y_offset
-        self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES)
-        RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1)
-
-    def refresh(self, edit=None):
-        """Refresh the UI between edition/display mode
-        @param edit: set to True to display the edition mode"""
-        if edit is None:
-            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
-        RichTextEditor.refresh(self, edit)
-
-        for widget in ['recipient', 'recipient_spacer']:
-            if hasattr(self, widget):
-                getattr(self, widget).setVisible(edit)
-
-        if not edit:
-            return
-
-        if not hasattr(self, 'recipient'):
-            # recipient types sub-panels are automatically added by the manager
-            self.recipient = RecipientManager(self, self.recipient_offset)
-            self.recipient.createWidgets(title_format="%s: ")
-            self.recipient_spacer = HTML('')
-            self.recipient_spacer.setStyleName('recipientSpacer')
-            self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2)
-            self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer)
-
-        if not hasattr(self, 'sync_button'):
-            self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True))
-            self.command.insert(self.sync_button, 1)
-
-    def syncToEditor(self):
-        """Synchronize from unibox."""
-        def setContent(target, data):
-            if hasattr(self, 'recipient'):
-                self.recipient.setContacts({"To": [target]} if target else {})
-            self.setContent({'text': data if data else '', 'syntax': ''})
-            self.textarea.setText(data if data else '')
-        data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None)
-        setContent(target, data)
-
-    def __syncToUniBox(self, recipients=None, emptyText=False):
-        """Synchronize to unibox if a maximum of one recipient is set.
-        @return True if the sync could be done, False otherwise"""
-        if not self.host.uni_box:
-            return
-        setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text'])
-        if not hasattr(self, 'recipient'):
-            setText()
-            return True
-        if recipients is None:
-            recipients = self.recipient.getContacts()
-        target = ""
-        # we could eventually allow more in the future
-        allowed = 1
-        for key in recipients:
-            count = len(recipients[key])
-            if count == 0:
-                continue
-            allowed -= count
-            if allowed < 0:
-                return False
-            # TODO: change this if later more then one recipients are allowed
-            target = recipients[key][0]
-        setText()
-        if target == "":
-            return True
-        if target.startswith("@"):
-            _class = panels.MicroblogPanel
-            target = None if target == "@@" else target[1:]
-        else:
-            _class = panels.ChatPanel
-        self.host.getOrCreateLiberviaWidget(_class, target)
-        return True
-
-    def syncFromEditor(self, content):
-        """Synchronize to unibox and close the dialog afterward. Display
-        a message and leave the dialog open if the sync was not possible."""
-        if self.__syncToUniBox():
-            self._afterEditCb(content)
-            return
-        InfoDialog("Too many recipients",
-                   "A message with more than one direct recipient (To)," +
-                   " or with any special recipient (Cc or Bcc), could not be" +
-                   " stored in the quick box.\n\nPlease finish your composing" +
-                   " in the rich text editor, and send your message directly" +
-                   " from here.", Width="400px").center()
-
-    def edit(self, edit=True, abort=False, sync=False):
-        if not edit and not abort and not sync:  # force sending message even when the text has not been modified
-            if not self.__sendMessage():  # message has not been sent (missing information), do nothing
-                return
-        RichTextEditor.edit(self, edit, abort, sync)
-
-    def __sendMessage(self):
-        """Send the message."""
-        recipients = self.recipient.getContacts()
-        targets = []
-        for addr in recipients:
-            for recipient in recipients[addr]:
-                if recipient.startswith("@"):
-                    targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr))
-                else:
-                    targets.append(("chat", recipient, addr))
-        # check that we actually have a message target and data
-        content = self.getContent()
-        if content['text'] == "" or len(targets) == 0:
-            InfoDialog("Missing information",
-                       "Some information are missing and the message hasn't been sent.", Width="400px").center()
-            return None
-        self.__syncToUniBox(recipients, emptyText=True)
-        extra = {'content_rich': content['text']}
-        if hasattr(self, 'title_panel'):
-            extra.update({'title': content['title']})
-        self.host.send(targets, content['text'], extra=extra)
-        return True
-
-
-class RecipientManager(ListManager):
-    """A manager for sub-panels to set the recipients for each recipient type."""
-
-    def __init__(self, parent, y_offset=0):
-        # TODO: be sure we also display empty groups and disconnected contacts + their groups
-        # store the full list of potential recipients (groups and contacts)
-        list_ = []
-        list_.append("@@")
-        list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups())
-        list_.extend(contact for contact in parent.host.contact_panel.getContacts())
-        ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset})
-
-        self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES,
-                                    hide=lambda sender, key: self.__children[key]["panel"].isVisible(),
-                                    callback=self.setContactPanelVisible)
-
-
-class EditTextArea(TextArea, KeyboardHandler):
-    def __init__(self, _parent):
-        TextArea.__init__(self)
-        self._parent = _parent
-        KeyboardHandler.__init__(self)
-        self.addKeyboardListener(self)
-
-    def onKeyDown(self, sender=None, keycode=None, modifiers=None):
-        for listener in self._parent.edit_listeners:
-            listener(self, keycode)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/base_panels.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,609 @@
+#!/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.sanitize(nick),
+                            "msg_class": ' '.join(_msg_class),
+                            "msg": strings.addURLToText(html_tools.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)
+        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("<div class='occupant'>%s%s%s</div>" % (html_tools.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):
+        _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 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.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:
+            self.textarea.setCursorPos(len(self.textarea.getText()))
+        self.textarea.setFocus(focus)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/base_widget.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,732 @@
+#!/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.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.Button import Button
+from pyjamas.ui.Image import Image
+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
+
+
+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
+
+
+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
+
+    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 WidgetHeader(AbsolutePanel, LiberviaDragWidget):
+
+    def __init__(self, parent, title):
+        AbsolutePanel.__init__(self)
+        self.add(title)
+        button_group_wrapper = SimplePanel()
+        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
+        button_group = HorizontalPanel()
+        button_group.setStyleName('widgetHeader_buttonGroup')
+        setting_button = Image("media/icons/misc/settings.png")
+        setting_button.setStyleName('widgetHeader_settingButton')
+        setting_button.addClickListener(parent.onSetting)
+        close_button = Image("media/icons/misc/close.png")
+        close_button.setStyleName('widgetHeader_closeButton')
+        close_button.addClickListener(parent.onClose)
+        button_group.add(setting_button)
+        button_group.add(close_button)
+        button_group_wrapper.setWidget(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='', selectable=False):
+        """Init the widget
+        @param host: SatWebFrontend object
+        @param title: title show in the header of the widget
+        @param selectable: 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')
+        self._close_listeners = []
+        header = WidgetHeader(self, self.__title)
+        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.debug("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 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, entity):
+        """This method should be overwritten by child classes."""
+        raise NotImplementedError
+
+
+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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/card_game.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,387 @@
+#!/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:", file_)
+                card = CardWidget(self, file_)
+                log.debug("card:", 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.XMLUI(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.XMLUI(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/constants.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from libervia.common.constants import Const as C
+
+
+# Auxiliary functions
+param_to_bool = lambda value: value == 'true'
+
+
+class Const(C):
+    """Add here the constants that are only used by the browser side."""
+
+    # Parameters that have an incidence on UI display/refresh:
+    #     - they can be any parameter (not necessarily specific to Libervia)
+    #     - 'cast_from_str' is a method used to eventually convert to a non string type
+    #     - 'initial_value' is used to initialize the display before any profile connection
+    UI_PARAMS = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM,
+                            "category": C.COMPOSITION_KEY,
+                            "cast_from_str": param_to_bool,
+                            "initial_value": False
+                             },
+                 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,419 @@
+#!/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.ClickListener import ClickHandler
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas import Window
+from pyjamas import DOM
+from __pyjamas__ import doc
+
+from jid import JID
+
+import base_panels
+import base_widget
+import panels
+import html_tools
+
+
+def setPresenceStyle(element, presence, base_style="contact"):
+    """
+    Set the CSS style of a contact's element according to its presence.
+    @param item: the UI element of the contact
+    @param presence: a value in ("", "chat", "away", "dnd", "xa").
+    @param base_style: the base name of the style to apply
+    """
+    if not hasattr(element, 'presence_style'):
+        element.presence_style = None
+    style = '%s-%s' % (base_style, presence or 'connected')
+    if style == element.presence_style:
+        return
+    if element.presence_style is not None:
+        element.removeStyleName(element.presence_style)
+    element.addStyleName(style)
+    element.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, self.group)
+
+
+class ContactLabel(base_widget.DragLabel, HTML, ClickHandler):
+    def __init__(self, host, jid, name=None, handleClick=True):
+        HTML.__init__(self)
+        self.host = host
+        self.name = name or jid
+        self.waiting = False
+        self.jid = jid
+        self._fill()
+        self.setStyleName('contact')
+        base_widget.DragLabel.__init__(self, jid, "CONTACT")
+        if handleClick:
+            ClickHandler.__init__(self)
+            self.addClickListener(self)
+
+    def _fill(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._fill()
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(panels.ChatPanel, 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)
+
+
+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, handleClick=False):
+        VerticalPanel.__init__(self)
+        self.host = host
+        self.contacts = []
+        self.handleClick = handleClick
+
+    def add(self, jid, name=None, item_cb=None):
+        if jid in self.contacts:
+            return
+        index = 0
+        for contact_ in self.contacts:
+            if contact_ > jid:
+                break
+            index += 1
+        self.contacts.insert(index, jid)
+        _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick)
+        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
+        VerticalPanel.insert(self, _item, index)
+        if item_cb is not None:
+            item_cb(_item)
+
+    def remove(self, jid):
+        wid = self.getContactLabel(jid)
+        if not wid:
+            return
+        VerticalPanel.remove(self, wid)
+        self.contacts.remove(jid)
+
+    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 getContactLabel(self, contact_jid):
+        """get contactList widget of a contact
+        @return: ContactLabel item if present, else None"""
+        for wid in self:
+            if isinstance(wid, ContactLabel) and wid.jid == contact_jid:
+                return wid
+        return None
+
+
+class ContactList(GenericContactList):
+    """The contact list that is displayed on the left side."""
+
+    def __init__(self, host):
+        GenericContactList.__init__(self, host, handleClick=True)
+        self.menu_entries = {"blog": {"title": "Public blog..."}}
+        self.context_menu = base_panels.PopupMenuPanel(entries=self.menu_entries,
+                                           hide=self.contextMenuHide,
+                                           callback=self.contextMenuCallback,
+                                           vertical=False, style={"selected": "menu-selected"})
+
+    def contextMenuHide(self, sender, key):
+        """Return True if the item for that sender should be hidden."""
+        # TODO: enable the blogs of users that are on another server
+        return JID(sender.jid).domain != self.host._defaultDomain
+
+    def contextMenuCallback(self, sender, key):
+        if key == "blog":
+            # TODO: use the bare when all blogs can be retrieved
+            node = JID(sender.jid).node
+            web_panel = panels.WebPanel(self.host, "/blog/%s" % node)
+            self.host.addTab("%s's blog" % node, web_panel)
+        else:
+            sender.onClick(sender)
+
+    def add(self, jid_s, name=None):
+        """Add a contact
+
+        @param jid_s (str): JID as unicode
+        @param name (str): nickname
+        """
+        def item_cb(item):
+            self.context_menu.registerRightClickSender(item)
+        GenericContactList.add(self, jid_s, name, item_cb)
+
+    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"""
+        _item = self.getContactLabel(jid)
+        if _item:
+            if type_ == 'availability':
+                setPresenceStyle(_item, state)
+            elif type_ == 'messageWaiting':
+                _item.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, 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('contactBox')
+        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 not group 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)
+
+    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, resource, availability, priority, statuses):
+        """Set connection status
+        @param jid: JID userhost as unicode
+        """
+        if availability == 'unavailable':
+            if jid in self.connected:
+                if resource in self.connected[jid]:
+                    del self.connected[jid][resource]
+                if not self.connected[jid]:
+                    del self.connected[jid]
+        else:
+            if not jid in self.connected:
+                self.connected[jid] = {}
+            self.connected[jid][resource] = (availability, priority, statuses)
+
+        # check if the contact is connected with another resource, use the one with highest priority
+        if jid in self.connected:
+            max_resource = max_priority = None
+            for tmp_resource in self.connected[jid]:
+                if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority:
+                    max_resource = tmp_resource
+                    max_priority = self.connected[jid][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][max_resource][0]
+        if jid 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, "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()
+
+    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_label in self._contact_list:
+            if contact_jid == _contact_label.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.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.removeStyleName("selected")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_group.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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 pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.DockPanel import DockPanel
+from pyjamas.Timer import Timer
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui import HasAlignment
+
+import dialog
+import list_manager
+import contact
+
+
+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)
+        self.registerPopupMenuPanel(entries={"Remove group": {}},
+                                    callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
+
+    def removeContactKey(self, sender, key):
+        key = sender.getText()
+
+        def confirm_cb(answer):
+            if answer:
+                list_manager.ListManager.removeContactKey(self, key)
+                self._parent.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)
+
+    def addToRemainingList(self, contacts, ignore_key=None):
+        list_manager.ListManager.addToRemainingList(self, contacts, ignore_key)
+        self._parent.updateContactList(contacts=contacts)
+
+
+class ContactGroupEditor(DockPanel):
+    """Panel for the contact groups manager."""
+
+    def __init__(self, host, parent=None, onCloseCallback=None):
+        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
+        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.add_group_panel = self.getAddGroupPanel(groups_list)
+        south_panel = self.getCloseSaveButtons()
+        center_panel = self.getContactGroupManager(groups_list)
+        east_panel = self.getContactList()
+
+        self.add(self.add_group_panel, DockPanel.CENTER)
+        self.add(east_panel, DockPanel.EAST)
+        self.add(center_panel, DockPanel.NORTH)
+        self.add(south_panel, DockPanel.SOUTH)
+
+        self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT)
+        self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP)
+        self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT)
+        self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP)
+        self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM)
+        self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT)
+        self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM)
+        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.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():
+            self.restore_contact_panel = True
+            self.host.panel._contactsSwitch()
+
+        parent.add(self)
+        parent.setVisible(True)
+        if isinstance(parent, DialogBox):
+            parent.center()
+
+    def getContactGroupManager(self, groups_list):
+        """Set the list manager for the groups"""
+        flex_table = FlexTable(len(groups_list), 2)
+        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
+        # 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 add_group_cb(text):
+            self.groups.addContactKey(text)
+            self.add_group_panel.textbox.setFocus(True)
+
+        add_group_panel = dialog.AddGroupPanel(groups_list, 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"""
+        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"""
+        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
+
+    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)
+
+    def updateContactList(self, sender=None, contacts=None):
+        """Update the contact list regarding the toggle button"""
+        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_)
+        else:
+            contacts = self.all_contacts
+        for contact_ in contacts:
+            if sender.showAll:
+                self.contacts.getContactLabel(contact_).setVisible(True)
+            else:
+                if contact_ in self.groups.remaining_list:
+                    self.contacts.getContactLabel(contact_).setVisible(True)
+                else:
+                    self.contacts.getContactLabel(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 self._on_close_callback is not None:
+            self._on_close_callback()
+        if self.restore_contact_panel:
+            self.host.panel._contactsSwitch()
+
+    def cancelWithoutSaving(self):
+        """Ask for confirmation before closing the dialog."""
+        def confirm_cb(answer):
+            if answer:
+                self.__close()
+
+        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?")
+        _dialog.show()
+
+    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))
+        self.__close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/dialog.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,547 @@
+#!/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.misc import DEFAULT_MUC
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.Grid import Grid
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.Button import Button
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.RadioButton import RadioButton
+from pyjamas.ui import HasAlignment
+from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER
+from pyjamas.ui.MouseListener import MouseWheelHandler
+from pyjamas import Window
+
+import base_panels
+
+
+# List here the patterns that are not allowed in contact group names
+FORBIDDEN_PATTERNS_IN_GROUP = ()
+
+
+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):
+        Grid.__init__(self, 2, 2, Width='100%')
+        self.host = host
+
+        self.new_radio = RadioButton("room", "Discussion room:")
+        self.new_radio.setChecked(True)
+        self.box = TextBox(Width='95%')
+        self.box.setText(self.GENERATE_MUC if default_room == "" else default_room)
+        self.exist_radio = RadioButton("room", "Already joined:")
+        self.rooms_list = ListBox(Width='95%')
+
+        self.add(self.new_radio, 0, 0)
+        self.add(self.box, 0, 1)
+        self.add(self.exist_radio, 1, 0)
+        self.add(self.rooms_list, 1, 1)
+
+        self.box.addFocusListener(self)
+        self.rooms_list.addFocusListener(self)
+
+        self.exist_radio.setVisible(False)
+        self.rooms_list.setVisible(False)
+        self.setRooms()
+
+    def onFocus(self, sender):
+        if sender == self.rooms_list:
+            self.exist_radio.setChecked(True)
+        elif sender == self.box:
+            if self.box.getText() == self.GENERATE_MUC:
+                self.box.setText("")
+            self.new_radio.setChecked(True)
+
+    def onLostFocus(self, sender):
+        if sender == self.box:
+            if self.box.getText() == "":
+                self.box.setText(self.GENERATE_MUC)
+
+    def setRooms(self):
+        for room in self.host.room_list:
+            self.rooms_list.addItem(room.bare)
+        if len(self.host.room_list) > 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"""
+
+    def __init__(self, host, nb_contact=None, ok_button=None):
+        """
+        @param host: SatWebFrontend instance
+        @param nb_contact: number of contacts that have to be selected, None for no limit
+        If a tuple is given instead of an integer, nb_contact[0] is the minimal and
+        nb_contact[1] is the maximal number of contacts to be chosen.
+        """
+        self.host = host
+        if isinstance(nb_contact, tuple):
+            if len(nb_contact) == 0:
+                nb_contact = None
+            elif len(nb_contact) == 1:
+                nb_contact = (nb_contact[0], nb_contact[0])
+        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")
+        else:
+            log.warning("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%')
+        self.contacts_list = ListBox()
+        self.contacts_list.setMultipleSelect(True)
+        self.contacts_list.setWidth("95%")
+        self.contacts_list.addStyleName('contactsChooser')
+        self.contacts_list.addChangeListener(self.onChange)
+        self.add(self.contacts_list)
+        self.setContacts()
+        self.onChange()
+
+    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]:
+                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
+        """
+        self.contacts_list.clear()
+        contacts = self.host.contact_panel.getConnected(filter_muc=True)
+        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)
+
+
+class RoomAndContactsChooser(DialogBox):
+    """Select a room and some users to invite in"""
+
+    def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups",
+                 title_room="Join room", title_invite="Invite contacts", visible=(True, True)):
+        DialogBox.__init__(self, centered=True)
+        self.host = host
+        self.callback = callback
+        self.title_room = title_room
+        self.title_invite = title_invite
+
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        ok_button = Button("OK", self.onOK)
+        button_panel.add(ok_button)
+        button_panel.add(Button("Cancel", self.onCancel))
+
+        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.add(self.room_panel, visible=visible[0])
+        self.stack_panel.add(self.contact_panel, visible=visible[1])
+        self.stack_panel.addStackChangeListener(self)
+        self.onStackChanged(self.stack_panel, 0, visible[0])
+        self.onStackChanged(self.stack_panel, 1, visible[1])
+
+        main_panel = VerticalPanel()
+        main_panel.setStyleName("room-contact-chooser")
+        main_panel.add(self.stack_panel)
+        main_panel.add(button_panel)
+
+        self.setWidget(main_panel)
+        self.setHTML(title)
+        self.show()
+
+        # needed to update the contacts list when someone logged in/out
+        self.host.room_contacts_chooser = self
+
+    def getRoom(self, asSuffix=False):
+        room = self.room_panel.getRoom()
+        if asSuffix:
+            return room if room == "" else ": %s" % room
+        else:
+            return room
+
+    def getContacts(self, asSuffix=False):
+        contacts = self.contact_panel.getContacts()
+        if asSuffix:
+            return "" if contacts == [] else ": %s" % ", ".join(contacts)
+        else:
+            return 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)))
+        elif index == 1:
+            sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True)))
+
+    def resetContacts(self):
+        """Called when someone log in/out to update the list"""
+        self.contact_panel.setContacts(self.getContacts())
+
+    def onOK(self, sender):
+        room_jid = self.getRoom()
+        if room_jid != "" and "@" not in room_jid:
+            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())
+
+    def onCancel(self, sender):
+        self.hide()
+
+    def hide(self):
+        self.host.room_contacts_chooser = None
+        DialogBox.hide(self, autoClosed=True)
+
+
+class GenericConfirmDialog(DialogBox):
+
+    def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs):
+        """
+        Dialog to confirm an action
+        @param widgets: widgets to attach
+        @param callback: method to call when a button is clicked
+        @param title: title of the dialog
+        @param prompt: textbox from which to retrieve the string value to be passed to the callback when
+        OK button is pressed. If None, OK button will return "True". Cancel button always returns "False".
+        """
+        self.callback = callback
+        DialogBox.__init__(self, centered=True, **kwargs)
+
+        content = VerticalPanel()
+        content.setWidth('100%')
+        for wid in widgets:
+            content.add(wid)
+            if wid == prompt:
+                wid.setWidth('100%')
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        self.confirm_button = Button("OK", self.onConfirm)
+        button_panel.add(self.confirm_button)
+        self.cancel_button = Button("Cancel", self.onCancel)
+        button_panel.add(self.cancel_button)
+        content.add(button_panel)
+        self.setHTML(title)
+        self.setWidget(content)
+        self.prompt = prompt
+
+    def onConfirm(self, sender):
+        self.hide()
+        self.callback(self.prompt.getText() if self.prompt else True)
+
+    def onCancel(self, sender):
+        self.hide()
+        self.callback(False)
+
+    def show(self):
+        DialogBox.show(self)
+        if self.prompt:
+            self.prompt.setFocus(True)
+
+
+class ConfirmDialog(GenericConfirmDialog):
+
+    def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs):
+        GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs)
+
+
+class GenericDialog(DialogBox):
+    """Dialog which just show a widget and a close button"""
+
+    def __init__(self, title, main_widget, callback=None, options=None, **kwargs):
+        """Simple notice dialog box
+        @param title: HTML put in the header
+        @param main_widget: widget put in the body
+        @param callback: method to call on closing
+        @param options: one or more of the following options:
+                        - NO_CLOSE: don't add a close button"""
+        DialogBox.__init__(self, centered=True, **kwargs)
+        self.callback = callback
+        if not options:
+            options = []
+        _body = VerticalPanel()
+        _body.setSize('100%', '100%')
+        _body.setSpacing(4)
+        _body.add(main_widget)
+        _body.setCellWidth(main_widget, '100%')
+        _body.setCellHeight(main_widget, '100%')
+        if not 'NO_CLOSE' in options:
+            _close_button = Button("Close", self.onClose)
+            _body.add(_close_button)
+            _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
+        self.setHTML(title)
+        self.setWidget(_body)
+        self.panel.setSize('100%', '100%')  # Need this hack to have correct size in Gecko & Webkit
+
+    def close(self):
+        """Same effect as clicking the close button"""
+        self.onClose(None)
+
+    def onClose(self, sender):
+        self.hide()
+        if self.callback:
+            self.callback()
+
+
+class InfoDialog(GenericDialog):
+
+    def __init__(self, title, body, callback=None, options=None, **kwargs):
+        GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs)
+
+
+class PromptDialog(GenericConfirmDialog):
+
+    def __init__(self, callback, text='', title='User input', **kwargs):
+        prompt = TextBox()
+        prompt.setText(text)
+        GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs)
+
+
+class PopupPanelWrapper(PopupPanel):
+    """This wrapper catch Escape event to avoid request cancellation by Firefox"""
+
+    def onEventPreview(self, event):
+        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
+            #needed to prevent request cancellation in Firefox
+            event.preventDefault()
+        return PopupPanel.onEventPreview(self, event)
+
+
+class ExtTextBox(TextBox):
+    """Extended TextBox"""
+
+    def __init__(self, *args, **kwargs):
+        if 'enter_cb' in kwargs:
+            self.enter_cb = kwargs['enter_cb']
+            del kwargs['enter_cb']
+        TextBox.__init__(self, *args, **kwargs)
+        self.addKeyboardListener(self)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        if self.enter_cb and keycode == KEY_ENTER:
+            self.enter_cb(self)
+
+
+class GroupSelector(DialogBox):
+
+    def __init__(self, top_widgets, initial_groups, selected_groups,
+                 ok_title="OK", ok_cb=None, cancel_cb=None):
+        DialogBox.__init__(self, centered=True)
+        main_panel = VerticalPanel()
+        self.ok_cb = ok_cb
+        self.cancel_cb = cancel_cb
+
+        for wid in top_widgets:
+            main_panel.add(wid)
+
+        main_panel.add(Label('Select in which groups your contact is:'))
+        self.list_box = ListBox()
+        self.list_box.setMultipleSelect(True)
+        self.list_box.setVisibleItemCount(5)
+        self.setAvailableGroups(initial_groups)
+        self.setGroupsSelected(selected_groups)
+        main_panel.add(self.list_box)
+
+        def cb(text):
+            self.list_box.addItem(text)
+            self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected")
+
+        main_panel.add(AddGroupPanel(initial_groups, cb))
+
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        button_panel.add(Button(ok_title, self.onOK))
+        button_panel.add(Button("Cancel", self.onCancel))
+        main_panel.add(button_panel)
+
+        self.setWidget(main_panel)
+
+    def getSelectedGroups(self):
+        """Return a list of selected groups"""
+        return self.list_box.getSelectedValues()
+
+    def setAvailableGroups(self, groups):
+        _groups = list(set(groups))
+        _groups.sort()
+        self.list_box.clear()
+        for group in _groups:
+            self.list_box.addItem(group)
+
+    def setGroupsSelected(self, selected_groups):
+        self.list_box.setItemTextSelection(selected_groups)
+
+    def onOK(self, sender):
+        self.hide()
+        if self.ok_cb:
+            self.ok_cb(self)
+
+    def onCancel(self, sender):
+        self.hide()
+        if self.cancel_cb:
+            self.cancel_cb(self)
+
+
+class AddGroupPanel(HorizontalPanel):
+    def __init__(self, groups, cb=None):
+        """
+        @param groups: list of the already existing groups
+        """
+        HorizontalPanel.__init__(self)
+        self.groups = groups
+        self.add(Label('Add group:'))
+        self.textbox = ExtTextBox(enter_cb=self.onGroupInput)
+        self.add(self.textbox)
+        self.add(Button("add", lambda sender: self.onGroupInput(self.textbox)))
+        self.cb = cb
+
+    def onGroupInput(self, sender):
+        text = sender.getText()
+        if text == "":
+            return
+        for group in self.groups:
+            if text == group:
+                Window.alert("The group '%s' already exists." % text)
+                return
+        for pattern in FORBIDDEN_PATTERNS_IN_GROUP:
+            if pattern in text:
+                Window.alert("The pattern '%s' is not allowed in group names." % pattern)
+                return
+        sender.setText('')
+        self.groups.append(text)
+        if self.cb is not None:
+            self.cb(text)
+
+
+class WheelTextBox(TextBox, MouseWheelHandler):
+
+    def __init__(self, *args, **kwargs):
+        TextBox.__init__(self, *args, **kwargs)
+        MouseWheelHandler.__init__(self)
+
+
+class IntSetter(HorizontalPanel):
+    """This class show a bar with button to set an int value"""
+
+    def __init__(self, label, value=0, value_max=None, visible_len=3):
+        """initialize the intSetter
+        @param label: text shown in front of the setter
+        @param value: initial value
+        @param value_max: limit value, None or 0 for unlimited"""
+        HorizontalPanel.__init__(self)
+        self.value = value
+        self.value_max = value_max
+        _label = Label(label)
+        self.add(_label)
+        self.setCellWidth(_label, "100%")
+        minus_button = Button("-", self.onMinus)
+        self.box = WheelTextBox()
+        self.box.setVisibleLength(visible_len)
+        self.box.setText(str(value))
+        self.box.addInputListener(self)
+        self.box.addMouseWheelListener(self)
+        plus_button = Button("+", self.onPlus)
+        self.add(minus_button)
+        self.add(self.box)
+        self.add(plus_button)
+        self.valueChangedListener = []
+
+    def addValueChangeListener(self, listener):
+        self.valueChangedListener.append(listener)
+
+    def removeValueChangeListener(self, listener):
+        if listener in self.valueChangedListener:
+            self.valueChangedListener.remove(listener)
+
+    def _callListeners(self):
+        for listener in self.valueChangedListener:
+            listener(self.value)
+
+    def setValue(self, value):
+        """Change the value and fire valueChange listeners"""
+        self.value = value
+        self.box.setText(str(value))
+        self._callListeners()
+
+    def onMinus(self, sender, step=1):
+        self.value = max(0, self.value - step)
+        self.box.setText(str(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._callListeners()
+
+    def onInput(self, sender):
+        """Accept only valid integer && normalize print (no leading 0)"""
+        try:
+            self.value = int(self.box.getText()) if self.box.getText() else 0
+        except ValueError:
+            pass
+        if self.value_max:
+            self.value = min(self.value, self.value_max)
+        self.box.setText(str(self.value))
+        self._callListeners()
+
+    def onMouseWheel(self, sender, velocity):
+        if velocity > 0:
+            self.onMinus(sender, 10)
+        else:
+            self.onPlus(sender, 10)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/file_tools.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,148 @@
+#!/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 pyjamas.ui.FileUpload import FileUpload
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas import Window
+from pyjamas import DOM
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Label import Label
+
+
+class FilterFileUpload(FileUpload):
+
+    def __init__(self, name, max_size, types=None):
+        """
+        @param name: the input element name and id
+        @param max_size: maximum file size in MB
+        @param types: allowed types as a list of couples (x, y, z):
+        - x: MIME content type e.g. "audio/ogg"
+        - y: file extension e.g. "*.ogg"
+        - z: description for the user e.g. "Ogg Vorbis Audio"
+        If types is None, all file format are accepted
+        """
+        FileUpload.__init__(self)
+        self.setName(name)
+        while DOM.getElementById(name):
+            name = "%s_" % name
+        self.setID(name)
+        self._id = name
+        self.max_size = max_size
+        self.types = types
+
+    def getFileInfo(self):
+        from __pyjamas__ import JS
+        JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]")
+
+    def check(self):
+        if self.getFilename() == "":
+            return False
+        (size, filetype) = self.getFileInfo()
+        if self.types and filetype not in [x for (x, y, z) in self.types]:
+            types = []
+            for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]:
+                if type_ not in types:
+                    types.append(type_)
+            Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types))
+            return False
+        if size > self.max_size * pow(2, 20):
+            Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size)
+            return False
+        return True
+
+
+class FileUploadPanel(FormPanel):
+
+    def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None):
+        """Build a form panel to upload a file.
+        @param action_url: the form action URL
+        @param input_id: the input element name and id
+        @param max_size: maximum file size in MB
+        @param texts: a dict to ovewrite the default textual values
+        @param close_cb: the close button callback method
+        """
+        FormPanel.__init__(self)
+        self.texts = {'ok_button': 'Upload file',
+                     'cancel_button': 'Cancel',
+                     'body': 'Please select a file.',
+                     'submitting': '<strong>Submitting, please wait...</strong>',
+                     'errback': "Your file has been rejected...",
+                     'body_errback': 'Please select another file.',
+                     'callback': "Your file has been accepted!"}
+        if isinstance(texts, dict):
+            self.texts.update(texts)
+        self.close_cb = close_cb
+        self.setEncoding(FormPanel.ENCODING_MULTIPART)
+        self.setMethod(FormPanel.METHOD_POST)
+        self.setAction(action_url)
+        self.vPanel = VerticalPanel()
+        self.message = HTML(self.texts['body'])
+        self.vPanel.add(self.message)
+
+        hPanel = HorizontalPanel()
+        hPanel.setSpacing(5)
+        hPanel.setStyleName('marginAuto')
+        self.file_upload = FilterFileUpload(input_id, max_size)
+        self.vPanel.add(self.file_upload)
+
+        self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick"))
+        hPanel.add(self.upload_btn)
+        hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick")))
+
+        self.status = Label()
+        hPanel.add(self.status)
+
+        self.vPanel.add(hPanel)
+
+        self.add(self.vPanel)
+        self.addFormHandler(self)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def onCloseBtnClick(self):
+        if self.close_cb:
+            self.close_cb()
+        else:
+            log.warning("no close method defined")
+
+    def onSubmitBtnClick(self):
+        if not self.file_upload.check():
+            return
+        self.message.setHTML(self.texts['submitting'])
+        self.upload_btn.setEnabled(False)
+        self.submit()
+
+    def onSubmit(self, event):
+        pass
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result != "OK":
+            Window.alert(self.texts['errback'])
+            self.message.setHTML(self.texts['body_errback'])
+            self.upload_btn.setEnabled(True)
+        else:
+            Window.alert(self.texts['callback'])
+            self.close_cb()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/html_tools.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,47 @@
+#!/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_frontends.tools import xmltools
+
+import nativedom
+import re
+
+dom = nativedom.NativeDOM()
+
+
+def html_sanitize(html):
+    """Naive sanitization of HTML"""
+    return html.replace('<', '&lt;').replace('>', '&gt;')
+
+
+def html_strip(html):
+    """Strip leading/trailing white spaces, HTML line breaks and &nbsp; sequences."""
+    cleaned = re.sub(r"^(<br/?>|&nbsp;|\s)+", "", html)
+    cleaned = re.sub(r"(<br/?>|&nbsp;|\s)+$", "", cleaned)
+    return cleaned
+
+
+def inlineRoot(xhtml):
+    """ make root element inline """
+    doc = dom.parseString(xhtml)
+    return xmltools.inlineRoot(doc)
+
+
+def convertNewLinesToXHTML(text):
+    return text.replace('\n', '<br/>')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/jid.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,54 @@
+#!/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/>.
+
+
+class JID(object):
+    """This class help manage JID (Node@Domaine/Resource)"""
+
+    def __init__(self, jid):
+        self.__raw = str(jid)
+        self.__parse()
+
+    def __parse(self):
+        """find node domaine and resource"""
+        node_end = self.__raw.find('@')
+        if node_end < 0:
+            node_end = 0
+        domain_end = self.__raw.find('/')
+        if domain_end < 1:
+            domain_end = len(self.__raw)
+        self.node = self.__raw[:node_end]
+        self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end]
+        self.resource = self.__raw[domain_end + 1:]
+        if not node_end:
+            self.bare = self.__raw
+        else:
+            self.bare = self.node + '@' + self.domain
+
+    def __str__(self):
+        return self.__raw.__str__()
+
+    def is_valid(self):
+        """return True if the jid is xmpp compliant"""
+        #FIXME: always return True for the moment
+        return True
+
+    def __eq__(self, other):
+        """Redefine equality operator to implement the naturally expected test"""
+        return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/list_manager.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,608 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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 pyjamas.ui.Grid import Grid
+from pyjamas.ui.Button import Button
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.FlowPanel import FlowPanel
+from pyjamas.ui.AutoComplete import AutoCompleteTextBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.KeyboardListener import KEY_ENTER
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+
+import base_panels
+import base_widget
+
+# HTML content for the removal button (image or text)
+REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>'
+
+# Item to be considered for an empty list box selection.
+# Could be whatever which doesn't look like a JID or a group name.
+EMPTY_SELECTION_ITEM = ""
+
+
+class ListManager():
+    """A manager for sub-panels to assign elements to lists."""
+
+    def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}):
+        """
+        @param parent: FlexTable parent widget for the manager
+        @param keys_dict: dict with the contact keys mapped to data
+        @param contact_list: list of string (the contact JID userhosts)
+        @param offsets: dict to set widget positions offset within parent
+        - "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
+        """
+        self._parent = parent
+        if isinstance(keys_dict, set) or isinstance(keys_dict, list):
+            tmp = {}
+            for key in keys_dict:
+                tmp[key] = {}
+            keys_dict = tmp
+        self.__keys_dict = keys_dict
+        if isinstance(contact_list, set):
+            contact_list = list(contact_list)
+        self.__list = contact_list
+        self.__list.sort()
+        # store the list of contacts that are not assigned yet
+        self.__remaining_list = []
+        self.__remaining_list.extend(self.__list)
+        # mark a change to sort the list before it's used
+        self.__remaining_list_sorted = True
+
+        self.offsets = {"x_first": 0, "x": 0, "y": 0}
+        if "x" in offsets and not "x_first" in offsets:
+            offsets["x_first"] = offsets["x"]
+        self.offsets.update(offsets)
+
+        self.style = {
+           "keyItem": "recipientTypeItem",
+           "popupMenuItem": "recipientTypeItem",
+           "buttonCell": "recipientButtonCell",
+           "dragoverPanel": "dragover-recipientPanel",
+           "keyPanel": "recipientPanel",
+           "textBox": "recipientTextBox",
+           "textBox-invalid": "recipientTextBox-invalid",
+           "removeButton": "recipientRemoveButton",
+        }
+        self.style.update(style)
+
+    def createWidgets(self, title_format="%s"):
+        """Fill the parent grid with all the widgets (some may be hidden during the initialization)."""
+        self.__children = {}
+        for key in self.__keys_dict:
+            self.addContactKey(key, title_format=title_format)
+
+    def addContactKey(self, key, dict_={}, title_format="%s"):
+        if key not in self.__keys_dict:
+            self.__keys_dict[key] = dict_
+        # copy the key to its associated sub-map
+        self.__keys_dict[key]["title"] = key
+        self._addChild(self.__keys_dict[key], title_format)
+
+    def removeContactKey(self, key):
+        """Remove a list panel and all its associated data."""
+        contacts = self.__children[key]["panel"].getContacts()
+        (y, x) = self._parent.getIndex(self.__children[key]["button"])
+        self._parent.removeRow(y)
+        del self.__children[key]
+        del self.__keys_dict[key]
+        self.addToRemainingList(contacts)
+
+    def _addChild(self, entry, title_format):
+        """Add a button and FlowPanel for the corresponding map entry."""
+        button = Button(title_format % entry["title"])
+        button.setStyleName(self.style["keyItem"])
+        if hasattr(entry, "desc"):
+            button.setTitle(entry["desc"])
+        if not "optional" in entry:
+            entry["optional"] = False
+        button.setVisible(not entry["optional"])
+        y = len(self.__children) + self.offsets["y"]
+        x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
+
+        self._parent.insertRow(y)
+        self._parent.setWidget(y, x, button)
+        self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
+
+        _child = ListPanel(self, entry, self.style)
+        self._parent.setWidget(y, x + 1, _child)
+
+        self.__children[entry["title"]] = {}
+        self.__children[entry["title"]]["button"] = button
+        self.__children[entry["title"]]["panel"] = _child
+
+        if hasattr(self, "popup_menu"):
+            # this is done if self.registerPopupMenuPanel has been called yet
+            self.popup_menu.registerClickSender(button)
+
+    def _refresh(self, visible=True):
+        """Set visible the sub-panels that are non optional or non empty, hide the rest."""
+        for key in self.__children:
+            self.setContactPanelVisible(key, False)
+        if not visible:
+            return
+        _map = self.getContacts()
+        for key in _map:
+            if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]:
+                self.setContactPanelVisible(key, True)
+
+    def setVisible(self, visible):
+        self._refresh(visible)
+
+    def setContactPanelVisible(self, key, visible=True, sender=None):
+        """Do not remove the "sender" param as it is needed for the context menu."""
+        self.__children[key]["button"].setVisible(visible)
+        self.__children[key]["panel"].setVisible(visible)
+
+    @property
+    def list(self):
+        """Return the full list of potential contacts."""
+        return self.__list
+
+    @property
+    def keys(self):
+        return self.__keys_dict.keys()
+
+    @property
+    def keys_dict(self):
+        return self.__keys_dict
+
+    @property
+    def remaining_list(self):
+        """Return the contacts that have not been selected yet."""
+        if not self.__remaining_list_sorted:
+            self.__remaining_list_sorted = True
+            self.__remaining_list.sort()
+        return self.__remaining_list
+
+    def setRemainingListUnsorted(self):
+        """Mark a change (deletion) so the list will be sorted before it's used."""
+        self.__remaining_list_sorted = False
+
+    def removeFromRemainingList(self, contacts):
+        """Remove contacts after they have been added to a sub-panel."""
+        if not isinstance(contacts, list):
+            contacts = [contacts]
+        for contact_ in contacts:
+            if contact_ in self.__remaining_list:
+                self.__remaining_list.remove(contact_)
+
+    def addToRemainingList(self, contacts, ignore_key=None):
+        """Add contacts after they have been removed from a sub-panel."""
+        if not isinstance(contacts, list):
+            contacts = [contacts]
+        assigned_contacts = set()
+        assigned_map = self.getContacts()
+        for key_ in assigned_map.keys():
+            if ignore_key is not None and key_ == ignore_key:
+                continue
+            assigned_contacts.update(assigned_map[key_])
+        for contact_ in contacts:
+            if contact_ not in self.__list or contact_ in self.__remaining_list:
+                continue
+            if contact_ in assigned_contacts:
+                continue  # the contact is assigned somewhere else
+            self.__remaining_list.append(contact_)
+            self.setRemainingListUnsorted()
+
+    def setContacts(self, _map={}):
+        """Set the contacts for each contact key."""
+        for key in self.__keys_dict:
+            if key in _map:
+                self.__children[key]["panel"].setContacts(_map[key])
+            else:
+                self.__children[key]["panel"].setContacts([])
+        self._refresh()
+
+    def getContacts(self):
+        """Get the contacts for all the lists.
+        @return: a mapping between keys and contact lists."""
+        _map = {}
+        for key in self.__children:
+            _map[key] = self.__children[key]["panel"].getContacts()
+        return _map
+
+    @property
+    def target_drop_cell(self):
+        """@return: the panel where something has been dropped."""
+        return self._target_drop_cell
+
+    def setTargetDropCell(self, target_drop_cell):
+        """@param: target_drop_cell: the panel where something has been dropped."""
+        self._target_drop_cell = target_drop_cell
+
+    def registerPopupMenuPanel(self, entries, hide, callback):
+        "Register a popup menu panel that will be bound to all contact keys elements."
+        self.popup_menu = base_panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]})
+
+
+class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.DragLabel, MouseHandler, FocusHandler):
+    """A draggable AutoCompleteTextBox which is used for representing a contact.
+    This class is NOT generic because of the onDragEnd method which call methods
+    from ListPanel. It's probably not reusable for another scenario.
+    """
+
+    def __init__(self, parent, event_cbs, style):
+        AutoCompleteTextBox.__init__(self)
+        base_widget.DragLabel.__init__(self, '', 'CONTACT_TEXTBOX')  # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
+        self._parent = parent
+        self.event_cbs = event_cbs
+        self.style = style
+        self.addMouseListener(self)
+        self.addFocusListener(self)
+        self.addChangeListener(self)
+        self.addStyleName(style["textBox"])
+        self.reset()
+
+    def reset(self):
+        self.setText("")
+        self.setValid()
+
+    def setValid(self, valid=True):
+        if self.getText() == "":
+            valid = True
+        if valid:
+            self.removeStyleName(self.style["textBox-invalid"])
+        else:
+            self.addStyleName(self.style["textBox-invalid"])
+        self.valid = valid
+
+    def onDragStart(self, event):
+        self._text = self.getText()
+        base_widget.DragLabel.onDragStart(self, event)
+        self._parent.setTargetDropCell(None)
+        self.setSelectionRange(len(self.getText()), 0)
+
+    def onDragEnd(self, event):
+        target = self._parent.target_drop_cell  # parent or another ListPanel
+        if self.getText() == "" or target is None:
+            return
+        self.event_cbs["drop"](self, target)
+
+    def setRemoveButton(self):
+
+        def remove_cb(sender):
+            """Callback for the button to remove this contact."""
+            self._parent.remove(self)
+            self._parent.remove(self.remove_btn)
+            self.event_cbs["remove"](self)
+
+        self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
+        self.remove_btn.setStyleName(self.style["removeButton"])
+        self._parent.add(self.remove_btn)
+
+    def removeOrReset(self):
+        if hasattr(self, "remove_btn"):
+            self.remove_btn.click()
+        else:
+            self.reset()
+
+    def onMouseMove(self, sender):
+        """Mouse enters the area of a DragAutoCompleteTextBox."""
+        if hasattr(sender, "remove_btn"):
+            sender.remove_btn.setVisible(True)
+
+    def onMouseLeave(self, sender):
+        """Mouse leaves the area of a DragAutoCompleteTextBox."""
+        if hasattr(sender, "remove_btn"):
+            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
+
+    def onFocus(self, sender):
+        sender.setSelectionRange(0, len(self.getText()))
+        self.event_cbs["focus"](sender)
+
+    def validate(self):
+        self.setSelectionRange(len(self.getText()), 0)
+        self.event_cbs["validate"](self)
+
+    def onChange(self, sender):
+        """The textbox or list selection is changed"""
+        if isinstance(sender, ListBox):
+            AutoCompleteTextBox.onChange(self, sender)
+        self.validate()
+
+    def onClick(self, sender):
+        """The list is clicked"""
+        AutoCompleteTextBox.onClick(self, sender)
+        self.validate()
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        """Listen for ENTER key stroke"""
+        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
+        if keycode == KEY_ENTER:
+            self.validate()
+
+
+class DropCell(DropWidget):
+    """A cell where you can drop widgets. This class is NOT generic because of
+    onDrop which uses methods from ListPanel. It has been created to
+    separate the drag and drop methods from the others and add a bit of
+    lisibility, but it's probably not reusable for another scenario.
+    """
+
+    def __init__(self, drop_cbs):
+        DropWidget.__init__(self)
+        self.drop_cbs = drop_cbs
+
+    def onDragEnter(self, event):
+        self.addStyleName(self.style["dragoverPanel"])
+        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, we don't want the style to be removed
+            self.removeStyleName(self.style["dragoverPanel"])
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def onDrop(self, event):
+        DOM.eventPreventDefault(event)
+        dt = event.dataTransfer
+        # 'text', 'text/plain', and 'Text' are equivalent.
+        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
+        if item_type in self.drop_cbs.keys():
+            self.drop_cbs[item_type](self, item)
+        self.removeStyleName(self.style["dragoverPanel"])
+
+
+VALID = 1
+INVALID = 2
+DELETE = 3
+
+
+class ListPanel(FlowPanel, DropCell):
+    """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel
+    is not fully implemented yet and can not be used with pyjamas.ui.Label."""
+
+    def __init__(self, parent, entry, style={}):
+        """Initialization with a button and a DragAutoCompleteTextBox."""
+        FlowPanel.__init__(self, Visible=(False if entry["optional"] else True))
+        drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item),
+                    "CONTACT": lambda panel, item: self.addContact(item),
+                    "CONTACT_TITLE": lambda panel, item: self.addContact('@@'),
+                    "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel)
+                    }
+        DropCell.__init__(self, drop_cbs)
+        self.style = style
+        self.addStyleName(self.style["keyPanel"])
+        self._parent = parent
+        self.key = entry["title"]
+        self._addTextBox()
+
+    def _addTextBox(self, switchPrevious=False):
+        """Add a text box to the last position. If switchPrevious is True, simulate
+        an insertion before the current last textbox by copying the text and valid state.
+        @return: the created textbox or the previous one if switchPrevious is True.
+        """
+        if hasattr(self, "_last_textbox"):
+            if self._last_textbox.getText() == "":
+                return
+            self._last_textbox.setRemoveButton()
+        else:
+            switchPrevious = False
+
+        def focus_cb(sender):
+            if sender != self._last_textbox:
+                # save the current value before it's being modified
+                self._parent.addToRemainingList(sender.getText(), ignore_key=self.key)
+            sender.setCompletionItems(self._parent.remaining_list)
+
+        def remove_cb(sender):
+            """Callback for the button to remove this contact."""
+            self._parent.addToRemainingList(sender.getText())
+            self._parent.setRemainingListUnsorted()
+            self._last_textbox.setFocus(True)
+
+        def drop_cb(sender, target):
+            """Callback when the textbox is drag-n-dropped."""
+            parent = sender._parent
+            if target != parent and target.addContact(sender.getText()):
+                sender.removeOrReset()
+            else:
+                parent._parent.removeFromRemainingList(sender.getText())
+
+        events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb}
+        textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
+        self.add(textbox)
+        if switchPrevious:
+            textbox.setText(self._last_textbox.getText())
+            textbox.setValid(self._last_textbox.valid)
+            self._last_textbox.reset()
+            previous = self._last_textbox
+        self._last_textbox = textbox
+        return previous if switchPrevious else textbox
+
+    def _checkContact(self, contact, modify):
+        """
+        @param contact: the contact to check
+        @param modify: True if the contact is being modified
+        @return:
+        - VALID if the contact is valid
+        - INVALID if the contact is not valid but can be displayed
+        - DELETE if the contact should not be displayed at all
+        """
+        def countItemInList(list_, item):
+            """For some reason the built-in count function doesn't work..."""
+            count = 0
+            for elem in list_:
+                if elem == item:
+                    count += 1
+            return count
+        if contact is None or contact == "":
+            return DELETE
+        if countItemInList(self.getContacts(), contact) > (1 if modify else 0):
+            return DELETE
+        return VALID if contact in self._parent.list else INVALID
+
+    def addContact(self, contact, sender=None):
+        """The first parameter type is checked, so it is also possible to call addContact(sender).
+        If contact is not defined, sender.getText() is used. If sender is not defined, contact will
+        be written to the last textbox and a new textbox is added afterward.
+        @param contact: unicode
+        @param sender: DragAutoCompleteTextBox instance
+        """
+        if isinstance(contact, DragAutoCompleteTextBox):
+            sender = contact
+            contact = sender.getText()
+        valid = self._checkContact(contact, sender is not None)
+        if sender is None:
+            # method has been called to modify but to add a contact
+            if valid == VALID:
+                # eventually insert before the last textbox if it's not empty
+                sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
+                sender.setText(contact)
+        else:
+            sender.setValid(valid == VALID)
+        if valid != VALID:
+            if sender is not None and valid == DELETE:
+                sender.removeOrReset()
+            return False
+        if sender == self._last_textbox:
+            self._addTextBox()
+        try:
+            sender.setVisibleLength(len(contact))
+        except:
+            # IndexSizeError: Index or size is negative or greater than the allowed amount
+            log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)))
+        self._parent.removeFromRemainingList(contact)
+        self._last_textbox.setFocus(True)
+        return True
+
+    def emptyContacts(self):
+        """Empty the list of contacts."""
+        for child in self.getChildren():
+            if hasattr(child, "remove_btn"):
+                child.remove_btn.click()
+
+    def setContacts(self, tab):
+        """Set the contacts."""
+        self.emptyContacts()
+        if isinstance(tab, set):
+            tab = list(tab)
+        tab.sort()
+        for contact in tab:
+            self.addContact(contact)
+
+    def getContacts(self):
+        """Get the contacts
+        @return: an array of string"""
+        tab = []
+        for widget in self.getChildren():
+            if isinstance(widget, DragAutoCompleteTextBox):
+                # not to be mixed with EMPTY_SELECTION_ITEM
+                if widget.getText() != "":
+                    tab.append(widget.getText())
+        return tab
+
+    @property
+    def target_drop_cell(self):
+        """@return: the panel where something has been dropped."""
+        return self._parent.target_drop_cell
+
+    def setTargetDropCell(self, target_drop_cell):
+        """
+        XXX: Property setter here would not make it, you need a proper method!
+        @param target_drop_cell: the panel where something has been dropped."""
+        self._parent.setTargetDropCell(target_drop_cell)
+
+
+class ContactChooserPanel(DialogBox):
+    """Display the contacts chooser dialog. This has been implemented while
+    prototyping and is currently not used. Left for an eventual later use.
+    Replaced by the popup menu which allows to add a panel for Cc or Bcc.
+    """
+
+    def __init__(self, manager, **kwargs):
+        """Display a listbox for each contact key"""
+        DialogBox.__init__(self, autoHide=False, centered=True, **kwargs)
+        self.setHTML("Select contacts")
+        self.manager = manager
+        self.listboxes = {}
+        self.contacts = manager.getContacts()
+
+        container = VerticalPanel(Visible=True)
+        container.addStyleName("marginAuto")
+
+        grid = Grid(2, len(self.manager.keys_dict))
+        index = -1
+        for key in self.manager.keys_dict:
+            index += 1
+            grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index)
+            listbox = ListBox()
+            listbox.setMultipleSelect(True)
+            listbox.setVisibleItemCount(15)
+            listbox.addItem(EMPTY_SELECTION_ITEM)
+            for element in manager.list:
+                listbox.addItem(element)
+            self.listboxes[key] = listbox
+            grid.add(listbox, 1, index)
+        self._reset()
+
+        buttons = HorizontalPanel()
+        buttons.addStyleName("marginAuto")
+        btn_close = Button("Cancel", self.hide)
+        buttons.add(btn_close)
+        btn_reset = Button("Reset", self._reset)
+        buttons.add(btn_reset)
+        btn_ok = Button("OK", self._validate)
+        buttons.add(btn_ok)
+
+        container.add(grid)
+        container.add(buttons)
+
+        self.add(container)
+        self.center()
+
+    def _reset(self):
+        """Reset the selections."""
+        for key in self.manager.keys_dict:
+            listbox = self.listboxes[key]
+            for i in xrange(0, listbox.getItemCount()):
+                if listbox.getItemText(i) in self.contacts[key]:
+                    listbox.setItemSelected(i, "selected")
+                else:
+                    listbox.setItemSelected(i, "")
+
+    def _validate(self):
+        """Sets back the selected contacts to the good sub-panels."""
+        _map = {}
+        for key in self.manager.keys_dict:
+            selections = self.listboxes[key].getSelectedItemText()
+            if EMPTY_SELECTION_ITEM in selections:
+                selections.remove(EMPTY_SELECTION_ITEM)
+            _map[key] = selections
+        self.manager.setContacts(_map)
+        self.hide()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/logging.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,50 @@
+#!/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/>.
+"""This module configure logs for Libervia browser side"""
+
+from __pyjamas__ import console
+from constants import Const as C
+from sat.core import log  # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas
+
+
+class LiberviaLogger(log.Logger):
+
+    def out(self, message, level=None):
+        if level == C.LOG_LVL_DEBUG:
+            console.debug(message)
+        elif level == C.LOG_LVL_INFO:
+            console.info(message)
+        elif level == C.LOG_LVL_WARNING:
+            console.warn(message)
+        else:
+            console.error(message)
+
+
+def configure():
+    fmt = '[%(name)s] %(message)s'
+    log.configure(C.LOG_BACKEND_CUSTOM,
+                  logger_class = LiberviaLogger,
+                  level = C.LOG_LVL_DEBUG,
+                  fmt = fmt,
+                  output = None,
+                  logger = None,
+                  colors = False,
+                  force_colors = False)
+    # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed
+    LiberviaLogger.fmt = fmt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/menu.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,271 @@
+#!/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 pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.MenuBar import MenuBar
+from pyjamas.ui.MenuItem import MenuItem
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Frame import Frame
+from pyjamas import Window
+
+import jid
+
+import file_tools
+import xmlui
+import panels
+import dialog
+import contact_group
+
+
+class MenuCmd:
+
+    def __init__(self, object_, handler):
+        self._object = object_
+        self._handler = handler
+
+    def execute(self):
+        handler = getattr(self._object, self._handler)
+        handler()
+
+
+class PluginMenuCmd:
+
+    def __init__(self, host, action_id):
+        self.host = host
+        self.action_id = action_id
+
+    def execute(self):
+        self.host.launchAction(self.action_id, None)
+
+
+class LiberviaMenuBar(MenuBar):
+
+    def __init__(self):
+        MenuBar.__init__(self, vertical=False)
+        self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal")
+                                                    # TODO: properly adapt CSS to the new class name
+
+    def doItemAction(self, item, fireCommand):
+        MenuBar.doItemAction(self, item, fireCommand)
+        if item == self.items[-1] and self.popup:
+            self.popup.setPopupPosition(Window.getClientWidth() -
+                                        self.popup.getOffsetWidth() - 22,
+                                        self.getAbsoluteTop() +
+                                        self.getOffsetHeight() - 1)
+            self.popup.addStyleName('menuLastPopup')
+
+
+class AvatarUpload(file_tools.FileUploadPanel):
+    def __init__(self):
+        texts = {'ok_button': 'Upload avatar',
+                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
+                 'errback': "Can't open image... did you actually submit an image?",
+                 'body_errback': 'Please select another image file.',
+                 'callback': "Your new profile picture has been set!"}
+        file_tools.FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
+
+
+class Menu(SimplePanel):
+
+    def __init__(self, host):
+        self.host = host
+        SimplePanel.__init__(self)
+        self.setStyleName('menuContainer')
+
+    def createMenus(self, add_menus):
+        _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s"
+        menus_dict = {}
+        menus_order = []
+
+        def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd):
+            """ add a menu to menu_dict """
+            log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd))
+            try:
+                menu_bar = menus_dict[menu_name]
+            except KeyError:
+                menu_bar = menus_dict[menu_name] = MenuBar(vertical=True)
+                menus_order.append((menu_name, menu_name_i18n, icon))
+            if item_name_i18n and menu_cmd:
+                menu_bar.addItem(item_name_i18n, menu_cmd)
+
+        addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget"))
+        addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect"))
+        addMenu("Contacts", _("Contacts"), None, 'social', None)
+        addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom"))
+        addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio"))
+        addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame"))
+        addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame"))
+
+        # additional menus
+        for action_id, type_, path, path_i18n in add_menus:
+            if not path:
+                log.warning("skipping menu without path")
+                continue
+            if len(path) != len(path_i18n):
+                log.error("inconsistency between menu paths")
+                continue
+            menu_name = path[0]
+            menu_name_i18n = path_i18n[0]
+            item_name = path[1:]
+            if not item_name:
+                log.warning("skipping menu with a path of lenght 1 [%s]" % path[0])
+                continue
+            item_name_i18n = ' | '.join(path_i18n[1:])
+            addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id))
+
+        # menu items that should be displayed after the automatically added ones
+        addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
+
+        menus_order.append(None)  # we add separator
+
+        addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract"))
+        addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout"))
+        addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount"))
+        addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters"))
+
+        # XXX: temporary, will change when a full profile will be managed in SàT
+        addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
+
+        menubar = LiberviaMenuBar()
+
+        for menu_data in menus_order:
+            if menu_data is None:
+                _separator = MenuItem('', None)
+                _separator.setStyleName('menuSeparator')
+                menubar.addItem(_separator, None)
+            else:
+                menu_name, menu_name_i18n, icon = menu_data
+                menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name]))
+
+        self.add(menubar)
+
+    #General menu
+    def onWebWidget(self):
+        web_panel = panels.WebPanel(self.host, "http://www.goffi.org")
+        self.host.addWidget(web_panel)
+        self.host.setSelected(web_panel)
+
+    def onDisconnect(self):
+        def confirm_cb(answer):
+            if answer:
+                log.info("disconnection")
+                self.host.bridge.call('disconnect', None)
+        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?")
+        _dialog.show()
+
+    def onSocialContract(self):
+        _frame = Frame('contrat_social.html')
+        _frame.setStyleName('infoFrame')
+        _dialog = dialog.GenericDialog("Contrat Social", _frame)
+        _dialog.setSize('80%', '80%')
+        _dialog.show()
+
+    def onAbout(self):
+        _about = HTML("""<b>Libervia</b>, a Salut &agrave; Toi project<br />
+<br />
+You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br />
+Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br />
+Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br />
+<br />
+Any help welcome :)
+<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p>
+""")
+        _dialog = dialog.GenericDialog("About", _about)
+        _dialog.show()
+
+    #Contact menu
+    def onManageContactGroups(self):
+        """Open the contact groups manager."""
+
+        def onCloseCallback():
+            pass
+
+        contact_group.ContactGroupEditor(self.host, None, onCloseCallback)
+
+    #Group menu
+    def onJoinRoom(self):
+
+        def invite(room_jid, contacts):
+            for contact in contacts:
+                self.host.bridge.call('inviteMUC', None, contact, room_jid)
+
+        def join(room_jid, contacts):
+            if self.host.whoami:
+                nick = self.host.whoami.node
+                if room_jid not in [room.bare for room in self.host.room_list]:
+                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick)
+                else:
+                    self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, jid.JID(room_jid).bare)
+                    invite(room_jid, contacts)
+
+        dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
+
+    def onCollectiveRadio(self):
+        def callback(room_jid, contacts):
+            self.host.bridge.call('launchRadioCollective', None, contacts, room_jid)
+        dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
+
+    #Game menu
+    def onTarotGame(self):
+        def onPlayersSelected(room_jid, other_players):
+            self.host.bridge.call('launchTarotGame', None, other_players, room_jid)
+        dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True))
+
+    def onXiangqiGame(self):
+        Window.alert("A Xiangqi game is planed, but not available yet")
+
+    #Settings menu
+
+    def onAccount(self):
+        def gotUI(xml_ui):
+            if not xml_ui:
+                return
+            body = xmlui.XMLUI(self.host, xml_ui)
+            _dialog = dialog.GenericDialog("Manage your account", body, options=['NO_CLOSE'])
+            body.setCloseCb(_dialog.close)
+            _dialog.show()
+        self.host.bridge.call('getAccountDialogUI', gotUI)
+
+    def onParameters(self):
+        def gotParams(xml_ui):
+            if not xml_ui:
+                return
+            body = xmlui.XMLUI(self.host, xml_ui)
+            _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE'])
+            body.setCloseCb(_dialog.close)
+            _dialog.setSize('80%', '80%')
+            _dialog.show()
+        self.host.bridge.call('getParamsUI', gotParams)
+
+    def removeItemParams(self):
+        """Remove the Parameters item from the Settings menu bar."""
+        self.menu_settings.removeItem(self.item_params)
+
+    def onAvatarUpload(self):
+        body = AvatarUpload()
+        _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.setWidth('40%')
+        _dialog.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/nativedom.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,107 @@
+#!/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/>.
+
+"""
+This class provide basic DOM parsing based on native javascript parser
+__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989
+"""
+
+from __pyjamas__ import JS
+
+
+class Node():
+
+    def __init__(self, js_node):
+        self._node = js_node
+
+    def _jsNodesList2List(self, js_nodes_list):
+        ret = []
+        for i in range(len(js_nodes_list)):
+            #ret.append(Element(js_nodes_list.item(i)))
+            ret.append(self.__class__(js_nodes_list.item(i)))  # XXX: Ugly, but used to word around a Pyjamas's bug
+        return ret
+
+    @property
+    def nodeName(self):
+        return self._node.nodeName
+
+    @property
+    def wholeText(self):
+        return self._node.wholeText
+
+    @property
+    def childNodes(self):
+        return self._jsNodesList2List(self._node.childNodes)
+
+    def getAttribute(self, attr):
+        return self._node.getAttribute(attr)
+
+    def setAttribute(self, attr, value):
+        return self._node.setAttribute(attr, value)
+
+    def hasAttribute(self, attr):
+        return self._node.hasAttribute(attr)
+
+    def toxml(self):
+        return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""")
+
+
+class Element(Node):
+
+    def __init__(self, js_node):
+        Node.__init__(self, js_node)
+
+    def getElementsByTagName(self, tagName):
+        return self._jsNodesList2List(self._node.getElementsByTagName(tagName))
+
+
+class Document(Node):
+
+    def __init__(self, js_document):
+        Node.__init__(self, js_document)
+
+    @property
+    def documentElement(self):
+        return Element(self._node.documentElement)
+
+
+class NativeDOM:
+
+    def __init__(self):
+        JS("""
+
+        if (typeof window.DOMParser != "undefined") {
+            this.parseXml = function(xmlStr) {
+                return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml");
+            };
+        } else if (typeof window.ActiveXObject != "undefined" &&
+               new window.ActiveXObject("Microsoft.XMLDOM")) {
+            this.parseXml = function(xmlStr) {
+                var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
+                xmlDoc.async = "false";
+                xmlDoc.loadXML(xmlStr);
+                return xmlDoc;
+            };
+        } else {
+            throw new Error("No XML parser found");
+        }
+        """)
+
+    def parseString(self, xml):
+        return Document(self.parseXml(xml))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/notification.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,122 @@
+from __pyjamas__ import JS, wnd
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat.core.i18n import _
+
+from pyjamas import Window
+from pyjamas.Timer import Timer
+
+import dialog
+
+TIMER_DELAY = 5000
+
+
+class Notification(object):
+    """
+    If the browser supports it, the user allowed it to and the tab is in the
+    background, send desktop notifications on messages.
+
+    Requires both Web Notifications and Page Visibility API.
+    """
+
+    def __init__(self):
+        self.enabled = False
+        user_agent = None
+        notif_permission = None
+        JS("""
+        if (!('hidden' in document))
+            document.hidden = false;
+
+        user_agent = navigator.userAgent
+
+        if (!('Notification' in window))
+            return;
+
+        notif_permission = Notification.permission
+
+        if (Notification.permission === 'granted')
+            this.enabled = true;
+
+        else if (Notification.permission === 'default') {
+            Notification.requestPermission(function(permission){
+                if (permission !== 'granted')
+                    return;
+
+                self.enabled = true; //need to use self instead of this
+            });
+        }
+        """)
+
+        if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']:
+            self.user_agent = user_agent
+            self._installChromiumWorkaround()
+
+        wnd().onfocus = self.onFocus
+        # wnd().onblur = self.onBlur
+        self._notif_count = 0
+        self._orig_title = Window.getTitle()
+
+    def _installChromiumWorkaround(self):
+        # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event
+        # see https://code.google.com/p/chromium/issues/detail?id=274284
+        # FIXME: need to be removed if Chromium behaviour changes
+        try:
+            version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1]
+            version = int(version_full.split('.')[0])
+        except (IndexError, ValueError):
+            log.warning("Can't find Chromium version")
+            version = 0
+        log.info("Chromium version: %d" % (version,))
+        if version < 22:
+            log.info("Notification use the old prefixed version or are unmanaged")
+            return
+        if version < 32:
+            dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show()
+            return
+
+        log.info("==> Installing Chromium notifications request workaround <==")
+        self._old_click = wnd().onclick
+        wnd().onclick = self._chromiumWorkaround
+
+    def _chromiumWorkaround(self):
+        log.info("Activating workaround")
+        JS("""
+            Notification.requestPermission(function(permission){
+                if (permission !== 'granted')
+                    return;
+                self.enabled = true; //need to use self instead of this
+            });
+        """)
+        wnd().onclick = self._old_click
+
+    def onFocus(self):
+        Window.setTitle(self._orig_title)
+        self._notif_count = 0
+
+    # def onBlur(self):
+    #     pass
+
+    def isHidden(self):
+        JS("""return document.hidden;""")
+
+    def _notify(self, title, body, icon):
+        if not self.enabled:
+            return
+        notification = None
+        JS("""
+           notification = new Notification(title, {body: body, icon: icon});
+           // Probably won’t work, but it doesn’t hurt to try.
+           notification.addEventListener('click', function() {
+               window.focus();
+           });
+           """)
+        notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close())
+
+    def highlightTab(self):
+        self._notif_count += 1
+        Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count))
+
+    def notify(self, title, body, icon='/media/icons/apps/48/sat.png'):
+        if self.isHidden():
+            self._notify(title, body, icon)
+            self.highlightTab()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/panels.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,1419 @@
+#!/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.strings import addURLToText
+from sat_frontends.tools.games import SYMBOLS
+from sat.core.i18n import _
+
+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.Frame import Frame
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+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, KEY_UP, KEY_DOWN, KeyboardHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+from pyjamas import Window
+from __pyjamas__ import doc
+
+from datetime import datetime
+from time import time
+
+import jid
+import html_tools
+import base_panels
+import card_game
+import radiocol
+import menu
+import dialog
+import base_widget
+import richtext
+import contact
+from constants import Const as C
+import plugin_xep_0085
+
+
+# TODO: at some point we should decide which behaviors to keep and remove these two constants
+TOGGLE_EDITION_USE_ICON = False  # set to True to use an icon inside the "toggle syntax" button
+NEW_MESSAGE_USE_BUTTON = False  # set to True to display the "New message" button instead of an empty entry
+
+
+class UniBoxPanel(HorizontalPanel):
+    """Panel containing the UniBox"""
+
+    def __init__(self, host):
+        HorizontalPanel.__init__(self)
+        self.host = host
+        self.setStyleName('uniBoxPanel')
+        self.unibox = None
+
+    def refresh(self):
+        """Enable or disable this panel. Contained widgets are created when necessary."""
+        enable = self.host.getUIParam('unibox')
+        self.setVisible(enable)
+        if enable and not self.unibox:
+            self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
+            self.button.setTitle('Open the rich text editor')
+            self.button.addStyleName('uniBoxButton')
+            self.add(self.button)
+            self.unibox = UniBox(self.host)
+            self.add(self.unibox)
+            self.setCellWidth(self.unibox, '100%')
+            self.button.addClickListener(self.openRichMessageEditor)
+            self.unibox.addKey("@@: ")
+            self.unibox.onSelectedChange(self.host.getSelected())
+
+    def openRichMessageEditor(self):
+        """Open the rich text editor."""
+        self.button.setVisible(False)
+        self.unibox.setVisible(False)
+        self.setCellWidth(self.unibox, '0px')
+        self.host.panel._contactsMove(self)
+
+        def afterEditCb():
+            Window.removeWindowResizeListener(self)
+            self.host.panel._contactsMove(self.host.panel._hpanel)
+            self.setCellWidth(self.unibox, '100%')
+            self.button.setVisible(True)
+            self.unibox.setVisible(True)
+            self.host.resize()
+
+        richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
+        Window.addWindowResizeListener(self)
+        self.host.resize()
+
+    def onWindowResized(self, width, height):
+        right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
+        left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth()
+        ideal_width = right - left - 40
+        self.host.richtext.setWidth("%spx" % ideal_width)
+
+
+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)
+        self._selected_cache = None
+
+    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._selected_cache.onTextEntered(_txt)
+                self.host._updateInputHistory(_txt)
+            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."""
+        if hasattr(self._selected_cache, "target"):
+            self._selected_cache.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 UniBox(MessageBox, MouseHandler):  # AutoCompleteTextBox):
+    """This text box is used as a main typing point, for message, microblog, etc"""
+
+    def __init__(self, host):
+        MessageBox.__init__(self, host)
+        #AutoCompleteTextBox.__init__(self)
+        self.setStyleName('uniBox')
+        host.addSelectedListener(self.onSelectedChange)
+
+    def addKey(self, key):
+        return
+        #self.getCompletionItems().completions.append(key)
+
+    def removeKey(self, key):
+        return
+        # TODO: investigate why AutoCompleteTextBox doesn't work here,
+        # maybe it can work on a TextBox but no TextArea. Remove addKey
+        # and removeKey methods if they don't serve anymore.
+        try:
+            self.getCompletionItems().completions.remove(key)
+        except KeyError:
+            log.warning("trying to remove an unknown key")
+
+    def _getTarget(self, txt):
+        """ Say who will receive the messsage
+        @return: a tuple (selected, target_type, target info) with:
+            - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None))
+            - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
+            - msg: HTML message which will appear in the privacy warning banner """
+        target = self._selected_cache
+
+        def getSelectedOrStatus():
+            if target and target.isSelectable():
+                _type, msg = target.getWarningData()
+                target_hook = None  # we use the selected widget, not a hook
+            else:
+                _type, msg = "STATUS", "This will be your new status message"
+                target_hook = (txt, None)
+            return (target_hook, _type, msg)
+
+        if not txt.startswith('@'):
+            target_hook, _type, msg = getSelectedOrStatus()
+        elif txt.startswith('@@: '):
+            _type = "PUBLIC"
+            msg = MicroblogPanel.warning_msg_public
+            target_hook = (txt[4:], None)
+        elif txt.startswith('@'):
+            _end = txt.find(': ')
+            if _end == -1:
+                target_hook, _type, msg = getSelectedOrStatus()
+            else:
+                group = txt[1:_end]  # only one target group is managed for the moment
+                if not group or not group in self.host.contact_panel.getGroups():
+                    # the group doesn't exists, we ignore the key
+                    group = None
+                    target_hook, _type, msg = getSelectedOrStatus()
+                else:
+                    _type = "GROUP"
+                    msg = MicroblogPanel.warning_msg_group % group
+                    target_hook = (txt[_end + 2:], group)
+        else:
+            log.error("Unknown target")
+            target_hook, _type, msg = getSelectedOrStatus()
+
+        return (target_hook, _type, msg)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        _txt = self.getText()
+        target_hook, type_, msg = self._getTarget(_txt)
+
+        if keycode == KEY_ENTER:
+            if _txt:
+                if target_hook:
+                    parsed_txt, data = target_hook
+                    self.host.send([(type_, data)], parsed_txt)
+                    self.host._updateInputHistory(_txt)
+                    self.setText('')
+            self.host.showWarning(None, None)
+        else:
+            self.host.showWarning(type_, msg)
+        MessageBox.onKeyPress(self, sender, keycode, modifiers)
+
+    def getTargetAndData(self):
+        """For external use, to get information about the (hypothetical) message
+        that would be sent if we press Enter right now in the unibox.
+        @return a tuple (target, data) with:
+          - data: what would be the content of the message (body)
+          - target: JID, group with the prefix "@" or the public entity "@@"
+        """
+        _txt = self.getText()
+        target_hook, _type, _msg = self._getTarget(_txt)
+        if target_hook:
+            data, target = target_hook
+            if target is None:
+                return target_hook
+            return (data, "@%s" % (target if target != "" else "@"))
+        if isinstance(self._selected_cache, MicroblogPanel):
+            groups = self._selected_cache.accepted_groups
+            target = "@%s" % (groups[0] if len(groups) > 0 else "@")
+            if len(groups) > 1:
+                Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0])
+        elif isinstance(self._selected_cache, ChatPanel):
+            target = self._selected_cache.target
+        else:
+            target = None
+        return (_txt, target)
+
+    def onWidgetClosed(self, lib_wid):
+        """Called when a libervia widget is closed"""
+        if self._selected_cache == lib_wid:
+            self.onSelectedChange(None)
+
+    """def complete(self):
+
+        #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
+        #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
+        return AutoCompleteTextBox.complete(self)"""
+
+
+class WarningPopup():
+
+    def __init__(self):
+        self._popup = None
+        self._timer = Timer(notify=self._timeCb)
+
+    def showWarning(self, type_=None, msg=None, duration=2000):
+        """Display a popup information message, e.g. to notify the recipient of a message being composed.
+        If type_ is None, a popup being currently displayed will be hidden.
+        @type_: a type determining the CSS style to be applied (see __showWarning)
+        @msg: message to be displayed
+        """
+        if type_ is None:
+            self.__removeWarning()
+            return
+        if not self._popup:
+            self.__showWarning(type_, msg)
+        elif (type_, msg) != self._popup.target_data:
+            self._timeCb(None)  # we remove the popup
+            self.__showWarning(type_, msg)
+
+        self._timer.schedule(duration)
+
+    def __showWarning(self, type_, msg):
+        """Display a popup information message, e.g. to notify the recipient of a message being composed.
+        @type_: a type determining the CSS style to be applied. For now the defined styles are
+        "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE".
+        @msg: message to be displayed
+        """
+        if type_ == "NONE":
+            return
+        if not msg:
+            log.warning("no msg set uniBox warning")
+            return
+        if type_ == "PUBLIC":
+            style = "targetPublic"
+        elif type_ == "GROUP":
+            style = "targetGroup"
+        elif type_ == "STATUS":
+            style = "targetStatus"
+        elif type_ == "ONE2ONE":
+            style = "targetOne2One"
+        else:
+            log.error("unknown message type")
+            return
+        contents = HTML(msg)
+
+        self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
+        self._popup.target_data = (type_, msg)
+        self._popup.add(contents)
+        self._popup.setStyleName("warningPopup")
+        if style:
+            self._popup.addStyleName(style)
+
+        left = 0
+        top = 0  # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
+        self._popup.setPopupPosition(left, top)
+        self._popup.show()
+
+    def _timeCb(self, timer):
+        if self._popup:
+            self._popup.hide()
+            del self._popup
+            self._popup = None
+
+    def __removeWarning(self):
+        """Remove the popup"""
+        self._timeCb(None)
+
+
+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 = 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 = HTMLPanel('')
+        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')
+        self.avatar = Image(self._blog_panel.host.getAvatar(self.author))
+        entry_avatar.add(self.avatar)
+        self.panel.add(entry_avatar)
+
+        if TOGGLE_EDITION_USE_ICON:
+            self.entry_dialog = HorizontalPanel()
+        else:
+            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.setHTML("""<div class='mb_entry_header'>
+                                   <span class='mb_entry_author'>%(author)s</span> on
+                                   <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
+                               </div>""" % {'author': html_tools.html_sanitize(self.author),
+                                            'published': datetime.fromtimestamp(self.published),
+                                            'updated': update_text if self.published != self.updated else ''
+                                            }
+                            )
+
+    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 str(self.node).endswith(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:
+            try:  # prevent re-selection of the main entry after a comment has been focused
+                if self.__ignoreNextEvent:
+                    self.__ignoreNextEvent = False
+                    return
+            except AttributeError:
+                pass
+            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.__ignoreNextEvent = True
+            self._comment()
+
+    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': str(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, 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)
+            if self.type == 'main_item':  # restore the "New message" button
+                self._blog_panel.refresh()
+            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 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 = base_panels.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
+        self.bubble.addStyleName("bubble")
+        try:
+            self.toggle_syntax_button.removeFromParent()
+        except 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)
+            return
+        data = {'id': str(time()),
+                'new': True,
+                'type': 'comment',
+                'author': self._blog_panel.host.whoami.bare,
+                'service': self.comments_service,
+                'node': self.comments_node
+                }
+        entry = self._blog_panel.addEntry(data)
+        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)
+
+    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 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')
+            if TOGGLE_EDITION_USE_ICON:
+                entry.entry_dialog.setWidth('80%')
+                entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax)
+                entry.toggle_syntax_button.setTitle(title)
+                entry.entry_dialog.add(entry.toggle_syntax_button)
+            else:
+                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(base_widget.LiberviaWidget):
+    warning_msg_public = "This message will be PUBLIC 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 group <span class='warningTarget'>%s</span>"
+
+    def __init__(self, host, accepted_groups):
+        """Panel used to show microblog
+        @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
+        """
+        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
+        self.setAcceptedGroup(accepted_groups)
+        self.host = host
+        self.entries = {}
+        self.comments = {}
+        self.selected_entry = None
+        self.vpanel = VerticalPanel()
+        self.vpanel.setStyleName('microblogPanel')
+        self.setWidget(self.vpanel)
+
+    def refresh(self):
+        """Refresh the display of this widget. If the unibox is disabled,
+        display the 'New message' button or an empty bubble on top of the panel"""
+        if hasattr(self, 'new_button'):
+            self.new_button.setVisible(self.host.uni_box is None)
+            return
+        if self.host.uni_box is None:
+            def addBox():
+                if hasattr(self, 'new_button'):
+                    self.new_button.setVisible(False)
+                data = {'id': str(time()),
+                        'new': True,
+                        'author': self.host.whoami.bare,
+                        }
+                entry = self.addEntry(data)
+                entry.edit(True)
+            if NEW_MESSAGE_USE_BUTTON:
+                self.new_button = Button("New message", listener=addBox)
+                self.new_button.setStyleName("microblogNewButton")
+                self.vpanel.insert(self.new_button, 0)
+            elif not self.getNewMainEntry():
+                addBox()
+
+    def getNewMainEntry(self):
+        """Get the new entry being edited, or None if it doesn't exists.
+
+        @return (MicroblogEntry): the new entry being edited.
+        """
+        try:
+            first = self.vpanel.children[0]
+        except IndexError:
+            return None
+        assert(first.type == 'main_item')
+        return first if first.empty else None
+
+    @classmethod
+    def registerClass(cls):
+        base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel)
+        base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel)
+
+    @classmethod
+    def createPanel(cls, host, item):
+        """Generic panel creation for one, several or all groups (meta).
+        @parem host: the SatWebFrontend instance
+        @param item: single group as a string, list of groups
+         (as an array) or None (for the meta group = "all groups")
+        @return: the created MicroblogPanel
+        """
+        _items = item if isinstance(item, list) else ([] if item is None else [item])
+        _type = 'ALL' if _items == [] else 'GROUP'
+        # XXX: pyjamas doesn't support use of cls directly
+        _new_panel = MicroblogPanel(host, _items)
+        host.FillMicroblogPanel(_new_panel)
+        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
+        host.setSelected(_new_panel)
+        _new_panel.refresh()
+        return _new_panel
+
+    @classmethod
+    def createMetaPanel(cls, host, item):
+        """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group"""
+        return MicroblogPanel.createPanel(host, None)
+
+    @property
+    def accepted_groups(self):
+        return self._accepted_groups
+
+    def matchEntity(self, entity):
+        """
+        @param entity: single group as a string, list of groups
+        (as an array) or None (for the meta group = "all groups")
+        @return: True if self matches the given entity
+        """
+        entity = entity if isinstance(entity, list) else ([] if entity is None else [entity])
+        entity.sort()  # sort() do not return the sorted list: do it here, not on the "return" line
+        return self.accepted_groups == entity
+
+    def getWarningData(self, comment=None):
+        """
+        @param comment: True if the composed message is a comment. If None, consider we are
+        composing from the unibox and guess the message type from self.selected_entry
+        @return: a couple (type, msg) for calling self.host.showWarning"""
+        if comment is None:  # composing from the unibox
+            if self.selected_entry and not self.selected_entry.comments:
+                log.error("an item without comment is selected")
+                return ("NONE", None)
+            comment = self.selected_entry is not None
+        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:
+            # we only accept one group at the moment
+            # FIXME: manage several groups
+            return ("GROUP", self.warning_msg_group % self._accepted_groups[0])
+
+    def onTextEntered(self, text):
+        if self.selected_entry:
+            # we are entering a comment
+            comments_url = self.selected_entry.comments
+            if not comments_url:
+                raise Exception("ERROR: the comments URL is empty")
+            target = ("COMMENT", comments_url)
+        elif not self._accepted_groups:
+            # we are entering a public microblog
+            target = ("PUBLIC", None)
+        else:
+            # we are entering a microblog restricted to a group
+            # FIXME: manage several groups
+            target = ("GROUP", self._accepted_groups[0])
+        self.host.send([target], 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: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
+        """
+        log.debug("Massive insertion of %d microblogs" % len(mblogs))
+        for publisher in mblogs:
+            log.debug("adding blogs for [%s]" % publisher)
+            for mblog in mblogs[publisher]:
+                if not "content" in mblog:
+                    log.warning("No content found in microblog [%s]", mblog)
+                    continue
+                self.addEntry(mblog)
+
+    def mblogsInsert(self, mblogs):
+        """ Insert several microblogs at once
+        @param mblogs: list of microblogs
+        """
+        for mblog in mblogs:
+            if not "content" in mblog:
+                log.warning("No content found in microblog [%s]", mblog)
+                continue
+            self.addEntry(mblog)
+
+    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"""
+        assert(isinstance(reverse, bool))
+        if entry.empty:
+            entry.published = 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:
+            if not isinstance(child, MicroblogEntry):
+                idx += 1
+                continue
+            condition_to_stop = child.empty or (child.published > entry.published)
+            if condition_to_stop != reverse:  # != is XOR
+                break
+            idx += 1
+
+        vpanel.insert(entry, idx)
+
+    def addEntry(self, data):
+        """Add an entry to the panel
+        @param data: dict containing the item data
+        @return: the added entry, or None
+        """
+        _entry = MicroblogEntry(self, data)
+        if _entry.type == "comment":
+            comments_hash = (_entry.service, _entry.node)
+            if not comments_hash 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)
+            return _entry
+
+        if _entry.id in self.entries:  # update
+            idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
+            self.vpanel.remove(self.entries[_entry.id])
+            self.vpanel.insert(_entry, idx)
+        else:  # new entry
+            self._chronoInsert(self.vpanel, _entry)
+        self.entries[_entry.id] = _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)
+
+        return _entry
+
+    def removeEntry(self, type_, id_):
+        """Remove an entry from the panel
+        @param type_: entry type ('main_item' or 'comment')
+        @param id_: entry id
+        """
+        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()
+                    self.selected_entry = None
+                    break
+            elif isinstance(child, VerticalPanel) and type_ == 'comment':
+                for comment in child.getChildren():
+                    if comment.id == id_:
+                        comment.removeFromParent()
+                        self.selected_entry = None
+                        break
+
+    def setSelectedEntry(self, 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!")
+        removeStyle = lambda entry: entry.removeStyleName('selected_entry')
+        if not self.host.uni_box or not entry.comments:
+            entry.addStyleName('selected_entry')  # blink the clicked entry
+            clicked_entry = entry  # entry may be None when the timer is done
+            Timer(500, lambda timer: removeStyle(clicked_entry))
+        if not self.host.uni_box:
+            return  # unibox is disabled
+        # from here the previous behavior (toggle main item selection) is conserved
+        entry = entry if entry.comments else None
+        if self.selected_entry == entry:
+            entry = None
+        if self.selected_entry:
+            removeStyle(self.selected_entry)
+        if entry:
+            log.debug("microblog entry selected (author=%s)" % entry.author)
+            entry.addStyleName('selected_entry')
+        self.selected_entry = entry
+
+    def updateValue(self, type_, jid, value):
+        """Update a jid value in entries
+        @param type_: one of 'avatar', 'nick'
+        @param jid: jid concerned
+        @param value: new value"""
+        def updateVPanel(vpanel):
+            for child in vpanel.children:
+                if isinstance(child, MicroblogEntry) and child.author == jid:
+                    child.updateAvatar(value)
+                elif isinstance(child, VerticalPanel):
+                    updateVPanel(child)
+        if type_ == 'avatar':
+            updateVPanel(self.vpanel)
+
+    def setAcceptedGroup(self, group):
+        """Add one or more group(s) which can be displayed in this panel.
+        Prevent from duplicate values and keep the list sorted.
+        @param group: string of the group, or list of string
+        """
+        if not hasattr(self, "_accepted_groups"):
+            self._accepted_groups = []
+        groups = group if isinstance(group, list) else [group]
+        for _group in groups:
+            if _group not in self._accepted_groups:
+                self._accepted_groups.append(_group)
+        self._accepted_groups.sort()
+
+    def isJidAccepted(self, jid):
+        """Tell if a jid is actepted and shown in this panel
+        @param jid: jid
+        @return: True if the jid is accepted"""
+        if self.accept_all():
+            return True
+        for group in self._accepted_groups:
+            if self.host.contact_panel.isContactInGroup(group, jid):
+                return True
+        return False
+
+
+class StatusPanel(base_panels.HTMLTextEditor):
+
+    EMPTY_STATUS = '&lt;click to set a status&gt;'
+
+    def __init__(self, host, status=''):
+        self.host = host
+        modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
+        base_panels.HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True})
+        self.edit(False)
+        self.setStyleName('statusPanel')
+
+    @property
+    def status(self):
+        return self._original_content['text']
+
+    def __cleanContent(self, content):
+        status = content['text']
+        if status == self.EMPTY_STATUS or status in C.PRESENCE.values():
+            content['text'] = ''
+        return content
+
+    def getContent(self):
+        return self.__cleanContent(base_panels.HTMLTextEditor.getContent(self))
+
+    def setContent(self, content):
+        content = self.__cleanContent(content)
+        base_panels.BaseTextEditor.setContent(self, content)
+
+    def setDisplayContent(self):
+        status = self._original_content['text']
+        try:
+            presence = self.host.status_panel.presence
+        except AttributeError:  # during initialization
+            presence = None
+        if not status:
+            if presence and presence in C.PRESENCE:
+                status = C.PRESENCE[presence]
+            else:
+                status = self.EMPTY_STATUS
+        self.display.setHTML(addURLToText(status))
+
+
+class PresenceStatusPanel(HorizontalPanel, ClickHandler):
+
+    def __init__(self, host, presence="", status=""):
+        self.host = host
+        HorizontalPanel.__init__(self, Width='100%')
+        self.presence_button = Label(u"◉")
+        self.presence_button.setStyleName("presence-button")
+        self.status_panel = StatusPanel(host, status=status)
+        self.setPresence(presence)
+        entries = {}
+        for value in C.PRESENCE.keys():
+            entries.update({C.PRESENCE[value]: {"value": value}})
+
+        def callback(sender, key):
+            self.setPresence(entries[key]["value"])  # order matters
+            self.host.send([("STATUS", None)], self.status_panel.status)
+
+        self.presence_list = base_panels.PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"})
+        self.presence_list.registerClickSender(self.presence_button)
+
+        panel = HorizontalPanel()
+        panel.add(self.presence_button)
+        panel.add(self.status_panel)
+        panel.setCellVerticalAlignment(self.presence_button, 'baseline')
+        panel.setCellVerticalAlignment(self.status_panel, 'baseline')
+        panel.setStyleName("marginAuto")
+        self.add(panel)
+
+        self.status_panel.edit(False)
+
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    @property
+    def presence(self):
+        return self._presence
+
+    @property
+    def status(self):
+        return self.status_panel._original_content['text']
+
+    def setPresence(self, presence):
+        self._presence = presence
+        contact.setPresenceStyle(self.presence_button, self._presence)
+
+    def setStatus(self, status):
+        self.status_panel.setContent({'text': status})
+        self.status_panel.setDisplayContent()
+
+    def onClick(self, sender):
+        # As status is the default target of uniBar, we don't want to select anything if click on it
+        self.host.setSelected(None)
+
+
+class ChatPanel(base_widget.LiberviaWidget):
+
+    def __init__(self, host, target, type_='one2one'):
+        """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"""
+        base_widget.LiberviaWidget.__init__(self, host, title=target.bare, selectable=True)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+        self.type = type_
+        self.nick = None
+        if not target:
+            log.error("Empty target !")
+            return
+        self.target = target
+        self.__body = AbsolutePanel()
+        self.__body.setStyleName('chatPanel_body')
+        chat_area = HorizontalPanel()
+        chat_area.setStyleName('chatArea')
+        if type_ == 'group':
+            self.occupants_list = base_panels.OccupantsList()
+            chat_area.add(self.occupants_list)
+        self.__body.add(chat_area)
+        self.content = AbsolutePanel()
+        self.content.setStyleName('chatContent')
+        self.content_scroll = base_widget.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, str(self.target))
+        self._state = None
+
+    @classmethod
+    def registerClass(cls):
+        base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
+
+    @classmethod
+    def createPanel(cls, host, item):
+        _contact = item if isinstance(item, jid.JID) else jid.JID(item)
+        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
+        _new_panel = ChatPanel(host, _contact)  # 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"""
+        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)
+            return
+        if enable_box:
+            self.message_box = MessageBox(self.host)
+            self.message_box.onSelectedChange(self)
+            self.vpanel.add(self.message_box)
+
+    def matchEntity(self, entity):
+        """
+        @param entity: target jid as a string or jid.JID instance.
+        Could also be a couple with a type in the second element.
+        @return: True if self matches the given entity
+        """
+        if isinstance(entity, tuple):
+            entity, type_ = entity if len(entity) > 1 else (entity[0], self.type)
+        else:
+            type_ = self.type
+        entity = entity if isinstance(entity, jid.JID) else jid.JID(entity)
+        try:
+            return self.target.bare == entity.bare and self.type == type_
+        except AttributeError as e:
+            e.include_traceback()
+            return False
+
+    def getWarningData(self):
+        if self.type not in ["one2one", "group"]:
+            raise Exception("Unmanaged type !")
+        if self.type == "one2one":
+            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
+        elif self.type == "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 == "one2one" else "GROUP", msg)
+
+    def onTextEntered(self, text):
+        self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text)
+        self.state_machine._onEvent("active")
+
+    def onQuit(self):
+        base_widget.LiberviaWidget.onQuit(self)
+        if self.type == 'group':
+            self.host.bridge.call('mucLeave', None, 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 users presents in this room
+        @param occupants: list of nicks (string)"""
+        self.occupants_list.clear()
+        for nick in nicks:
+            self.occupants_list.addOccupant(nick)
+
+    def userJoined(self, nick, data):
+        self.occupants_list.addOccupant(nick)
+        self.printInfo("=> %s has joined the room" % nick)
+
+    def userLeft(self, nick, data):
+        self.occupants_list.removeOccupant(nick)
+        self.printInfo("<= %s has left the room" % nick)
+
+    def changeUserNick(self, old_nick, new_nick):
+        assert(self.type == "group")
+        self.occupants_list.removeOccupant(old_nick)
+        self.occupants_list.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=20):
+        """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, to_jid, 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(from_jid, message, extra, timestamp)
+        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
+
+    def printInfo(self, msg, type_='normal', link_cb=None):
+        """Print general info
+        @param msg: message to print
+        @param type_: one of:
+            "normal": general info like "toto has joined the room"
+            "link": general info that is clickable like "click here to join the main room"
+            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
+        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
+        """
+        _wid = HTML(msg) if type_ == 'link' else Label(msg)
+        if type_ == 'normal':
+            _wid.setStyleName('chatTextInfo')
+        elif type_ == 'link':
+            _wid.setStyleName('chatTextInfo-link')
+            if link_cb:
+                _wid.addClickListener(link_cb)
+        elif type_ == 'me':
+            _wid.setStyleName('chatTextMe')
+        else:
+            _wid.setStyleName('chatTextInfo')
+        self.content.add(_wid)
+
+    def printMessage(self, from_jid, msg, extra, timestamp=None):
+        """Print message in chat window. Must be implemented by child class"""
+        _jid = jid.JID(from_jid)
+        nick = _jid.node if self.type == 'one2one' else _jid.resource
+        mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare  # mymess = True if message comes from local user
+        if msg.startswith('/me '):
+            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me')
+            return
+        self.content.add(base_panels.ChatText(timestamp, nick, mymess, msg, extra.get('xhtml')))
+        self.content_scroll.scrollToBottom()
+
+    def startGame(self, game_type, waiting, referee, players, *args):
+        """Configure the chat window to start a game"""
+        classes = {"Tarot": card_game.CardPanel, "RadioCol": radiocol.RadioColPanel}
+        if game_type not in classes.keys():
+            return  # unknown game
+        attr = game_type.lower()
+        self.occupants_list.updateSpecials(players, SYMBOLS[attr])
+        if waiting or not self.nick in players:
+            return  # waiting for player or not playing
+        attr = "%s_panel" % attr
+        if hasattr(self, attr):
+            return
+        log.info("%s Game Started \o/" % game_type)
+        panel = classes[game_type](self, referee, self.nick, players, *args)
+        setattr(self, attr, panel)
+        self.vpanel.insert(panel, 0)
+        self.vpanel.setCellHeight(panel, panel.getHeight())
+
+    def getGame(self, game_type):
+        """Return class managing the game type"""
+        # TODO: check that the game is launched, and manage errors
+        if game_type == "Tarot":
+            return self.tarot_panel
+        elif game_type == "RadioCol":
+            return self.radiocol_panel
+
+    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: None for one2one, the MUC user nick or ALL_OCCUPANTS
+        """
+        if nick:
+            assert(self.type == 'group')
+            occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick]
+            for occupant in occupants:
+                self.occupants_list.occupants_list[occupant].setState(state)
+        else:
+            assert(self.type == 'one2one')
+            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 ChatPanel dialog"""
+        if self._state:
+            self.setTitle(self.target.bare + " (" + self._state + ")")
+        else:
+            self.setTitle(self.target.bare)
+
+
+class WebPanel(base_widget.LiberviaWidget):
+    """ (mini)browser like widget """
+
+    def __init__(self, host, url=None):
+        """
+        @param host: SatWebFrontend instance
+        """
+        base_widget.LiberviaWidget.__init__(self, host)
+        self._vpanel = VerticalPanel()
+        self._vpanel.setSize('100%', '100%')
+        self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
+        self._url.setText(url or "")
+        self._url.setWidth('100%')
+        hpanel = HorizontalPanel()
+        hpanel.add(self._url)
+        btn = Button("Go", self.onUrlClick)
+        hpanel.setCellWidth(self._url, "100%")
+        #self.setCellWidth(btn, "10%")
+        hpanel.add(self._url)
+        hpanel.add(btn)
+        self._vpanel.add(hpanel)
+        self._vpanel.setCellHeight(hpanel, '20px')
+        self._frame = Frame(url or "")
+        self._frame.setSize('100%', '100%')
+        DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
+        self._vpanel.add(self._frame)
+        self.setWidget(self._vpanel)
+
+    def onUrlClick(self, sender):
+        self._frame.setUrl(self._url.getText())
+
+
+class MainPanel(AbsolutePanel):
+
+    def __init__(self, host):
+        self.host = host
+        AbsolutePanel.__init__(self)
+
+        # menu
+        self.menu = menu.Menu(host)
+
+        # unibox
+        self.unibox_panel = UniBoxPanel(host)
+        self.unibox_panel.setVisible(False)
+
+        # contacts
+        self._contacts = HorizontalPanel()
+        self._contacts.addStyleName('globalLeftArea')
+        self.contacts_switch = Button(u'«', self._contactsSwitch)
+        self.contacts_switch.addStyleName('contactsSwitch')
+        self._contacts.add(self.contacts_switch)
+        self._contacts.add(self.host.contact_panel)
+
+        # tabs
+        self.tab_panel = base_widget.MainTabPanel(host)
+        self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
+        self.tab_panel.add(self.discuss_panel, "Discussions")
+        self.tab_panel.selectTab(0)
+
+        self.header = AbsolutePanel()
+        self.header.add(self.menu)
+        self.header.add(self.unibox_panel)
+        self.header.add(self.host.status_panel)
+        self.header.setStyleName('header')
+        self.add(self.header)
+
+        self._hpanel = HorizontalPanel()
+        self._hpanel.add(self._contacts)
+        self._hpanel.add(self.tab_panel)
+        self.add(self._hpanel)
+
+        self.setWidth("100%")
+        Window.addWindowResizeListener(self)
+
+    def _contactsSwitch(self, btn=None):
+        """ (Un)hide contacts panel """
+        if btn is None:
+            btn = self.contacts_switch
+        cpanel = self.host.contact_panel
+        cpanel.setVisible(not cpanel.getVisible())
+        btn.setText(u"«" if cpanel.getVisible() else u"»")
+        self.host.resize()
+
+    def _contactsMove(self, parent):
+        """Move the contacts container (containing the contact list and
+        the "hide/show" button) to another parent, but always as the
+        first child position (insert at index 0).
+        """
+        if self._contacts.getParent():
+            if self._contacts.getParent() == parent:
+                return
+            self._contacts.removeFromParent()
+        parent.insert(self._contacts, 0)
+
+    def onWindowResized(self, width, height):
+        _elts = doc().getElementsByClassName('gwt-TabBar')
+        if not _elts.length:
+            tab_bar_h = 0
+        else:
+            tab_bar_h = _elts.item(0).offsetHeight
+        ideal_height = Window.getClientHeight() - tab_bar_h
+        self.setHeight("%s%s" % (ideal_height, "px"))
+
+    def refresh(self):
+        """Refresh the main panel"""
+        self.unibox_panel.refresh()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/plugin_xep_0085.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,82 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# SAT plugin for Chat State Notifications Protocol (xep-0085)
+# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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 pyjamas.Timer import Timer
+
+
+# Copy of the map from sat/src/plugins/plugin_xep_0085
+TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120},
+               "inactive": {"next_state": "gone", "delay": 480},
+               "gone": {"next_state": "", "delay": 0},
+               "composing": {"next_state": "paused", "delay": 30},
+               "paused": {"next_state": "inactive", "delay": 450}
+               }
+
+
+class ChatStateMachine:
+    """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085
+    which manage a timer on the web browser and keep it synchronized with the timer that runs
+    on the backend. This is only needed to avoid calling the bridge method chatStateComposing
+    too often ; accuracy is not needed so we can ignore the delay of the communication between
+    the web browser and the backend (the timer on the web browser always starts a bit before).
+    /!\ Keep this file up to date if you modify the one in the sat plugins directory.
+    """
+    def __init__(self, host, target_s):
+
+        self.host = host
+        self.target_s = target_s
+        self.started = False
+        self.state = None
+        self.timer = None
+
+    def _onEvent(self, state):
+        """Pyjamas callback takes no extra argument so we need this trick"""
+        # Here we should check the value of the parameter "Send chat state notifications"
+        # but this costs two messages. It's even better to call chatStateComposing
+        # with a doubt, it will be checked by the back-end anyway before sending
+        # the actual notifications to the other client.
+        if state == "composing" and not self.started:
+            return
+        self.started = True
+        self.next_state = state
+        self.__onEvent(None)
+
+    def __onEvent(self, timer):
+        # print "on event %s" % self.next_state
+        state = self.next_state
+        self.next_state = ""
+        if state != self.state and state == "composing":
+            self.host.bridge.call('chatStateComposing', None, self.target_s)
+        self.state = state
+        if not self.timer is None:
+            self.timer.cancel()
+
+        if not state in TRANSITIONS:
+            return
+        if not "next_state" in TRANSITIONS[state]:
+            return
+        if not "delay" in TRANSITIONS[state]:
+            return
+        next_state = TRANSITIONS[state]["next_state"]
+        delay = TRANSITIONS[state]["delay"]
+        if next_state == "" or delay < 0:
+            return
+        self.next_state = next_state
+        # pyjamas timer in milliseconds
+        self.timer = Timer(delay * 1000, self.__onEvent)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/radiocol.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,321 @@
+#!/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.misc import DEFAULT_MUC
+from sat.core.i18n import _
+
+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
+
+
+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 == "OK":
+            # the song can still be rejected (not readable, full queue...)
+            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
+        elif result == "KO":
+            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
+            self._parent.radiocolSongRejected(_("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, player_nick, players, queue_data):
+        """
+        @param parent
+        @param referee
+        @param player_nick
+        @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 radiocolPreload(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 radiocolPlay(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 radiocolNoUpload(self):
+        self.control_panel.blockUpload()
+
+    def radiocolUploadOk(self):
+        self.control_panel.unblockUpload()
+
+    def radiocolSongRejected(self, reason):
+        Window.alert("Song rejected: %s" % reason)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/register.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,255 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+# Copyright (C) 2011, 2012  Adrien Vigneron <adrienvigneron@mailoo.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#This page manage subscription and new account creation
+
+import pyjd  # this is dummy in pyjs
+from sat.core.i18n import _
+
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.TabBar import TabBar
+from pyjamas.ui.PasswordTextBox import PasswordTextBox
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Hidden import Hidden
+from pyjamas import Window
+from pyjamas.ui.KeyboardListener import KEY_ENTER
+from pyjamas.Timer import Timer
+
+import re
+
+from constants import Const as C
+
+
+class RegisterPanel(FormPanel):
+
+    def __init__(self, callback):
+        """
+        @param callback: method to call if login successful
+        """
+        FormPanel.__init__(self)
+        self.setSize('600px', '350px')
+        self.callback = callback
+        self.setMethod(FormPanel.METHOD_POST)
+        main_panel = HorizontalPanel()
+        main_panel.setStyleName('registerPanel_main')
+        left_side = Image("media/libervia/register_left.png")
+        main_panel.add(left_side)
+
+        ##TabPanel##
+        tab_bar = TabBar()
+        tab_bar.setStyleName('registerPanel_tabs')
+        self.right_side = TabPanel(tab_bar)
+        self.right_side.setStyleName('registerPanel_right_side')
+        main_panel.add(self.right_side)
+        main_panel.setCellWidth(self.right_side, '100%')
+
+        ##Login tab##
+        login_tab = SimplePanel()
+        login_tab.setStyleName('registerPanel_content')
+        login_vpanel = VerticalPanel()
+        login_tab.setWidget(login_vpanel)
+
+        self.login_warning_msg = Label('')
+        self.login_warning_msg.setVisible(False)
+        self.login_warning_msg.setStyleName('formWarning')
+        login_vpanel.add(self.login_warning_msg)
+
+        login_label = Label('Login:')
+        self.login_box = TextBox()
+        self.login_box.setName("login")
+        self.login_box.addKeyboardListener(self)
+        login_pass_label = Label('Password:')
+        self.login_pass_box = PasswordTextBox()
+        self.login_pass_box.setName("login_password")
+        self.login_pass_box.addKeyboardListener(self)
+
+        login_vpanel.add(login_label)
+        login_vpanel.add(self.login_box)
+        login_vpanel.add(login_pass_label)
+        login_vpanel.add(self.login_pass_box)
+        login_but = Button("Log in", getattr(self, "onLogin"))
+        login_but.setStyleName('button')
+        login_but.addStyleName('red')
+        login_vpanel.add(login_but)
+
+        #The hidden submit_type field
+        self.submit_type = Hidden('submit_type')
+        login_vpanel.add(self.submit_type)
+
+        ##Register tab##
+        register_tab = SimplePanel()
+        register_tab.setStyleName('registerPanel_content')
+        register_vpanel = VerticalPanel()
+        register_tab.setWidget(register_vpanel)
+
+        self.register_warning_msg = HTML('')
+        self.register_warning_msg.setVisible(False)
+        self.register_warning_msg.setStyleName('formWarning')
+        register_vpanel.add(self.register_warning_msg)
+
+        register_login_label = Label('Login:')
+        self.register_login_box = TextBox()
+        self.register_login_box.setName("register_login")
+        self.register_login_box.addKeyboardListener(self)
+        email_label = Label('E-mail:')
+        self.email_box = TextBox()
+        self.email_box.setName("email")
+        self.email_box.addKeyboardListener(self)
+        register_pass_label = Label('Password:')
+        self.register_pass_box = PasswordTextBox()
+        self.register_pass_box.setName("register_password")
+        self.register_pass_box.addKeyboardListener(self)
+        register_vpanel.add(register_login_label)
+        register_vpanel.add(self.register_login_box)
+        register_vpanel.add(email_label)
+        register_vpanel.add(self.email_box)
+        register_vpanel.add(register_pass_label)
+        register_vpanel.add(self.register_pass_box)
+
+        register_but = Button("Register", getattr(self, "onRegister"))
+        register_but.setStyleName('button')
+        register_but.addStyleName('red')
+        register_vpanel.add(register_but)
+
+        self.right_side.add(login_tab, 'Login')
+        self.right_side.add(register_tab, 'Register')
+        self.right_side.addTabListener(self)
+        self.right_side.selectTab(1)
+        login_tab.setWidth(None)
+        register_tab.setWidth(None)
+
+        self.add(main_panel)
+        self.addFormHandler(self)
+        self.setAction('register_api/login')
+
+    def onBeforeTabSelected(self, sender, tabIndex):
+        return True
+
+    def onTabSelected(self, sender, tabIndex):
+        if tabIndex == 0:
+            self.login_box.setFocus(True)
+        elif tabIndex == 1:
+            self.register_login_box.setFocus(True)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        if keycode == KEY_ENTER:
+            # Browsers offer an auto-completion feature to any
+            # text box, but the selected value is not set when
+            # the widget looses the focus. Using a timer with
+            # any delay value > 0 would do the trick.
+            if sender == self.login_box:
+                Timer(5, lambda timer: self.login_pass_box.setFocus(True))
+            elif sender == self.login_pass_box:
+                self.onLogin(None)
+            elif sender == self.register_login_box:
+                Timer(5, lambda timer: self.email_box.setFocus(True))
+            elif sender == self.email_box:
+                Timer(5, lambda timer: self.register_pass_box.setFocus(True))
+            elif sender == self.register_pass_box:
+                self.onRegister(None)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        pass
+
+    def onLogin(self, button):
+        if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE):
+            self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -')
+            self.login_warning_msg.setVisible(True)
+        else:
+            self.submit_type.setValue('login')
+            self.submit()
+
+    def onRegister(self, button):
+        if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE):
+            self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -'))
+            self.register_warning_msg.setVisible(True)
+        elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE):
+            self.register_warning_msg.setHTML(_('Invalid email address'))
+            self.register_warning_msg.setVisible(True)
+        elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH:
+            self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH)
+            self.register_warning_msg.setVisible(True)
+        else:
+            self.register_warning_msg.setVisible(False)
+            self.submit_type.setValue('register')
+            self.submit()
+
+    def onSubmit(self, event):
+        pass
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result == "PROFILE AUTH ERROR":
+            Window.alert(_('Your login and/or password is incorrect. Please try again'))
+        elif result == "XMPP AUTH ERROR":
+            # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus
+            Window.alert(_(u'Your SàT profile has been authenticated but the associated XMPP account failed to connect. Please use another SàT frontend to set another XMPP password.'))
+        elif result == "LOGGED":
+            self.callback()
+        elif result == "SESSION_ACTIVE":
+            Window.alert(_('Session already active, this should not happen, please contact the author to fix it'))
+        elif result == "ALREADY EXISTS":
+            self.register_warning_msg.setHTML(_('This login already exists,<br>please choose another one'))
+            self.register_warning_msg.setVisible(True)
+        elif result == "INTERNAL":
+            self.register_warning_msg.setHTML(_('SERVER ERROR: something went wrong during registration process, please contact the server administrator'))
+            self.register_warning_msg.setVisible(True)
+        elif result == "REGISTRATION":
+            self.login_warning_msg.setVisible(False)
+            self.register_warning_msg.setVisible(False)
+            self.login_box.setText(self.register_login_box.getText())
+            self.login_pass_box.setText('')
+            self.register_login_box.setText('')
+            self.register_pass_box.setText('')
+            self.email_box.setText('')
+            self.right_side.selectTab(0)
+            self.login_pass_box.setFocus(True)
+            Window.alert(_('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO'))
+        else:
+            Window.alert(_('Submit error: %s' % result))
+
+
+class RegisterBox(PopupPanel):
+
+    def __init__(self, callback, *args, **kwargs):
+        PopupPanel.__init__(self, *args, **kwargs)
+        self._form = RegisterPanel(callback)
+        self.setWidget(self._form)
+
+    def onWindowResized(self, width, height):
+        super(RegisterBox, self).onWindowResized(width, height)
+        self.centerBox()
+
+    def show(self):
+        super(RegisterBox, self).show()
+        self.centerBox()
+        self._form.login_box.setFocus(True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/richtext.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,536 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat_frontends.tools import composition
+from sat.core.i18n import _
+
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Button import Button
+from pyjamas.ui.CheckBox import CheckBox
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas import Window
+from pyjamas.ui.KeyboardListener import KeyboardHandler
+from __pyjamas__ import doc
+
+from constants import Const as C
+import dialog
+import base_panels
+import list_manager
+import html_tools
+import panels
+
+
+class RichTextEditor(base_panels.BaseTextEditor, FlexTable):
+    """Panel for the rich text editor."""
+
+    def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None):
+        """
+        @param host: the SatWebFrontend instance
+        @param content: dict with at least a 'text' key
+        @param modifiedCb: method to be called when the text has been modified
+        @param afterEditCb: method to be called when the edition is done
+        @param options: list of UI options (see self.readOptions)
+        """
+        self.host = host
+        self._debug = False  # TODO: don't forget to set  it False before commit
+        self.wysiwyg = False
+        self.__readOptions(options)
+        self.style = {'main': 'richTextEditor',
+                      'title': 'richTextTitle',
+                      'toolbar': 'richTextToolbar',
+                      'textarea': 'richTextArea'}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._prepareUI()
+        base_panels.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
+
+    def __readOptions(self, options):
+        """Set the internal flags according to the given options."""
+        if options is None:
+            options = []
+        self.read_only = 'read_only' in options
+        self.update_msg = 'update_msg' in options
+        self.no_title = 'no_title' in options or self.read_only
+        self.no_command = 'no_command' in options or self.read_only
+
+    def _prepareUI(self, y_offset=0):
+        """Prepare the UI to host title panel, toolbar, text area...
+        @param y_offset: Y offset to start from (extra rows on top)"""
+        if not self.read_only:
+            self.title_offset = y_offset
+            self.toolbar_offset = self.title_offset + (0 if self.no_title else 1)
+            self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1)
+            self.command_offset = self.content_offset + 1
+        else:
+            self.title_offset = self.toolbar_offset = self.content_offset = y_offset
+            self.command_offset = self.content_offset + 1
+        FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2)
+        self.addStyleName(self.style['main'])
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        base_panels.BaseTextEditor.addEditListener(self, listener)
+        if hasattr(self, 'display'):
+            self.display.addEditListener(listener)
+
+    def refresh(self, edit=None):
+        """Refresh the UI for edition/display mode
+        @param edit: set to True to display the edition mode"""
+        if edit is None:
+            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
+
+        for widget in ['title_panel', 'command']:
+            if hasattr(self, widget):
+                getattr(self, widget).setVisible(edit)
+
+        if hasattr(self, 'toolbar'):
+            self.toolbar.setVisible(False)
+        if not hasattr(self, 'display'):
+            self.display = base_panels.HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False})  # for display mode
+            for listener in self.edit_listeners:
+                self.display.addEditListener(listener)
+        if not self.read_only and not hasattr(self, 'textarea'):
+            self.textarea = EditTextArea(self)  # for edition mode
+            self.textarea.addStyleName(self.style['textarea'])
+
+        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
+        if edit and not self.wysiwyg:
+            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
+            self.setWidget(self.content_offset, 0, self.textarea)
+        else:
+            self.setWidget(self.content_offset, 0, self.display)
+        if not edit:
+            return
+
+        if not self.no_title and not hasattr(self, 'title_panel'):
+            self.title_panel = base_panels.TitlePanel()
+            self.title_panel.addStyleName(self.style['title'])
+            self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2)
+            self.setWidget(self.title_offset, 0, self.title_panel)
+
+        if not self.no_command and not hasattr(self, 'command'):
+            self.command = HorizontalPanel()
+            self.command.addStyleName("marginAuto")
+            self.command.add(Button("Cancel", lambda: self.edit(True, True)))
+            self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False)))
+            self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2)
+            self.setWidget(self.command_offset, 0, self.command)
+
+    def setToolBar(self, syntax):
+        """This method is called asynchronously after the parameter
+        holding the rich text syntax is retrieved. It is called at
+        each call of self.edit(True) because the user may
+        have change his setting since the last time."""
+        if syntax is None or syntax not in composition.RICH_SYNTAXES.keys():
+            syntax = composition.RICH_SYNTAXES.keys()[0]
+        if hasattr(self, "toolbar") and self.toolbar.syntax == syntax:
+            self.toolbar.setVisible(True)
+            return
+        count = 0
+        for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]:
+            self.toolbar = HorizontalPanel()
+            self.toolbar.syntax = syntax
+            self.toolbar.addStyleName(self.style['toolbar'])
+            for key in composition.RICH_SYNTAXES[syntax].keys():
+                self.addToolbarButton(syntax, key)
+            self.wysiwyg_button = CheckBox(_('WYSIWYG edition'))
+            wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked())
+            self.wysiwyg_button.addClickListener(wysiywgCb)
+            self.toolbar.add(self.wysiwyg_button)
+            self.syntax_label = Label(_("Syntax: %s") % syntax)
+            self.syntax_label.addStyleName("richTextSyntaxLabel")
+            self.toolbar.add(self.syntax_label)
+            self.toolbar.setCellWidth(self.syntax_label, "100%")
+            self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2)
+            self.setWidget(self.toolbar_offset + count, 0, self.toolbar)
+            count += 1
+
+    def setWysiwyg(self, wysiwyg, init=False):
+        """Toggle the edition mode between rich content syntax and wysiwyg.
+        @param wysiwyg: boolean value
+        @param init: set to True to re-init without switching the widgets."""
+        def setWysiwyg():
+            self.wysiwyg = wysiwyg
+            try:
+                self.wysiwyg_button.setChecked(wysiwyg)
+            except TypeError:
+                pass
+            try:
+                if wysiwyg:
+                    self.syntax_label.addStyleName('transparent')
+                else:
+                    self.syntax_label.removeStyleName('transparent')
+            except TypeError:
+                pass
+            if not wysiwyg:
+                self.display.removeStyleName('richTextWysiwyg')
+
+        if init:
+            setWysiwyg()
+            return
+
+        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
+        if wysiwyg:
+            def syntaxConvertCb(text):
+                self.display.setContent({'text': text})
+                self.textarea.removeFromParent()  # XXX: force as it is not always done...
+                self.setWidget(self.content_offset, 0, self.display)
+                self.display.addStyleName('richTextWysiwyg')
+                self.display.edit(True)
+            content = self.getContent()
+            if content['text'] and content['syntax'] != C.SYNTAX_XHTML:
+                self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML)
+            else:
+                syntaxConvertCb(content['text'])
+        else:
+            syntaxConvertCb = lambda text: self.textarea.setText(text)
+            text = self.display.getContent()['text']
+            if text and self.toolbar.syntax != C.SYNTAX_XHTML:
+                self.host.bridge.call('syntaxConvert', syntaxConvertCb, text)
+            else:
+                syntaxConvertCb(text)
+            self.setWidget(self.content_offset, 0, self.textarea)
+            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
+
+        setWysiwyg()  # do it in the end because it affects self.getContent
+
+    def addToolbarButton(self, syntax, key):
+        """Add a button with the defined parameters."""
+        button = Button('<img src="%s" class="richTextIcon" />' %
+                        composition.RICH_BUTTONS[key]["icon"])
+        button.setTitle(composition.RICH_BUTTONS[key]["tip"])
+        button.addStyleName('richTextToolButton')
+        self.toolbar.add(button)
+
+        def buttonCb():
+            """Generic callback for a toolbar button."""
+            text = self.textarea.getText()
+            cursor_pos = self.textarea.getCursorPos()
+            selection_length = self.textarea.getSelectionLength()
+            data = composition.RICH_SYNTAXES[syntax][key]
+            if selection_length == 0:
+                middle_text = data[1]
+            else:
+                middle_text = text[cursor_pos:cursor_pos + selection_length]
+            self.textarea.setText(text[:cursor_pos]
+                                  + data[0]
+                                  + middle_text
+                                  + data[2]
+                                  + text[cursor_pos + selection_length:])
+            self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text))
+            self.textarea.setFocus(True)
+            self.textarea.onKeyDown()
+
+        def wysiwygCb():
+            """Callback for a toolbar button while wysiwyg mode is enabled."""
+            data = composition.COMMANDS[key]
+
+            def execCommand(command, arg):
+                self.display.setFocus(True)
+                doc().execCommand(command, False, arg.strip() if arg else '')
+            # use Window.prompt instead of dialog.PromptDialog to not loose the focus
+            prompt = lambda command, text: execCommand(command, Window.prompt(text))
+            if isinstance(data, tuple) or isinstance(data, list):
+                if data[1]:
+                    prompt(data[0], data[1])
+                else:
+                    execCommand(data[0], data[2])
+            else:
+                execCommand(data, False, '')
+            self.textarea.onKeyDown()
+
+        button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb())
+
+    def getContent(self):
+        assert(hasattr(self, 'textarea'))
+        assert(hasattr(self, 'toolbar'))
+        if self.wysiwyg:
+            content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML}
+        else:
+            content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax}
+        if hasattr(self, 'title_panel'):
+            content.update({'title': self.strproc(self.title_panel.getText())})
+        return content
+
+    def edit(self, edit=False, 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 not (edit and abort):
+            self.refresh(edit)  # not when we are asking for a confirmation
+        base_panels.BaseTextEditor.edit(self, edit, abort, sync)  # after the UI has been refreshed
+        if (edit and abort):
+            return  # self.abortEdition is called by base_panels.BaseTextEditor.edit
+        self.setWysiwyg(False, init=True)  # after base_panels.BaseTextEditor (it affects self.getContent)
+        if sync:
+            return
+        # the following must NOT be done at each UI refresh!
+        content = self._original_content
+        if edit:
+            def getParamCb(syntax):
+                # set the editable text in the current user-selected syntax
+                def syntaxConvertCb(text=None):
+                    if text is not None:
+                        # Important: this also update self._original_content
+                        content.update({'text': text})
+                    content.update({'syntax': syntax})
+                    self.textarea.setText(content['text'])
+                    if hasattr(self, 'title_panel') and 'title' in content:
+                        self.title_panel.setText(content['title'])
+                        self.title_panel.setStackVisible(0, content['title'] != '')
+                    self.setToolBar(syntax)
+                if content['text'] and content['syntax'] != syntax:
+                    self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'])
+                else:
+                    syntaxConvertCb()
+            self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION)
+        else:
+            if not self.initialized:
+                # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification
+                self.setDisplayContent()
+            self.display.edit(False)
+
+    def setDisplayContent(self):
+        """Set the content of the base_panels.HTMLTextEditor which is used for display/wysiwyg"""
+        content = self._original_content
+        text = content['text']
+        if 'title' in content and content['title']:
+            text = '<h1>%s</h1>%s' % (html_tools.html_sanitize(content['title']), content['text'])
+        self.display.setContent({'text': text})
+
+    def setFocus(self, focus):
+        self.textarea.setFocus(focus)
+
+    def abortEdition(self, content):
+        """Ask for confirmation before closing the dialog."""
+        def confirm_cb(answer):
+            if answer:
+                self.edit(False, True)
+        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message"))
+        _dialog.cancel_button.setText(_("No"))
+        _dialog.show()
+
+
+class RichMessageEditor(RichTextEditor):
+    """Use the rich text editor for sending messages with extended addressing.
+    Recipient panels are on top and data may be synchronized from/to the unibox."""
+
+    @classmethod
+    def getOrCreate(cls, host, parent=None, callback=None):
+        """Get or create the message editor associated to that host.
+        Add it to parent if parent is not None, otherwise display it
+        in a popup dialog.
+        @param host: the host
+        @param parent: parent panel (or None to display in a popup).
+        @return: the RichTextEditor instance if parent is not None,
+        otherwise a popup DialogBox containing the RichTextEditor.
+        """
+        if not hasattr(host, 'richtext'):
+            modifiedCb = lambda content: True
+
+            def afterEditCb(content):
+                if hasattr(host.richtext, 'popup'):
+                    host.richtext.popup.hide()
+                else:
+                    host.richtext.setVisible(False)
+                callback()
+            options = ['no_title']
+            style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'}
+            host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style)
+
+        def add(widget, parent):
+            if widget.getParent() is not None:
+                if widget.getParent() != parent:
+                    widget.removeFromParent()
+                    parent.add(widget)
+            else:
+                parent.add(widget)
+            widget.setVisible(True)
+            widget.initialized = False  # fake a new creation
+            widget.edit(True)
+
+        if parent is None:
+            if not hasattr(host.richtext, 'popup'):
+                host.richtext.popup = DialogBox(autoHide=False, centered=True)
+                host.richtext.popup.setHTML("Compose your message")
+                host.richtext.popup.add(host.richtext)
+            add(host.richtext, host.richtext.popup)
+            host.richtext.popup.center()
+        else:
+            add(host.richtext, parent)
+        return host.richtext.popup if parent is None else host.richtext
+
+    def _prepareUI(self, y_offset=0):
+        """Prepare the UI to host recipients panel, toolbar, text area...
+        @param y_offset: Y offset to start from (extra rows on top)"""
+        self.recipient_offset = y_offset
+        self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES)
+        RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1)
+
+    def refresh(self, edit=None):
+        """Refresh the UI between edition/display mode
+        @param edit: set to True to display the edition mode"""
+        if edit is None:
+            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
+        RichTextEditor.refresh(self, edit)
+
+        for widget in ['recipient', 'recipient_spacer']:
+            if hasattr(self, widget):
+                getattr(self, widget).setVisible(edit)
+
+        if not edit:
+            return
+
+        if not hasattr(self, 'recipient'):
+            # recipient types sub-panels are automatically added by the manager
+            self.recipient = RecipientManager(self, self.recipient_offset)
+            self.recipient.createWidgets(title_format="%s: ")
+            self.recipient_spacer = HTML('')
+            self.recipient_spacer.setStyleName('recipientSpacer')
+            self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2)
+            self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer)
+
+        if not hasattr(self, 'sync_button'):
+            self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True))
+            self.command.insert(self.sync_button, 1)
+
+    def syncToEditor(self):
+        """Synchronize from unibox."""
+        def setContent(target, data):
+            if hasattr(self, 'recipient'):
+                self.recipient.setContacts({"To": [target]} if target else {})
+            self.setContent({'text': data if data else '', 'syntax': ''})
+            self.textarea.setText(data if data else '')
+        data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None)
+        setContent(target, data)
+
+    def __syncToUniBox(self, recipients=None, emptyText=False):
+        """Synchronize to unibox if a maximum of one recipient is set.
+        @return True if the sync could be done, False otherwise"""
+        if not self.host.uni_box:
+            return
+        setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text'])
+        if not hasattr(self, 'recipient'):
+            setText()
+            return True
+        if recipients is None:
+            recipients = self.recipient.getContacts()
+        target = ""
+        # we could eventually allow more in the future
+        allowed = 1
+        for key in recipients:
+            count = len(recipients[key])
+            if count == 0:
+                continue
+            allowed -= count
+            if allowed < 0:
+                return False
+            # TODO: change this if later more then one recipients are allowed
+            target = recipients[key][0]
+        setText()
+        if target == "":
+            return True
+        if target.startswith("@"):
+            _class = panels.MicroblogPanel
+            target = None if target == "@@" else target[1:]
+        else:
+            _class = panels.ChatPanel
+        self.host.getOrCreateLiberviaWidget(_class, target)
+        return True
+
+    def syncFromEditor(self, content):
+        """Synchronize to unibox and close the dialog afterward. Display
+        a message and leave the dialog open if the sync was not possible."""
+        if self.__syncToUniBox():
+            self._afterEditCb(content)
+            return
+        dialog.InfoDialog("Too many recipients",
+                   "A message with more than one direct recipient (To)," +
+                   " or with any special recipient (Cc or Bcc), could not be" +
+                   " stored in the quick box.\n\nPlease finish your composing" +
+                   " in the rich text editor, and send your message directly" +
+                   " from here.", Width="400px").center()
+
+    def edit(self, edit=True, abort=False, sync=False):
+        if not edit and not abort and not sync:  # force sending message even when the text has not been modified
+            if not self.__sendMessage():  # message has not been sent (missing information), do nothing
+                return
+        RichTextEditor.edit(self, edit, abort, sync)
+
+    def __sendMessage(self):
+        """Send the message."""
+        recipients = self.recipient.getContacts()
+        targets = []
+        for addr in recipients:
+            for recipient in recipients[addr]:
+                if recipient.startswith("@"):
+                    targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr))
+                else:
+                    targets.append(("chat", recipient, addr))
+        # check that we actually have a message target and data
+        content = self.getContent()
+        if content['text'] == "" or len(targets) == 0:
+            dialog.InfoDialog("Missing information",
+                       "Some information are missing and the message hasn't been sent.", Width="400px").center()
+            return None
+        self.__syncToUniBox(recipients, emptyText=True)
+        extra = {'content_rich': content['text']}
+        if hasattr(self, 'title_panel'):
+            extra.update({'title': content['title']})
+        self.host.send(targets, content['text'], extra=extra)
+        return True
+
+
+class RecipientManager(list_manager.ListManager):
+    """A manager for sub-panels to set the recipients for each recipient type."""
+
+    def __init__(self, parent, y_offset=0):
+        # TODO: be sure we also display empty groups and disconnected contacts + their groups
+        # store the full list of potential recipients (groups and contacts)
+        list_ = []
+        list_.append("@@")
+        list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups())
+        list_.extend(contact for contact in parent.host.contact_panel.getContacts())
+        list_manager.ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset})
+
+        self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES,
+                                    hide=lambda sender, key: self.__children[key]["panel"].isVisible(),
+                                    callback=self.setContactPanelVisible)
+
+
+class EditTextArea(TextArea, KeyboardHandler):
+    def __init__(self, _parent):
+        TextArea.__init__(self)
+        self._parent = _parent
+        KeyboardHandler.__init__(self)
+        self.addKeyboardListener(self)
+
+    def onKeyDown(self, sender=None, keycode=None, modifiers=None):
+        for listener in self._parent.edit_listeners:
+            listener(self, keycode)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/xmlui.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,418 @@
+#!/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 xmlui
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.Grid import Grid
+from pyjamas.ui.Label import Label
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.PasswordTextBox import PasswordTextBox
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.CheckBox import CheckBox
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HTML import HTML
+
+import nativedom
+
+
+class EmptyWidget(xmlui.EmptyWidget, Label):
+
+    def __init__(self, parent):
+        Label.__init__(self, '')
+
+
+class TextWidget(xmlui.TextWidget, Label):
+
+    def __init__(self, parent, value):
+        Label.__init__(self, value)
+
+
+class LabelWidget(xmlui.LabelWidget, TextWidget):
+
+    def __init__(self, parent, value):
+        TextWidget.__init__(self, parent, value + ": ")
+
+
+class JidWidget(xmlui.JidWidget, TextWidget):
+
+    def __init__(self, parent, value):
+        TextWidget.__init__(self, parent, value)
+
+
+class DividerWidget(xmlui.DividerWidget, HTML):
+
+    def __init__(self, parent, style='line'):
+        """Add a divider
+
+        @param parent
+        @param style (string): one of:
+                               - line: a simple line
+                               - dot: a line of dots
+                               - dash: a line of dashes
+                               - plain: a full thick line
+                               - blank: a blank line/space
+        """
+        HTML.__init__(self, "<hr/>")
+        self.addStyleName(style)
+
+
+class StringWidget(xmlui.StringWidget, TextBox):
+
+    def __init__(self, parent, value):
+        TextBox.__init__(self)
+        self.setText(value)
+
+    def _xmluiSetValue(self, value):
+        self.setText(value)
+
+    def _xmluiGetValue(self):
+        return self.getText()
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+
+class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox):
+
+    def __init__(self, parent, value):
+        PasswordTextBox.__init__(self)
+        self.setText(value)
+
+    def _xmluiSetValue(self, value):
+        self.setText(value)
+
+    def _xmluiGetValue(self):
+        return self.getText()
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+
+class TextBoxWidget(xmlui.TextBoxWidget, TextArea):
+
+    def __init__(self, parent, value):
+        TextArea.__init__(self)
+        self.setText(value)
+
+    def _xmluiSetValue(self, value):
+        self.setText(value)
+
+    def _xmluiGetValue(self):
+        return self.getText()
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+
+class BoolWidget(xmlui.BoolWidget, CheckBox):
+
+    def __init__(self, parent, state):
+        CheckBox.__init__(self)
+        self.setChecked(state)
+
+    def _xmluiSetValue(self, value):
+        self.setChecked(value == "true")
+
+    def _xmluiGetValue(self):
+        return "true" if self.isChecked() else "false"
+
+    def _xmluiOnChange(self, callback):
+        self.addClickListener(callback)
+
+
+class ButtonWidget(xmlui.ButtonWidget, Button):
+
+    def __init__(self, parent, value, click_callback):
+        Button.__init__(self, value, click_callback)
+
+    def _xmluiOnClick(self, callback):
+        self.addClickListener(callback)
+
+
+class ListWidget(xmlui.ListWidget, ListBox):
+
+    def __init__(self, parent, options, selected, flags):
+        ListBox.__init__(self)
+        multi_selection = 'single' not in flags
+        self.setMultipleSelect(multi_selection)
+        if multi_selection:
+            self.setVisibleItemCount(5)
+        for option in options:
+            self.addItem(option[1])
+        self._xmlui_attr_map = {label: value for value, label in options}
+        self._xmluiSelectValues(selected)
+
+    def _xmluiSelectValue(self, value):
+        """Select a value checking its item"""
+        try:
+            label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0]
+        except IndexError:
+            log.warning("Can't find value [%s] to select" % value)
+            return
+        self.selectItem(label)
+
+    def _xmluiSelectValues(self, values):
+        """Select multiple values, ignore the items"""
+        self.setValueSelection(values)
+
+    def _xmluiGetSelectedValues(self):
+        ret = []
+        for label in self.getSelectedItemText():
+            ret.append(self._xmlui_attr_map[label])
+        return ret
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+    def _xmluiAddValues(self, values, select=True):
+        selected = self._xmluiGetSelectedValues()
+        for value in values:
+            if value not in self._xmlui_attr_map.values():
+                self.addItem(value)
+                self._xmlui_attr_map[value] = value
+            if value not in selected:
+                selected.append(value)
+        self._xmluiSelectValues(selected)
+
+
+class LiberviaContainer(object):
+
+    def _xmluiAppend(self, widget):
+        self.append(widget)
+
+
+class AdvancedListContainer(xmlui.AdvancedListContainer, Grid):
+
+    def __init__(self, parent, columns, selectable='no'):
+        Grid.__init__(self, 0, columns)
+        self.columns = columns
+        self.row = -1
+        self.col = 0
+        self._xmlui_rows_idx = []
+        self._xmlui_selectable = selectable != 'no'
+        self._xmlui_selected_row = None
+        self.addTableListener(self)
+        if self._xmlui_selectable:
+            self.addStyleName('AdvancedListSelectable')
+
+    def onCellClicked(self, grid, row, col):
+        if not self._xmlui_selectable:
+            return
+        self._xmlui_selected_row = row
+        try:
+            self._xmlui_select_cb(self)
+        except AttributeError:
+            log.warning("no select callback set")
+
+    def _xmluiAppend(self, widget):
+        self.setWidget(self.row, self.col, widget)
+        self.col += 1
+
+    def _xmluiAddRow(self, idx):
+        self.row += 1
+        self.col = 0
+        self._xmlui_rows_idx.insert(self.row, idx)
+        self.resizeRows(self.row + 1)
+
+    def _xmluiGetSelectedWidgets(self):
+        return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)]
+
+    def _xmluiGetSelectedIndex(self):
+        try:
+            return self._xmlui_rows_idx[self._xmlui_selected_row]
+        except TypeError:
+            return None
+
+    def _xmluiOnSelect(self, callback):
+        self._xmlui_select_cb = callback
+
+
+class PairsContainer(xmlui.PairsContainer, Grid):
+
+    def __init__(self, parent):
+        Grid.__init__(self, 0, 0)
+        self.row = 0
+        self.col = 0
+
+    def _xmluiAppend(self, widget):
+        if self.col == 0:
+            self.resize(self.row + 1, 2)
+        self.setWidget(self.row, self.col, widget)
+        self.col += 1
+        if self.col == 2:
+            self.row += 1
+            self.col = 0
+
+
+class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel):
+
+    def __init__(self, parent):
+        TabPanel.__init__(self)
+        self.setStyleName('liberviaTabPanel')
+
+    def _xmluiAddTab(self, label):
+        tab_panel = VerticalContainer(self)
+        self.add(tab_panel, label)
+        if len(self.getChildren()) == 1:
+            self.selectTab(0)
+        return tab_panel
+
+
+class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel):
+    __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel)
+
+    def __init__(self, parent):
+        VerticalPanel.__init__(self)
+
+
+class WidgetFactory(object):
+    # XXX: __getattr__ doesn't work here for an unknown reason
+
+    def createVerticalContainer(self, *args, **kwargs):
+        instance = VerticalContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createPairsContainer(self, *args, **kwargs):
+        instance = PairsContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createTabsContainer(self, *args, **kwargs):
+        instance = TabsContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createAdvancedListContainer(self, *args, **kwargs):
+        instance = AdvancedListContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createEmptyWidget(self, *args, **kwargs):
+        instance = EmptyWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createTextWidget(self, *args, **kwargs):
+        instance = TextWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createLabelWidget(self, *args, **kwargs):
+        instance = LabelWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createJidWidget(self, *args, **kwargs):
+        instance = JidWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createDividerWidget(self, *args, **kwargs):
+        instance = DividerWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createStringWidget(self, *args, **kwargs):
+        instance = StringWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createPasswordWidget(self, *args, **kwargs):
+        instance = PasswordWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createTextBoxWidget(self, *args, **kwargs):
+        instance = TextBoxWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createBoolWidget(self, *args, **kwargs):
+        instance = BoolWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createButtonWidget(self, *args, **kwargs):
+        instance = ButtonWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createListWidget(self, *args, **kwargs):
+        instance = ListWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+
+    # def __getattr__(self, attr):
+    #     if attr.startswith("create"):
+    #         cls = globals()[attr[6:]]
+    #         cls._xmlui_main = self._xmlui_main
+    #         return cls
+
+
+class XMLUI(xmlui.XMLUI, VerticalPanel):
+    widget_factory = WidgetFactory()
+
+    def __init__(self, host, xml_data, title=None, flags=None):
+        self.widget_factory._xmlui_main = self
+        self.dom = nativedom.NativeDOM()
+        dom_parse = lambda xml_data: self.dom.parseString(xml_data)
+        VerticalPanel.__init__(self)
+        self.setSize('100%', '100%')
+        xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def _xmluiClose(self):
+        if self.close_cb:
+            self.close_cb()
+        else:
+            log.warning("no close method defined")
+
+    def _xmluiLaunchAction(self, action_id, data):
+        self.host.launchAction(action_id, data)
+
+    def _xmluiSetParam(self, name, value, category):
+        self.host.bridge.call('setParam', None, name, value, category)
+
+    def constructUI(self, xml_data):
+        super(XMLUI, self).constructUI(xml_data)
+        self.add(self.main_cont)
+        self.setCellHeight(self.main_cont, '100%')
+        if self.type == 'form':
+            hpanel = HorizontalPanel()
+            hpanel.setStyleName('marginAuto')
+            hpanel.add(Button('Submit', self.onFormSubmitted))
+            if not 'NO_CANCEL' in self.flags:
+                hpanel.add(Button('Cancel', self.onFormCancelled))
+            self.add(hpanel)
+        elif self.type == 'param':
+            assert(isinstance(self.children[0][0], TabPanel))
+            hpanel = HorizontalPanel()
+            hpanel.add(Button('Save', self.onSaveParams))
+            hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose()))
+            self.add(hpanel)
--- a/src/browser/xmlui.py	Mon Jun 09 20:37:28 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,418 +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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat_frontends.tools import xmlui
-
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.Grid import Grid
-from pyjamas.ui.Label import Label
-from pyjamas.ui.TextBox import TextBox
-from pyjamas.ui.PasswordTextBox import PasswordTextBox
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.CheckBox import CheckBox
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HTML import HTML
-
-from nativedom import NativeDOM
-
-
-class EmptyWidget(xmlui.EmptyWidget, Label):
-
-    def __init__(self, parent):
-        Label.__init__(self, '')
-
-
-class TextWidget(xmlui.TextWidget, Label):
-
-    def __init__(self, parent, value):
-        Label.__init__(self, value)
-
-
-class LabelWidget(xmlui.LabelWidget, TextWidget):
-
-    def __init__(self, parent, value):
-        TextWidget.__init__(self, parent, value + ": ")
-
-
-class JidWidget(xmlui.JidWidget, TextWidget):
-
-    def __init__(self, parent, value):
-        TextWidget.__init__(self, parent, value)
-
-
-class DividerWidget(xmlui.DividerWidget, HTML):
-
-    def __init__(self, parent, style='line'):
-        """Add a divider
-
-        @param parent
-        @param style (string): one of:
-                               - line: a simple line
-                               - dot: a line of dots
-                               - dash: a line of dashes
-                               - plain: a full thick line
-                               - blank: a blank line/space
-        """
-        HTML.__init__(self, "<hr/>")
-        self.addStyleName(style)
-
-
-class StringWidget(xmlui.StringWidget, TextBox):
-
-    def __init__(self, parent, value):
-        TextBox.__init__(self)
-        self.setText(value)
-
-    def _xmluiSetValue(self, value):
-        self.setText(value)
-
-    def _xmluiGetValue(self):
-        return self.getText()
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-
-class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox):
-
-    def __init__(self, parent, value):
-        PasswordTextBox.__init__(self)
-        self.setText(value)
-
-    def _xmluiSetValue(self, value):
-        self.setText(value)
-
-    def _xmluiGetValue(self):
-        return self.getText()
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-
-class TextBoxWidget(xmlui.TextBoxWidget, TextArea):
-
-    def __init__(self, parent, value):
-        TextArea.__init__(self)
-        self.setText(value)
-
-    def _xmluiSetValue(self, value):
-        self.setText(value)
-
-    def _xmluiGetValue(self):
-        return self.getText()
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-
-class BoolWidget(xmlui.BoolWidget, CheckBox):
-
-    def __init__(self, parent, state):
-        CheckBox.__init__(self)
-        self.setChecked(state)
-
-    def _xmluiSetValue(self, value):
-        self.setChecked(value == "true")
-
-    def _xmluiGetValue(self):
-        return "true" if self.isChecked() else "false"
-
-    def _xmluiOnChange(self, callback):
-        self.addClickListener(callback)
-
-
-class ButtonWidget(xmlui.ButtonWidget, Button):
-
-    def __init__(self, parent, value, click_callback):
-        Button.__init__(self, value, click_callback)
-
-    def _xmluiOnClick(self, callback):
-        self.addClickListener(callback)
-
-
-class ListWidget(xmlui.ListWidget, ListBox):
-
-    def __init__(self, parent, options, selected, flags):
-        ListBox.__init__(self)
-        multi_selection = 'single' not in flags
-        self.setMultipleSelect(multi_selection)
-        if multi_selection:
-            self.setVisibleItemCount(5)
-        for option in options:
-            self.addItem(option[1])
-        self._xmlui_attr_map = {label: value for value, label in options}
-        self._xmluiSelectValues(selected)
-
-    def _xmluiSelectValue(self, value):
-        """Select a value checking its item"""
-        try:
-            label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0]
-        except IndexError:
-            log.warning("Can't find value [%s] to select" % value)
-            return
-        self.selectItem(label)
-
-    def _xmluiSelectValues(self, values):
-        """Select multiple values, ignore the items"""
-        self.setValueSelection(values)
-
-    def _xmluiGetSelectedValues(self):
-        ret = []
-        for label in self.getSelectedItemText():
-            ret.append(self._xmlui_attr_map[label])
-        return ret
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-    def _xmluiAddValues(self, values, select=True):
-        selected = self._xmluiGetSelectedValues()
-        for value in values:
-            if value not in self._xmlui_attr_map.values():
-                self.addItem(value)
-                self._xmlui_attr_map[value] = value
-            if value not in selected:
-                selected.append(value)
-        self._xmluiSelectValues(selected)
-
-
-class LiberviaContainer(object):
-
-    def _xmluiAppend(self, widget):
-        self.append(widget)
-
-
-class AdvancedListContainer(xmlui.AdvancedListContainer, Grid):
-
-    def __init__(self, parent, columns, selectable='no'):
-        Grid.__init__(self, 0, columns)
-        self.columns = columns
-        self.row = -1
-        self.col = 0
-        self._xmlui_rows_idx = []
-        self._xmlui_selectable = selectable != 'no'
-        self._xmlui_selected_row = None
-        self.addTableListener(self)
-        if self._xmlui_selectable:
-            self.addStyleName('AdvancedListSelectable')
-
-    def onCellClicked(self, grid, row, col):
-        if not self._xmlui_selectable:
-            return
-        self._xmlui_selected_row = row
-        try:
-            self._xmlui_select_cb(self)
-        except AttributeError:
-            log.warning("no select callback set")
-
-    def _xmluiAppend(self, widget):
-        self.setWidget(self.row, self.col, widget)
-        self.col += 1
-
-    def _xmluiAddRow(self, idx):
-        self.row += 1
-        self.col = 0
-        self._xmlui_rows_idx.insert(self.row, idx)
-        self.resizeRows(self.row + 1)
-
-    def _xmluiGetSelectedWidgets(self):
-        return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)]
-
-    def _xmluiGetSelectedIndex(self):
-        try:
-            return self._xmlui_rows_idx[self._xmlui_selected_row]
-        except TypeError:
-            return None
-
-    def _xmluiOnSelect(self, callback):
-        self._xmlui_select_cb = callback
-
-
-class PairsContainer(xmlui.PairsContainer, Grid):
-
-    def __init__(self, parent):
-        Grid.__init__(self, 0, 0)
-        self.row = 0
-        self.col = 0
-
-    def _xmluiAppend(self, widget):
-        if self.col == 0:
-            self.resize(self.row + 1, 2)
-        self.setWidget(self.row, self.col, widget)
-        self.col += 1
-        if self.col == 2:
-            self.row += 1
-            self.col = 0
-
-
-class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel):
-
-    def __init__(self, parent):
-        TabPanel.__init__(self)
-        self.setStyleName('liberviaTabPanel')
-
-    def _xmluiAddTab(self, label):
-        tab_panel = VerticalContainer(self)
-        self.add(tab_panel, label)
-        if len(self.getChildren()) == 1:
-            self.selectTab(0)
-        return tab_panel
-
-
-class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel):
-    __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel)
-
-    def __init__(self, parent):
-        VerticalPanel.__init__(self)
-
-
-class WidgetFactory(object):
-    # XXX: __getattr__ doesn't work here for an unknown reason
-
-    def createVerticalContainer(self, *args, **kwargs):
-        instance = VerticalContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createPairsContainer(self, *args, **kwargs):
-        instance = PairsContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createTabsContainer(self, *args, **kwargs):
-        instance = TabsContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createAdvancedListContainer(self, *args, **kwargs):
-        instance = AdvancedListContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createEmptyWidget(self, *args, **kwargs):
-        instance = EmptyWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createTextWidget(self, *args, **kwargs):
-        instance = TextWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createLabelWidget(self, *args, **kwargs):
-        instance = LabelWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createJidWidget(self, *args, **kwargs):
-        instance = JidWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createDividerWidget(self, *args, **kwargs):
-        instance = DividerWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createStringWidget(self, *args, **kwargs):
-        instance = StringWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createPasswordWidget(self, *args, **kwargs):
-        instance = PasswordWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createTextBoxWidget(self, *args, **kwargs):
-        instance = TextBoxWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createBoolWidget(self, *args, **kwargs):
-        instance = BoolWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createButtonWidget(self, *args, **kwargs):
-        instance = ButtonWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createListWidget(self, *args, **kwargs):
-        instance = ListWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-
-    # def __getattr__(self, attr):
-    #     if attr.startswith("create"):
-    #         cls = globals()[attr[6:]]
-    #         cls._xmlui_main = self._xmlui_main
-    #         return cls
-
-
-class XMLUI(xmlui.XMLUI, VerticalPanel):
-    widget_factory = WidgetFactory()
-
-    def __init__(self, host, xml_data, title=None, flags=None):
-        self.widget_factory._xmlui_main = self
-        self.dom = NativeDOM()
-        dom_parse = lambda xml_data: self.dom.parseString(xml_data)
-        VerticalPanel.__init__(self)
-        self.setSize('100%', '100%')
-        xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse)
-
-    def setCloseCb(self, close_cb):
-        self.close_cb = close_cb
-
-    def _xmluiClose(self):
-        if self.close_cb:
-            self.close_cb()
-        else:
-            log.warning("no close method defined")
-
-    def _xmluiLaunchAction(self, action_id, data):
-        self.host.launchAction(action_id, data)
-
-    def _xmluiSetParam(self, name, value, category):
-        self.host.bridge.call('setParam', None, name, value, category)
-
-    def constructUI(self, xml_data):
-        super(XMLUI, self).constructUI(xml_data)
-        self.add(self.main_cont)
-        self.setCellHeight(self.main_cont, '100%')
-        if self.type == 'form':
-            hpanel = HorizontalPanel()
-            hpanel.setStyleName('marginAuto')
-            hpanel.add(Button('Submit', self.onFormSubmitted))
-            if not 'NO_CANCEL' in self.flags:
-                hpanel.add(Button('Cancel', self.onFormCancelled))
-            self.add(hpanel)
-        elif self.type == 'param':
-            assert(isinstance(self.children[0][0], TabPanel))
-            hpanel = HorizontalPanel()
-            hpanel.add(Button('Save', self.onSaveParams))
-            hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose()))
-            self.add(hpanel)