changeset 648:6d3142b782c3 frontends_multi_profiles

browser_side: classes reorganisation: - moved widgets in dedicated modules (base, contact, editor, libervia) and a widget module for single classes - same thing for panels (base, main, contact) - libervia_widget mix main panels and widget and drag n drop for technical reasons (see comments) - renamed WebPanel to WebWidget
author Goffi <goffi@goffi.org>
date Thu, 26 Feb 2015 18:10:54 +0100
parents e0021d571eef
children ccf95ec87005
files src/browser/libervia_main.py src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_panel.py src/browser/sat_browser/base_panels.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/blog.py src/browser/sat_browser/chat.py src/browser/sat_browser/contact_group.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/contact_panel.py src/browser/sat_browser/contact_widget.py src/browser/sat_browser/dialog.py src/browser/sat_browser/editor_widget.py src/browser/sat_browser/file_tools.py src/browser/sat_browser/libervia_widget.py src/browser/sat_browser/list_manager.py src/browser/sat_browser/main_panel.py src/browser/sat_browser/menu.py src/browser/sat_browser/panels.py src/browser/sat_browser/richtext.py src/browser/sat_browser/widget.py
diffstat 21 files changed, 2450 insertions(+), 2265 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/libervia_main.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/libervia_main.py	Thu Feb 26 18:10:54 2015 +0100
@@ -42,13 +42,14 @@
 from sat_browser import json
 from sat_browser import register
 from sat_browser.contact_list import ContactList
-from sat_browser import base_widget
-from sat_browser import panels
+from sat_browser import widget
+from sat_browser import main_panel
 from sat_browser import blog
 from sat_browser import dialog
 from sat_browser import xmlui
 from sat_browser import html_tools
 from sat_browser import notification
+from sat_browser import libervia_widget
 
 from sat_browser.constants import Const as C
 import os.path
@@ -69,7 +70,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 base_widget.WidgetsPanel, or replaced to the expected
+# and added to new libervia_widget.WidgetsPanel, or replaced to the expected
 # position if the previous and the new parent are the same.
 REUSE_EXISTING_LIBERVIA_WIDGETS = True
 
@@ -81,7 +82,7 @@
         QuickApp.__init__(self, json.BridgeCall)
         self.uni_box = None # FIXME: to be removed
         self.status_panel = HTML('<br />')
-        self.panel = panels.MainPanel(self)
+        self.panel = main_panel.MainPanel(self)
         self.tab_panel = self.panel.tab_panel
         self.tab_panel.addTabListener(self)
         self._register_box = None
@@ -156,15 +157,15 @@
 
     def getSelected(self):
         wid = self.tab_panel.getCurrentPanel()
-        if not isinstance(wid, base_widget.WidgetsPanel):
-            log.error("Tab widget is not a base_widget.WidgetsPanel, can't get selected widget")
+        if not isinstance(wid, libervia_widget.WidgetsPanel):
+            log.error("Tab widget is not a WidgetsPanel, can't get selected widget")
             return None
         return wid.selected
 
     def setSelected(self, widget):
         """Define the selected widget"""
         widgets_panel = self.tab_panel.getCurrentPanel()
-        if not isinstance(widgets_panel, base_widget.WidgetsPanel):
+        if not isinstance(widgets_panel, libervia_widget.WidgetsPanel):
             return
 
         selected = widgets_panel.selected
@@ -194,7 +195,8 @@
         return True
 
     def onTabSelected(self, sender, tab_index):
-        selected = self.getSelected()
+        pass
+        # selected = self.getSelected()
         # FIXME:
         # for callback in self._selected_listeners:
         #     callback(selected)
@@ -306,7 +308,7 @@
 
         # display the real presence status panel
         self.panel.header.remove(self.status_panel)
-        self.status_panel = panels.PresenceStatusPanel(self)
+        self.status_panel = main_panel.PresenceStatusPanel(self)
         self.panel.header.add(self.status_panel)
 
         self.bridge_signals.call('getSignals', self.bridge_signals.signalHandler)
@@ -329,7 +331,7 @@
         self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg.
         microblog_widget = self.displayWidget(blog.MicroblogPanel, ())
         self.setSelected(microblog_widget)
-        # self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
+        # self.discuss_panel.addWidget(panel.MicroblogPanel(self, []))
 
         # # get cached params and refresh the display
         # def param_cb(cat, name, count):
@@ -344,11 +346,11 @@
     def profilePlugged(self, dummy):
         QuickApp.profilePlugged(self, dummy)
         # we fill the panels already here
-        for widget in self.widgets.getWidgets(blog.MicroblogPanel):
-            if widget.accept_all():
-                self.bridge.getMassiveLastMblogs('ALL', [], 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert)
+        for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+            if wid.accept_all():
+                self.bridge.getMassiveLastMblogs('ALL', [], 10, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
             else:
-                self.bridge.getMassiveLastMblogs('GROUP', widget.accepted_groups, 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert)
+                self.bridge.getMassiveLastMblogs('GROUP', wid.accepted_groups, 10, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
 
         #we ask for our own microblogs:
         self.bridge.getMassiveLastMblogs('JID', [unicode(self.whoami.bare)], 10, profile=C.PROF_KEY_NONE, callback=self._ownBlogsFills)
@@ -363,9 +365,9 @@
         self.panel.addContactList(contact_list)
         return contact_list
 
-    def newWidget(self, widget):
-        log.debug("newWidget: {}".format(widget))
-        self.addWidget(widget)
+    def newWidget(self, wid):
+        log.debug("newWidget: {}".format(wid))
+        self.addWidget(wid)
 
     def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
         if type_ == C.MESS_TYPE_HEADLINE:
@@ -418,7 +420,7 @@
             # TODO: use the bare instead of node when all blogs can be retrieved
             node = jid.JID(data['public_blog']).node
             # FIXME: "/blog/{}" won't work with unicode nodes
-            self.displayWidget(panels.WebPanel, "/blog/{}".format(node), show_url=False, new_tab="{}'s blog".format(unicode(node)))
+            self.displayWidget(widget.WebWidget, "/blog/{}".format(node), show_url=False, new_tab="{}'s blog".format(unicode(node)))
         else:
             dialog.InfoDialog("Error",
                               "Unmanaged action result", Width="400px").center()
@@ -464,8 +466,8 @@
 
         if len(self.mblog_cache) > MAX_MBLOG_CACHE:
             del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
-        for widget in self.widgets.getWidgets(blog.MicroblogPanel):
-            self.FillMicroblogPanel(widget)
+        for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+            self.FillMicroblogPanel(wid)
 
         # FIXME
         self.initialised = True  # initialisation phase is finished here
@@ -485,7 +487,7 @@
         # self.bridge.call('getWaitingSub', self._getWaitingSubCb)
         # #we fill the panels already here
         # for lib_wid in self.libervia_widgets:
-        #     if isinstance(lib_wid, panels.MicroblogPanel):
+        #     if isinstance(lib_wid, panel.MicroblogPanel):
         #         if lib_wid.accept_all():
         #             self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10)
         #         else:
@@ -516,8 +518,8 @@
                 _groups = None
             mblog_entry = blog.MicroblogItem(data)
 
-            for widget in self.widgets.getWidgets(blog.MicroblogPanel):
-                widget.addEntryIfAccepted(sender, _groups, mblog_entry)
+            for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+                wid.addEntryIfAccepted(sender, _groups, mblog_entry)
 
             if sender == self.whoami.bare:
                 found = False
@@ -534,8 +536,8 @@
                     if len(self.mblog_cache) > MAX_MBLOG_CACHE:
                         del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
         elif event_type == 'MICROBLOG_DELETE':
-            for widget in self.widgets.getWidgets(blog.MicroblogPanel):
-                widget.removeEntry(data['type'], data['id'])
+            for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+                wid.removeEntry(data['type'], data['id'])
             log.debug("%s %s %s" % (self.whoami.bare, sender, data['type']))
 
             if sender == self.whoami.bare and data['type'] == 'main_item':
@@ -622,7 +624,7 @@
         except quick_widgets.WidgetAlreadyExistsError:
             kwargs['on_existing_widget'] = C.WIDGET_KEEP
             wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
-            widgets_panel = wid.getParent(base_widget.WidgetsPanel, expect=False)
+            widgets_panel = wid.getParent(libervia_widget.WidgetsPanel, expect=False)
             if widgets_panel is None:
                 # The widget exists but is hidden
                 self.addWidget(wid)
@@ -675,18 +677,18 @@
     #     """Get the MUC widget for the given target.
 
     #     @param target (jid.JID): BARE jid of the MUC
-    #     @return: panels.ChatPanel instance or None
+    #     @return: panel.ChatPanel instance or None
     #     """
     #     entity = {'item': target, 'type_': 'group'}
     #     if target.full() in self.room_list or target in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
-    #         return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False)
+    #         return self.getLiberviaWidget(panel.ChatPanel, entity, ignoreOtherTabs=False)
     #     return None
 
     # def getOrCreateRoomWidget(self, target):
     #     """Get the MUC widget for the given target, create it if necessary.
 
     #     @param target (jid.JID): BARE jid of the MUC
-    #     @return: panels.ChatPanel instance
+    #     @return: panel.ChatPanel instance
     #     """
     #     lib_wid = self.getRoomWidget(target)
     #     if lib_wid:
@@ -707,7 +709,7 @@
 
     #     self.room_list.append(target)
     #     entity = {'item': target, 'type_': 'group'}
-    #     return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name)
+    #     return self.getOrCreateLiberviaWidget(panel.ChatPanel, entity, new_tab=tab_name)
 
     # def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra):
     #     from_jid = jid.JID(from_jid_s)
@@ -720,7 +722,7 @@
 
     # def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
     #     other = to_jid if from_jid.bare == self.whoami.bare else from_jid
-    #     lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False)
+    #     lib_wid = self.getLiberviaWidget(panel.ChatPanel, {'item': other}, ignoreOtherTabs=False)
     #     self.displayNotification(from_jid, msg)
     #     if msg_type == 'headline' and from_jid.full() == self._defaultDomain:
     #         try:
@@ -748,7 +750,7 @@
     # def _presenceUpdateCb(self, entity, show, priority, statuses):
     #     entity_jid = jid.JID(entity)
     #     if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
-    #         assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
+    #         assert(isinstance(self.status_panel, main_panel.PresenceStatusPanel))
     #         self.status_panel.setPresence(show)  # pylint: disable=E1103
     #         if statuses:
     #             self.status_panel.setStatus(statuses.values()[0])  # pylint: disable=E1103
@@ -881,11 +883,11 @@
     #     """
     #     if from_jid_s == '@ALL@':
     #         for lib_wid in self.libervia_widgets:
-    #             if isinstance(lib_wid, panels.ChatPanel):
+    #             if isinstance(lib_wid, panel.ChatPanel):
     #                 lib_wid.setState(state, nick=C.ALL_OCCUPANTS)
     #         return
     #     from_jid = jid.JID(from_jid_s)
-    #     lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False)
+    #     lib_wid = self.getLiberviaWidget(panel.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False)
     #     lib_wid.setState(state, nick=from_jid.resource)
 
     def _askConfirmation(self, confirmation_id, confirmation_type, data):
@@ -945,7 +947,7 @@
     #         elif type_ == "COMMENT":
     #             self.bridge.call("sendMblogComment", None, entities, text, extra)
     #         elif type_ == "STATUS":
-    #             assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
+    #             assert(isinstance(self.status_panel, main_panel.PresenceStatusPanel))
     #             self.bridge.call('setStatus', None, self.status_panel.presence, text)  # pylint: disable=E1103
     #         elif type_ in ("groupchat", "chat"):
     #             addresses.append((addr, entities))
@@ -970,7 +972,7 @@
         @msg: message to be displayed
         """
         if not hasattr(self, "warning_popup"):
-            self.warning_popup = panels.WarningPopup()
+            self.warning_popup = main_panel.WarningPopup()
         self.warning_popup.showWarning(type_, msg)
 
     def showDialog(self, message, title="", type_="info", answer_cb=None, answer_data=None):
--- a/src/browser/sat_browser/base_menu.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/base_menu.py	Thu Feb 26 18:10:54 2015 +0100
@@ -24,7 +24,6 @@
 by base_widget.py, and the import sequence caused a JS runtime error."""
 
 
-import pyjd  # this is dummy in pyjs
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/base_panel.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat.core.i18n import _
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.StackPanel import StackPanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
+from pyjamas import DOM
+
+
+### Menus ###
+
+
+class PopupMenuPanel(PopupPanel):
+    """This implementation of a popup menu (context menu) allow you to assign
+    two special methods which are common to all the items, in order to hide
+    certain items and also easily define their callbacks. The menu can be
+    bound to any of the mouse button (left, middle, right).
+    """
+    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
+        """
+        @param entries: a dict of dicts, where each sub-dict is representing
+        one menu item: the sub-dict key can be used as the item text and
+        description, but optional "title" and "desc" entries would be used
+        if they exists. The sub-dicts may be extended later to do
+        more complicated stuff or overwrite the common methods.
+        @param hide: function  with 2 args: widget, key as string and
+        returns True if that item should be hidden from the context menu.
+        @param callback: function with 2 args: sender, key as string
+        @param vertical: True or False, to set the direction
+        @param item_style: alternative CSS class for the menu items
+        @param menu_style: supplementary CSS class for the sender widget
+        """
+        PopupPanel.__init__(self, autoHide=True, **kwargs)
+        self._entries = entries
+        self._hide = hide
+        self._callback = callback
+        self.vertical = vertical
+        self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._senders = {}
+
+    def _show(self, sender):
+        """Popup the menu relative to this sender's position.
+        @param sender: the widget that has been clicked
+        """
+        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
+        menu.setStyleName(self.style["menu"])
+
+        def button_cb(item):
+            """You can not put that method in the loop and rely
+            on _key, because it is overwritten by each step.
+            You can rely on item.key instead, which is copied
+            from _key after the item creation.
+            @param item: the menu item that has been clicked
+            """
+            if self._callback is not None:
+                self._callback(sender=sender, key=item.key)
+            self.hide(autoClosed=True)
+
+        for _key in self._entries.keys():
+            entry = self._entries[_key]
+            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
+                continue
+            title = entry["title"] if "title" in entry.keys() else _key
+            item = Button(title, button_cb)
+            item.key = _key
+            item.setStyleName(self.style["item"])
+            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
+            menu.add(item)
+        if len(menu.getChildren()) == 0:
+            return
+        self.add(menu)
+        if self.vertical is True:
+            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
+            y = sender.getAbsoluteTop()
+        else:
+            x = sender.getAbsoluteLeft()
+            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
+        self.setPopupPosition(x, y)
+        self.show()
+        if self.style["selected"]:
+            sender.addStyleDependentName(self.style["selected"])
+
+        def _onHide(popup):
+            if self.style["selected"]:
+                sender.removeStyleDependentName(self.style["selected"])
+            return PopupPanel.onHideImpl(self, popup)
+
+        self.onHideImpl = _onHide
+
+    def registerClickSender(self, sender, button=BUTTON_LEFT):
+        """Bind the menu to the specified sender.
+        @param sender: the widget to which the menu should be bound
+        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
+        """
+        self._senders.setdefault(sender, [])
+        self._senders[sender].append(button)
+
+        if button == BUTTON_RIGHT:
+            # WARNING: to disable the context menu is a bit tricky...
+            # The following seems to work on Firefox 24.0, but:
+            # TODO: find a cleaner way to disable the context menu
+            sender.getElement().setAttribute("oncontextmenu", "return false")
+
+        def _onBrowserEvent(event):
+            button = DOM.eventGetButton(event)
+            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
+                self._show(sender)
+            return sender.__class__.onBrowserEvent(sender, event)
+
+        sender.onBrowserEvent = _onBrowserEvent
+
+    def registerMiddleClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_MIDDLE)
+
+    def registerRightClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_RIGHT)
+
+
+### Generic panels ###
+
+
+class ToggleStackPanel(StackPanel):
+    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
+    visible at the same time, clicking a sub-panel header will not display it and hide
+    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
+    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
+    """
+
+    def __init__(self, **kwargs):
+        StackPanel.__init__(self, **kwargs)
+
+    def onBrowserEvent(self, event):
+        if DOM.eventGetType(event) == "click":
+            index = self.getDividerIndex(DOM.eventGetTarget(event))
+            if index != -1:
+                self.toggleStack(index)
+
+    def add(self, widget, stackText="", asHTML=False, visible=False):
+        StackPanel.add(self, widget, stackText, asHTML)
+        self.setStackVisible(self.getWidgetCount() - 1, visible)
+
+    def toggleStack(self, index):
+        if index >= self.getWidgetCount():
+            return
+        visible = not self.getWidget(index).getVisible()
+        self.setStackVisible(index, visible)
+        for listener in self.stackListeners:
+            listener.onStackChanged(self, index, visible)
+
+
+class TitlePanel(ToggleStackPanel):
+    """A toggle panel to set the message title"""
+    def __init__(self):
+        ToggleStackPanel.__init__(self, Width="100%")
+        self.text_area = TextArea()
+        self.add(self.text_area, _("Title"))
+        self.addStackChangeListener(self)
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        text = self.text_area.getText()
+        suffix = "" if (visible or not text) else (": %s" % text)
+        sender.setStackText(index, _("Title") + suffix)
+
+    def getText(self):
+        return self.text_area.getText()
+
+    def setText(self, text):
+        self.text_area.setText(text)
+
+
+class ScrollPanelWrapper(SimplePanel):
+    """Scroll Panel like component, wich use the full available space
+    to work around percent size issue, it use some of the ideas found
+    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
+    specially in code given at comment #46, thanks to Stefan Bachert"""
+
+    def __init__(self, *args, **kwargs):
+        SimplePanel.__init__(self)
+        self.spanel = ScrollPanel(*args, **kwargs)
+        SimplePanel.setWidget(self, self.spanel)
+        DOM.setStyleAttribute(self.getElement(), "position", "relative")
+        DOM.setStyleAttribute(self.getElement(), "top", "0px")
+        DOM.setStyleAttribute(self.getElement(), "left", "0px")
+        DOM.setStyleAttribute(self.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.getElement(), "height", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
+        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
+
+    def setWidget(self, widget):
+        self.spanel.setWidget(widget)
+
+    def setScrollPosition(self, position):
+        self.spanel.setScrollPosition(position)
+
+    def scrollToBottom(self):
+        self.setScrollPosition(self.spanel.getElement().scrollHeight)
--- a/src/browser/sat_browser/base_panels.py	Thu Feb 26 13:10:46 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,688 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import pyjd  # this is dummy in pyjs
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from sat.core.i18n import _
-from sat_frontends.tools import strings
-from sat_frontends.tools import jid
-
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-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
-
-import html_tools
-import base_widget
-from constants import Const as C
-
-
-
-class Occupant(HTML):
-    """Occupant of a MUC room"""
-
-    def __init__(self, nick, state=None, special=""):
-        """
-        @param nick: the user nickname
-        @param state: the user chate state (XEP-0085)
-        @param special: a string of symbols (e.g: for activities)
-        """
-        HTML.__init__(self, StyleName="occupant")
-        self.nick = nick
-        self._state = state
-        self.special = special
-        self._refresh()
-
-    def __str__(self):
-        return self.nick
-
-    def setState(self, state):
-        self._state = state
-        self._refresh()
-
-    def addSpecial(self, special):
-        """@param special: unicode"""
-        if special not in self.special:
-            self.special += special
-            self._refresh()
-
-    def removeSpecials(self, special):
-        """@param special: unicode or list"""
-        if not isinstance(special, list):
-            special = [special]
-        for symbol in special:
-            self.special = self.special.replace(symbol, "")
-            self._refresh()
-
-    def _refresh(self):
-        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
-        special = "" if len(self.special) == 0 else " %s" % self.special
-        self.setHTML("%s%s%s" % (html_tools.html_sanitize(self.nick), special, state))
-
-
-class ContactsPanel(VerticalPanel):
-    """ContactList graphic representation
-
-    Special features like popup menu panel or changing the contact states must be done in a sub-class.
-    """
-
-    def __init__(self, parent, on_click=None, handle_menu=True):
-        """
-        @param on_click (callable): click callback (used if ContactBox is created)
-        @param handle_menu (bool): if True, bind a popup menu to the avatar (used if ContactBox is created)
-        """ # FIXME
-        VerticalPanel.__init__(self)
-        self._parent = parent
-        self.host = parent.host
-        self._contacts = {} # entity jid to ContactBox map
-        self.click_listener = None
-        self.handle_menu = handle_menu
-
-        if on_click is not None:
-            self.onClick = on_click
-
-    def display(self, jids):
-        """Display a contact in the list.
-
-        @param jids (list[jid.JID]): jids to display (the order is kept)
-        @param name (unicode): optional name of the contact
-        """
-        # FIXME: we do a full clear and add boxes after, we should only remove recently hidden boxes and add new ones, and re-order
-        current = [box.jid for box in self.children if isinstance(box, base_widget.ContactBox)]
-        if current == jids:
-            # the display doesn't change
-            return
-        self.clear()
-        for jid_ in jids:
-            assert isinstance(jid_, jid.JID)
-            box = self.getContactBox(jid_)
-            VerticalPanel.append(self, box)
-
-    def isContactPresent(self, contact_jid):
-        """Return True if a contact is present in the panel"""
-        return contact_jid in self._contacts
-
-    def getContacts(self):
-        return self._contacts
-
-    def getContactBox(self, contact_jid):
-        """get the Contactbox of a contact
-
-        if the Contactbox doesn't exists, it will be created
-        @param contact_jid (jid.JID): the contact
-        @return: ContactBox instance
-        """
-        try:
-            return self._contacts[contact_jid.bare]
-        except KeyError:
-            box = base_widget.ContactBox(self, contact_jid)
-            self._contacts[contact_jid.bare] = box
-            return box
-
-    def updateAvatar(self, jid_, url):
-        """Update the avatar of the given contact
-
-        @param jid_ (jid.JID): contact jid
-        @param url (unicode): image url
-        """
-        try:
-            self.getContactBox(jid_).updateAvatar(url)
-        except TypeError:
-            pass
-
-    def updateNick(self, jid_, new_nick):
-        """Update the avatar of the given contact
-
-        @param jid_ (jid.JID): contact jid
-        @param new_nick (unicode): new nick of the contact
-        """
-        try:
-            self.getContactBox(jid_).updateNick(new_nick)
-        except TypeError:
-            pass
-
-
-
-# FIXME: must be removed and ContactsPanel must be used instead
-class OccupantsList(AbsolutePanel):
-    """Panel user to show occupants of a room"""
-
-    def __init__(self):
-        AbsolutePanel.__init__(self)
-        self.occupants_list = {}
-        self.setStyleName('occupantsList')
-
-    def addOccupant(self, nick):
-        if nick in self.occupants_list:
-            return
-        _occupant = Occupant(nick)
-        self.occupants_list[nick] = _occupant
-        self.add(_occupant)
-
-    def removeOccupant(self, nick):
-        try:
-            self.remove(self.occupants_list[nick])
-        except KeyError:
-            log.error("trying to remove an unexisting nick")
-
-    def getOccupantBox(self, nick):
-        """Get the widget element of the given nick.
-
-        @return: Occupant
-        """
-        try:
-            return self.occupants_list[nick]
-        except KeyError:
-            return None
-
-    def clear(self):
-        self.occupants_list.clear()
-        AbsolutePanel.clear(self)
-
-    def updateSpecials(self, occupants=[], html=""):
-        """Set the specified html "symbol" to the listed occupants,
-        and eventually remove it from the others (if they got it).
-        This is used for example to visualize who is playing a game.
-        @param occupants: list of the occupants that need the symbol
-        @param html: unicode symbol (actually one character or more)
-        or a list to assign different symbols of the same family.
-        """
-        index = 0
-        special = html
-        for occupant in self.occupants_list.keys():
-            if occupant in occupants:
-                if isinstance(html, list):
-                    special = html[index]
-                    index = (index + 1) % len(html)
-                self.occupants_list[occupant].addSpecial(special)
-            else:
-                self.occupants_list[occupant].removeSpecials(html)
-
-
-class PopupMenuPanel(PopupPanel):
-    """This implementation of a popup menu (context menu) allow you to assign
-    two special methods which are common to all the items, in order to hide
-    certain items and also easily define their callbacks. The menu can be
-    bound to any of the mouse button (left, middle, right).
-    """
-    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
-        """
-        @param entries: a dict of dicts, where each sub-dict is representing
-        one menu item: the sub-dict key can be used as the item text and
-        description, but optional "title" and "desc" entries would be used
-        if they exists. The sub-dicts may be extended later to do
-        more complicated stuff or overwrite the common methods.
-        @param hide: function  with 2 args: widget, key as string and
-        returns True if that item should be hidden from the context menu.
-        @param callback: function with 2 args: sender, key as string
-        @param vertical: True or False, to set the direction
-        @param item_style: alternative CSS class for the menu items
-        @param menu_style: supplementary CSS class for the sender widget
-        """
-        PopupPanel.__init__(self, autoHide=True, **kwargs)
-        self._entries = entries
-        self._hide = hide
-        self._callback = callback
-        self.vertical = vertical
-        self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"}
-        if isinstance(style, dict):
-            self.style.update(style)
-        self._senders = {}
-
-    def _show(self, sender):
-        """Popup the menu relative to this sender's position.
-        @param sender: the widget that has been clicked
-        """
-        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
-        menu.setStyleName(self.style["menu"])
-
-        def button_cb(item):
-            """You can not put that method in the loop and rely
-            on _key, because it is overwritten by each step.
-            You can rely on item.key instead, which is copied
-            from _key after the item creation.
-            @param item: the menu item that has been clicked
-            """
-            if self._callback is not None:
-                self._callback(sender=sender, key=item.key)
-            self.hide(autoClosed=True)
-
-        for _key in self._entries.keys():
-            entry = self._entries[_key]
-            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
-                continue
-            title = entry["title"] if "title" in entry.keys() else _key
-            item = Button(title, button_cb)
-            item.key = _key
-            item.setStyleName(self.style["item"])
-            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
-            menu.add(item)
-        if len(menu.getChildren()) == 0:
-            return
-        self.add(menu)
-        if self.vertical is True:
-            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
-            y = sender.getAbsoluteTop()
-        else:
-            x = sender.getAbsoluteLeft()
-            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
-        self.setPopupPosition(x, y)
-        self.show()
-        if self.style["selected"]:
-            sender.addStyleDependentName(self.style["selected"])
-
-        def _onHide(popup):
-            if self.style["selected"]:
-                sender.removeStyleDependentName(self.style["selected"])
-            return PopupPanel.onHideImpl(self, popup)
-
-        self.onHideImpl = _onHide
-
-    def registerClickSender(self, sender, button=BUTTON_LEFT):
-        """Bind the menu to the specified sender.
-        @param sender: the widget to which the menu should be bound
-        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
-        """
-        self._senders.setdefault(sender, [])
-        self._senders[sender].append(button)
-
-        if button == BUTTON_RIGHT:
-            # WARNING: to disable the context menu is a bit tricky...
-            # The following seems to work on Firefox 24.0, but:
-            # TODO: find a cleaner way to disable the context menu
-            sender.getElement().setAttribute("oncontextmenu", "return false")
-
-        def _onBrowserEvent(event):
-            button = DOM.eventGetButton(event)
-            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
-                self._show(sender)
-            return sender.__class__.onBrowserEvent(sender, event)
-
-        sender.onBrowserEvent = _onBrowserEvent
-
-    def registerMiddleClickSender(self, sender):
-        self.registerClickSender(sender, BUTTON_MIDDLE)
-
-    def registerRightClickSender(self, sender):
-        self.registerClickSender(sender, BUTTON_RIGHT)
-
-
-class ToggleStackPanel(StackPanel):
-    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
-    visible at the same time, clicking a sub-panel header will not display it and hide
-    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
-    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
-    """
-
-    def __init__(self, **kwargs):
-        StackPanel.__init__(self, **kwargs)
-
-    def onBrowserEvent(self, event):
-        if DOM.eventGetType(event) == "click":
-            index = self.getDividerIndex(DOM.eventGetTarget(event))
-            if index != -1:
-                self.toggleStack(index)
-
-    def add(self, widget, stackText="", asHTML=False, visible=False):
-        StackPanel.add(self, widget, stackText, asHTML)
-        self.setStackVisible(self.getWidgetCount() - 1, visible)
-
-    def toggleStack(self, index):
-        if index >= self.getWidgetCount():
-            return
-        visible = not self.getWidget(index).getVisible()
-        self.setStackVisible(index, visible)
-        for listener in self.stackListeners:
-            listener.onStackChanged(self, index, visible)
-
-
-class TitlePanel(ToggleStackPanel):
-    """A toggle panel to set the message title"""
-    def __init__(self):
-        ToggleStackPanel.__init__(self, Width="100%")
-        self.text_area = TextArea()
-        self.add(self.text_area, _("Title"))
-        self.addStackChangeListener(self)
-
-    def onStackChanged(self, sender, index, visible=None):
-        if visible is None:
-            visible = sender.getWidget(index).getVisible()
-        text = self.text_area.getText()
-        suffix = "" if (visible or not text) else (": %s" % text)
-        sender.setStackText(index, _("Title") + suffix)
-
-    def getText(self):
-        return self.text_area.getText()
-
-    def setText(self, text):
-        self.text_area.setText(text)
-
-
-class BaseTextEditor(object):
-    """Basic definition of a text editor. The method edit gets a boolean parameter which
-    should be set to True when you want to edit the text and False to only display it."""
-
-    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
-        """
-        Remark when inheriting this class: since the setContent method could be
-        overwritten by the child class, you should consider calling this __init__
-        after all the parameters affecting this setContent method have been set.
-        @param content: dict with at least a 'text' key
-        @param strproc: method to be applied on strings to clean the content
-        @param modifiedCb: method to be called when the text has been modified.
-        If this method returns:
-        - True: the modification will be saved and afterEditCb called;
-        - False: the modification won't be saved and afterEditCb called;
-        - None: the modification won't be saved and afterEditCb not called.
-        @param afterEditCb: method to be called when the edition is done
-        """
-        if content is None:
-            content = {'text': ''}
-        assert('text' in content)
-        if strproc is None:
-            def strproc(text):
-                try:
-                    return text.strip()
-                except (TypeError, AttributeError):
-                    return text
-        self.strproc = strproc
-        self.__modifiedCb = modifiedCb
-        self._afterEditCb = afterEditCb
-        self.initialized = False
-        self.edit_listeners = []
-        self.setContent(content)
-
-    def setContent(self, content=None):
-        """Set the editable content. The displayed content, which is set from the child class, could differ.
-        @param content: dict with at least a 'text' key
-        """
-        if content is None:
-            content = {'text': ''}
-        elif not isinstance(content, dict):
-            content = {'text': content}
-        assert('text' in content)
-        self._original_content = {}
-        for key in content:
-            self._original_content[key] = self.strproc(content[key])
-
-    def getContent(self):
-        """Get the current edited or editable content.
-        @return: dict with at least a 'text' key
-        """
-        raise NotImplementedError
-
-    def setOriginalContent(self, content):
-        """Use this method with care! Content initialization should normally be
-        done with self.setContent. This method exists to let you trick the editor,
-        e.g. for self.modified to return True also when nothing has been modified.
-        @param content: dict
-        """
-        self._original_content = content
-
-    def getOriginalContent(self):
-        """
-        @return the original content before modification (dict)
-        """
-        return self._original_content
-
-    def modified(self, content=None):
-        """Check if the content has been modified.
-        Remark: we don't use the direct comparison because we want to ignore empty elements
-        @content: content to be check against the original content or None to use the current content
-        @return: True if the content has been modified.
-        """
-        if content is None:
-            content = self.getContent()
-        # the following method returns True if one non empty element exists in a but not in b
-        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
-        # the following method returns True if the values for the common keys are not equals
-        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
-        # finally the combination of both to return True if a difference is found
-        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
-
-        return diff(content, self._original_content)
-
-    def edit(self, edit, abort=False, sync=False):
-        """
-        Remark: the editor must be visible before you call this method.
-        @param edit: set to True to edit the content or False to only display it
-        @param abort: set to True to cancel the edition and loose the changes.
-        If edit and abort are both True, self.abortEdition can be used to ask for a
-        confirmation. When edit is False and abort is True, abortion is actually done.
-        @param sync: set to True to cancel the edition after the content has been saved somewhere else
-        """
-        if edit:
-            if not self.initialized:
-                self.syncToEditor()  # e.g.: use the selected target and unibox content
-            self.setFocus(True)
-            if abort:
-                content = self.getContent()
-                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
-                    self.edit(False, True, sync)
-                    return
-            if sync:
-                self.syncFromEditor(content)  # e.g.: save the content to unibox
-                return
-        else:
-            if not self.initialized:
-                return
-            content = self.getContent()
-            if abort:
-                self._afterEditCb(content)
-                return
-            if self.__modifiedCb and self.modified(content):
-                result = self.__modifiedCb(content)  # e.g.: send a message or update something
-                if result is not None:
-                    if self._afterEditCb:
-                        self._afterEditCb(content)  # e.g.: restore the display mode
-                    if result is True:
-                        self.setContent(content)
-            elif self._afterEditCb:
-                self._afterEditCb(content)
-
-        self.initialized = True
-
-    def setFocus(self, focus):
-        """
-        @param focus: set to True to focus the editor
-        """
-        raise NotImplementedError
-
-    def syncToEditor(self):
-        pass
-
-    def syncFromEditor(self, content):
-        pass
-
-    def abortEdition(self, content):
-        return True
-
-    def addEditListener(self, listener):
-        """Add a method to be called whenever the text is edited.
-        @param listener: method taking two arguments: sender, keycode"""
-        self.edit_listeners.append(listener)
-
-
-class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
-    """Base class for manage a simple text editor."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        """
-        @param content
-        @param modifiedCb
-        @param afterEditCb
-        @param options: dict with the following value:
-        - no_xhtml: set to True to clean any xhtml content.
-        - enhance_display: if True, the display text will be enhanced with strings.addURLToText
-        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
-        - listen_focus: set to True to terminate the edition when the focus is lost.
-        - listen_click: set to True to start the edition when you click on the widget.
-        """
-        self.options = {'no_xhtml': False,
-                        'enhance_display': True,
-                        'listen_keyboard': True,
-                        'listen_focus': False,
-                        'listen_click': False
-                        }
-        if options:
-            self.options.update(options)
-        self.__shift_down = False
-        if self.options['listen_focus']:
-            FocusHandler.__init__(self)
-        if self.options['listen_click']:
-            ClickHandler.__init__(self)
-        KeyboardHandler.__init__(self)
-        strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
-        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
-        self.textarea = self.display = None
-
-    def setContent(self, content=None):
-        BaseTextEditor.setContent(self, content)
-
-    def getContent(self):
-        raise NotImplementedError
-
-    def edit(self, edit, abort=False, sync=False):
-        BaseTextEditor.edit(self, edit)
-        if edit:
-            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
-                self.textarea.addFocusListener(self)
-            if self.options['listen_click']:
-                self.display.clearClickListener()
-            if self not in self.textarea._keyboardListeners:
-                self.textarea.addKeyboardListener(self)
-        else:
-            self.setDisplayContent()
-            if self.options['listen_focus']:
-                try:
-                    self.textarea.removeFocusListener(self)
-                except ValueError:
-                    pass
-            if self.options['listen_click'] and self not in self.display._clickListeners:
-                self.display.addClickListener(self)
-            try:
-                self.textarea.removeKeyboardListener(self)
-            except ValueError:
-                pass
-
-    def setDisplayContent(self):
-        text = self._original_content['text']
-        if not self.options['no_xhtml']:
-            text = strings.addURLToImage(text)
-        if self.options['enhance_display']:
-            text = strings.addURLToText(text)
-        self.display.setHTML(html_tools.convertNewLinesToXHTML(text))
-
-    def setFocus(self, focus):
-        raise NotImplementedError
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        for listener in self.edit_listeners:
-            listener(self.textarea, keycode)
-        if not self.options['listen_keyboard']:
-            return
-        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
-            self.__shift_down = True
-            return
-        if keycode == KEY_ENTER:  # finish the edition
-            self.textarea.setFocus(False)
-            if not self.options['listen_focus']:
-                self.edit(False)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        if keycode == KEY_SHIFT:
-            self.__shift_down = False
-
-    def onLostFocus(self, sender):
-        """Finish the edition when focus is lost"""
-        if self.options['listen_focus']:
-            self.edit(False)
-
-    def onClick(self, sender=None):
-        """Start the edition when the widget is clicked"""
-        if self.options['listen_click']:
-            self.edit(True)
-
-    def onBrowserEvent(self, event):
-        if self.options['listen_focus']:
-            FocusHandler.onBrowserEvent(self, event)
-        if self.options['listen_click']:
-            ClickHandler.onBrowserEvent(self, event)
-        KeyboardHandler.onBrowserEvent(self, event)
-
-
-class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
-    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        HTML.__init__(self)
-        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
-        self.textarea = self.display = self
-
-    def getContent(self):
-        text = DOM.getInnerHTML(self.getElement())
-        return {'text': self.strproc(text) if text else ''}
-
-    def edit(self, edit, abort=False, sync=False):
-        if edit:
-            self.textarea.setHTML(self._original_content['text'])
-        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
-        SimpleTextEditor.edit(self, edit, abort, sync)
-
-    def setFocus(self, focus):
-        if focus:
-            self.getElement().focus()
-        else:
-            self.getElement().blur()
-
-
-class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
-    """Manage a simple text editor with a TextArea for editing, HTML for display."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        SimplePanel.__init__(self)
-        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
-        self.textarea = TextArea()
-        self.display = HTML()
-
-    def getContent(self):
-        text = self.textarea.getText()
-        return {'text': self.strproc(text) if text else ''}
-
-    def edit(self, edit, abort=False, sync=False):
-        if edit:
-            self.textarea.setText(self._original_content['text'])
-        self.setWidget(self.textarea if edit else self.display)
-        SimpleTextEditor.edit(self, edit, abort, sync)
-
-    def setFocus(self, focus):
-        if focus and self.isAttached():
-            self.textarea.setCursorPos(len(self.textarea.getText()))
-        self.textarea.setFocus(focus)
--- a/src/browser/sat_browser/base_widget.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/base_widget.py	Thu Feb 26 18:10:54 2015 +0100
@@ -20,197 +20,18 @@
 import pyjd  # this is dummy in pyjs
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat.core import exceptions
-from sat.core.i18n import _
-from sat_frontends.quick_frontend import quick_widgets
+import base_menu
 
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.ScrollPanel import ScrollPanel
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.HTMLPanel import HTMLPanel
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Image import Image
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Widget import Widget
-from pyjamas.ui.DragWidget import DragWidget
-from pyjamas.ui.DropWidget import DropWidget
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui import HasAlignment
-from pyjamas import DOM
-from pyjamas import Window
-from constants import Const as C
 
-from __pyjamas__ import doc
-
-import dialog
-import base_menu
-import html_tools
-
-unicode = str # XXX: pyjama doesn't manage unicode
+### Exceptions ###
 
 
 class NoLiberviaWidgetException(Exception):
+    """A Libervia widget was expected"""
     pass
 
 
-class DragLabel(DragWidget):
-
-    def __init__(self, text, type_, host=None):
-        """Base of Drag n Drop mecanism in Libervia
-
-        @param text: data embedded with in drag n drop operation
-        @param type_: type of data that we are dragging
-        @param host: if not None, the host will be use to highlight BorderWidgets
-        """
-        DragWidget.__init__(self)
-        self.host = host
-        self._text = text
-        self.type_ = type_
-
-    def onDragStart(self, event):
-        dt = event.dataTransfer
-        dt.setData('text/plain', "%s\n%s" % (self._text, self.type_))
-        dt.setDragImage(self.getElement(), 15, 15)
-        if self.host is not None:
-            current_panel = self.host.tab_panel.getCurrentPanel()
-            for widget in current_panel.widgets:
-                if isinstance(widget, BorderWidget):
-                    widget.addStyleName('borderWidgetOnDrag')
-
-    def onDragEnd(self, event):
-        if self.host is not None:
-            current_panel = self.host.tab_panel.getCurrentPanel()
-            for widget in current_panel.widgets:
-                if isinstance(widget, BorderWidget):
-                    widget.removeStyleName('borderWidgetOnDrag')
-
-
-class LiberviaDragWidget(DragLabel):
-    """ A DragLabel which keep the widget being dragged as class value """
-    current = None  # widget currently dragged
-
-    def __init__(self, text, type_, widget):
-        DragLabel.__init__(self, text, type_, widget.host)
-        self.widget = widget
-
-    def onDragStart(self, event):
-        LiberviaDragWidget.current = self.widget
-        DragLabel.onDragStart(self, event)
-
-    def onDragEnd(self, event):
-        DragLabel.onDragEnd(self, event)
-        LiberviaDragWidget.current = None
-
-
-class DropCell(DropWidget):
-    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
-    drop_keys = {}
-
-    def __init__(self, host):
-        DropWidget.__init__(self)
-        self.host = host
-        self.setStyleName('dropCell')
-
-    @classmethod
-    def addDropKey(cls, key, cb):
-        """Add a association between a key and a class to create on drop.
-
-        @param key: key to be associated (e.g. "CONTACT", "CHAT")
-        @param cb: a callable (either a class or method) returning a
-            LiberviaWidget instance
-        """
-        DropCell.drop_keys[key] = cb
-
-    def onDragEnter(self, event):
-        if self == LiberviaDragWidget.current:
-            return
-        self.addStyleName('dragover')
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
-            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
-            # We check that we are inside widget's box, and we don't remove the style in this case because
-            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
-            # don't want that
-            self.removeStyleName('dragover')
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def _getCellAndRow(self, grid, event):
-        """Return cell and row index where the event is occuring"""
-        cell = grid.getEventTargetCell(event)
-        row = DOM.getParent(cell)
-        return (row.rowIndex, cell.cellIndex)
-
-    def onDrop(self, event):
-        """
-        @raise NoLiberviaWidgetException: something else than a LiberviaWidget
-            has been returned by the callback.
-        """
-        self.removeStyleName('dragover')
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        try:
-            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-            # item_type = dt.getData("type")
-            log.debug("message: %s" % item)
-            log.debug("type: %s" % item_type)
-        except:
-            log.debug("no message found")
-            item = '&nbsp;'
-            item_type = None
-        if item_type == "WIDGET":
-            if not LiberviaDragWidget.current:
-                log.error("No widget registered in LiberviaDragWidget !")
-                return
-            _new_panel = LiberviaDragWidget.current
-            if self == _new_panel:  # We can't drop on ourself
-                return
-            # we need to remove the widget from the panel as it will be inserted elsewhere
-            widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
-            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
-            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
-            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
-                # the dropped widget is the only one in the same row
-                # as the target widget (self), we don't do anything
-                return
-            widgets_panel.removeWidget(_new_panel)
-        elif item_type in self.drop_keys:
-            _new_panel = self.drop_keys[item_type](self.host, item)
-            if not isinstance(_new_panel, LiberviaWidget):
-                raise NoLiberviaWidgetException
-        else:
-            log.warning("unmanaged item type")
-            return
-        if isinstance(self, LiberviaWidget):
-            # self.host.unregisterWidget(self) # FIXME
-            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)"""
-        if isinstance(self, quick_widgets.QuickWidget):
-            self.host.widgets.deleteWidget(self)
+### Menus ###
 
 
 class WidgetMenuBar(base_menu.GenericMenuBar):
@@ -259,744 +80,3 @@
     @classmethod
     def getCategoryHTML(cls, menu_name_i18n, type_):
         return menu_name_i18n
-
-
-class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
-
-    def __init__(self, parent, host, title, info=None):
-        """
-        @param parent (LiberviaWidget): LiberWidget instance
-        @param host (SatWebFrontend): SatWebFrontend instance
-        @param title (Label, HTML): text widget instance
-        @param info (Widget): text widget instance
-        """
-        AbsolutePanel.__init__(self)
-        self.add(title)
-        if info:
-            # FIXME: temporary design to display the info near the menu
-            button_group_wrapper = HorizontalPanel()
-            button_group_wrapper.add(info)
-        else:
-            button_group_wrapper = SimplePanel()
-        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
-        button_group = WidgetMenuBar(parent, host)
-        button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.MenuCmd(parent, 'onSetting'))
-        button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.MenuCmd(parent, 'onClose'))
-        button_group_wrapper.add(button_group)
-        self.add(button_group_wrapper)
-        self.addStyleName('widgetHeader')
-        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
-
-
-class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
-    """Libervia's widget which can replace itself with a dropped widget on DnD"""
-
-    def __init__(self, host, title='', info=None, selectable=False):
-        """Init the widget
-
-        @param host (SatWebFrontend): SatWebFrontend instance
-        @param title (str): title shown in the header of the widget
-        @param info (str, callable): info shown in the header of the widget
-        @param selectable (bool): True is widget can be selected by user
-        """
-        VerticalPanel.__init__(self)
-        DropCell.__init__(self, host)
-        ClickHandler.__init__(self)
-        self._selectable = selectable
-        self._title_id = HTMLPanel.createUniqueId()
-        self._setting_button_id = HTMLPanel.createUniqueId()
-        self._close_button_id = HTMLPanel.createUniqueId()
-        self._title = Label(title)
-        self._title.setStyleName('widgetHeader_title')
-        if info is not None:
-            if isinstance(info, str):
-                self._info = HTML(info)
-            else:  # the info will be set by a callback
-                assert callable(info)
-                self._info = HTML()
-                info(self._info.setHTML)
-            self._info.setStyleName('widgetHeader_info')
-        else:
-            self._info = None
-        header = WidgetHeader(self, host, self._title, self._info)
-        self.add(header)
-        self.setSize('100%', '100%')
-        self.addStyleName('widget')
-        if self._selectable:
-            self.addClickListener(self)
-
-            # FIXME
-            # 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) # FIXME
-
-    def getDebugName(self):
-        return "%s (%s)" % (self, self._title.getText())
-
-    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 (raise an error if not found)
-        @return: the parent/ancestor or None if it has not been found
-        @raise exceptions.InternalError: expect is True and no parent is 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:
-            raise exceptions.InternalError("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 """
-        widgets_panel = self.getParent(WidgetsPanel, expect=True)
-        widgets_panel.removeWidget(self)
-        self.onQuit()
-        self.host.widgets.deleteWidget(self)
-
-    def onQuit(self):
-        """ Called when the widget is actually ending """
-        pass
-
-    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.getParent(WidgetsPanel, expect=True)
-        row, col = widpanel.getIndex(self)
-        body = VerticalPanel()
-
-        # colspan & rowspan
-        colspan = widpanel.getColSpan(row, col)
-        rowspan = widpanel.getRowSpan(row, col)
-
-        def onColSpanChange(value):
-            widpanel.setColSpan(row, col, value)
-
-        def onRowSpanChange(value):
-            widpanel.setRowSpan(row, col, value)
-        colspan_setter = dialog.IntSetter("Columns span", colspan)
-        colspan_setter.addValueChangeListener(onColSpanChange)
-        colspan_setter.setWidth('100%')
-        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
-        rowspan_setter.addValueChangeListener(onRowSpanChange)
-        rowspan_setter.setWidth('100%')
-        body.add(colspan_setter)
-        body.add(rowspan_setter)
-
-        # size
-        width_str = self.getWidth()
-        if width_str.endswith('px'):
-            width = int(width_str[:-2])
-        else:
-            width = 0
-        height_str = self.getHeight()
-        if height_str.endswith('px'):
-            height = int(height_str[:-2])
-        else:
-            height = 0
-
-        def onWidthChange(value):
-            if not value:
-                self.setWidth('100%')
-            else:
-                self.setWidth('%dpx' % value)
-
-        def onHeightChange(value):
-            if not value:
-                self.setHeight('100%')
-            else:
-                self.setHeight('%dpx' % value)
-        width_setter = dialog.IntSetter("width (0=auto)", width)
-        width_setter.addValueChangeListener(onWidthChange)
-        width_setter.setWidth('100%')
-        height_setter = dialog.IntSetter("height (0=auto)", height)
-        height_setter.addValueChangeListener(onHeightChange)
-        height_setter.setHeight('100%')
-        body.add(width_setter)
-        body.add(height_setter)
-
-        # reset
-        def onReset(sender):
-            colspan_setter.setValue(1)
-            rowspan_setter.setValue(1)
-            width_setter.setValue(0)
-            height_setter.setValue(0)
-
-        reset_bt = Button("Reset", onReset)
-        body.add(reset_bt)
-        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
-
-        _dialog = dialog.GenericDialog("Widget setting", body)
-        _dialog.show()
-
-    def setTitle(self, text):
-        """change the title in the header of the widget
-        @param text: text of the new title"""
-        self._title.setText(text)
-
-    def setHeaderInfo(self, text):
-        """change the info in the header of the widget
-        @param text: text of the new title"""
-        try:
-            self._info.setHTML(text)
-        except TypeError:
-            log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
-
-    def isSelectable(self):
-        return self._selectable
-
-    def setSelectable(self, selectable):
-        if not self._selectable:
-            try:
-                self.removeClickListener(self)
-            except ValueError:
-                pass
-        if self.selectable and not self in self._clickListeners:
-            self.addClickListener(self)
-        self._selectable = selectable
-
-    def getWarningData(self):
-        """ Return exposition warning level when this widget is selected and something is sent to it
-        This method should be overriden by children
-        @return: tuple (warning level type/HTML msg). Type can be one of:
-            - PUBLIC
-            - GROUP
-            - ONE2ONE
-            - MISC
-            - NONE
-        """
-        if not self._selectable:
-            log.error("getWarningLevel must not be called for an unselectable widget")
-            raise Exception
-        # TODO: cleaner warning types (more general constants)
-        return ("NONE", None)
-
-    def setWidget(self, widget, scrollable=True):
-        """Set the widget that will be in the body of the LiberviaWidget
-        @param widget: widget to put in the body
-        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
-        if scrollable:
-            _scrollpanelwrapper = ScrollPanelWrapper()
-            _scrollpanelwrapper.setStyleName('widgetBody')
-            _scrollpanelwrapper.setWidget(widget)
-            body_wid = _scrollpanelwrapper
-        else:
-            body_wid = widget
-        self.add(body_wid)
-        self.setCellHeight(body_wid, '100%')
-
-    def doDetachChildren(self):
-        # We need to force the use of a panel subclass method here,
-        # for the same reason as doAttachChildren
-        VerticalPanel.doDetachChildren(self)
-
-    def doAttachChildren(self):
-        # We need to force the use of a panel subclass method here, else
-        # the event will not propagate to children
-        VerticalPanel.doAttachChildren(self)
-
-    def matchEntity(self, item):
-        """Check if this widget corresponds to the given entity.
-
-        This method should be overwritten by child classes.
-        @return: True if the widget matches the entity"""
-        raise NotImplementedError
-
-    def addMenus(self, menu_bar):
-        """Add menus to the header.
-
-        This method can be overwritten by child classes.
-        @param menu_bar (GenericMenuBar): menu bar of the widget's header
-        """
-        pass
-
-
-class ScrollPanelWrapper(SimplePanel):
-    """Scroll Panel like component, wich use the full available space
-    to work around percent size issue, it use some of the ideas found
-    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
-    specially in code given at comment #46, thanks to Stefan Bachert"""
-
-    def __init__(self, *args, **kwargs):
-        SimplePanel.__init__(self)
-        self.spanel = ScrollPanel(*args, **kwargs)
-        SimplePanel.setWidget(self, self.spanel)
-        DOM.setStyleAttribute(self.getElement(), "position", "relative")
-        DOM.setStyleAttribute(self.getElement(), "top", "0px")
-        DOM.setStyleAttribute(self.getElement(), "left", "0px")
-        DOM.setStyleAttribute(self.getElement(), "width", "100%")
-        DOM.setStyleAttribute(self.getElement(), "height", "100%")
-        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
-        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
-        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
-
-    def setWidget(self, widget):
-        self.spanel.setWidget(widget)
-
-    def setScrollPosition(self, position):
-        self.spanel.setScrollPosition(position)
-
-    def scrollToBottom(self):
-        self.setScrollPosition(self.spanel.getElement().scrollHeight)
-
-
-class EmptyWidget(DropCell, SimplePanel):
-    """Empty dropable panel"""
-
-    def __init__(self, host):
-        SimplePanel.__init__(self)
-        DropCell.__init__(self, host)
-        #self.setWidget(HTML(''))
-        self.setSize('100%', '100%')
-
-
-class BorderWidget(EmptyWidget):
-    def __init__(self, host):
-        EmptyWidget.__init__(self, host)
-        self.addStyleName('borderPanel')
-
-
-class LeftBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('leftBorderWidget')
-
-
-class RightBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('rightBorderWidget')
-
-
-class BottomBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('bottomBorderWidget')
-
-
-class WidgetsPanel(ScrollPanelWrapper):
-
-    def __init__(self, host, locked=False):
-        """
-
-        @param host (SatWebFrontend): host instance
-        @param locked (bool): If True, the tab containing self will not be
-            removed when there are no more widget inside self. If False, the
-            tab will be removed with self's last widget.
-        """
-        ScrollPanelWrapper.__init__(self)
-        self.setSize('100%', '100%')
-        self.host = host
-        self.locked = locked
-        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 in a raw
-
-    @property
-    def widgets(self):
-        return iter(self.flextable)
-
-    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:  # FIXME: except without exception specified !
-        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
-            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.parent.addStyleName('dragover')
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        self.parent.removeStyleName('dragover')
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        self.parent.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.getParent(WidgetsPanel, expect=True).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, ClickHandler):
-
-    def __init__(self, host):
-        TabPanel.__init__(self)
-        ClickHandler.__init__(self)
-        self.host = host
-        self.setStyleName('liberviaTabPanel')
-        self.addStyleName('mainTabPanel')
-        Window.addWindowResizeListener(self)
-
-        self.tabBar.addTab(u'✚', True)
-
-    def onTabSelected(self, sender, tabIndex):
-        if tabIndex < self.getWidgetCount():
-            TabPanel.onTabSelected(self, sender, tabIndex)
-            return
-        # user clicked the "+" tab
-        default_label = _(u'new tab')
-        try:
-            label = Window.prompt(_(u'Name of the new tab'), default_label)
-            if not label:
-                label = default_label
-        except:  # this happens when the user prevents the page to open the prompt dialog
-            label = default_label
-        self.addWidgetsTab(label, select=True)
-
-    def getCurrentPanel(self):
-        """ Get the panel of the currently selected tab
-
-        @return: WidgetsPanel
-        """
-        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 addTab(self, widget, label, select=False):
-        """Create a new tab for the given widget.
-
-        @param widget (Widget): widget to associate to the tab
-        @param label (unicode): label of the tab
-        @param select (bool): True to select the added tab
-        """
-        TabPanel.add(self, widget, DropTab(self, label), False)
-        if select:
-            self.selectTab(self.getWidgetCount() - 1)
-
-    def addWidgetsTab(self, label, select=False, locked=False):
-        """Create a new tab for containing LiberviaWidgets.
-
-        @param label (unicode): label of the tab
-        @param select (bool): True to select the added tab
-        @param locked (bool): If True, the tab will not be removed when there
-            are no more widget inside. If False, the tab will be removed with
-            the last widget.
-        @return: WidgetsPanel
-        """
-        widgets_panel = WidgetsPanel(self, locked=locked)
-        self.addTab(widgets_panel, label, select)
-        return widgets_panel
-
-    def onWidgetPanelRemove(self, panel):
-        """ Called when a child WidgetsPanel is empty and need to be removed """
-        widget_index = self.getWidgetIndex(panel)
-        self.remove(panel)
-        widgets_count = self.getWidgetCount()
-        self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
-
-
-class ContactLabel(HTML):
-    """Display a contact in HTML, selecting best display (jid/nick/etc)"""
-
-    def __init__(self, host, jid_):
-        # TODO: add a listener for nick changes
-        HTML.__init__(self)
-        self.host = host
-        self.jid = jid_.bare
-        self.nick = self.host.contact_lists[C.PROF_KEY_NONE].getCache(self.jid, "nick")
-        self.alert = False
-        self.refresh()
-        self.setStyleName('contactLabel')
-
-    def refresh(self):
-        alert_html = "<strong>(*)</strong>&nbsp;" if self.alert else ""
-        contact_html = html_tools.html_sanitize(self.nick or unicode(self.jid))
-        html = "%(alert)s%(contact)s" % {'alert': alert_html,
-                                         'contact': contact_html}
-        self.setHTML(html)
-
-    def updateNick(self, new_nick):
-        """Change the current nick
-
-        @param new_nick(unicode): new nick to use
-        """
-        self.nick = new_nick
-        self.refresh()
-
-    def setAlert(self, alert):
-        """Show a visual indicator
-
-        @param alert: True if alert must be shown
-        """
-        self.alert = alert
-        self.refresh()
-
-
-class ContactMenuBar(WidgetMenuBar):
-
-    def onBrowserEvent(self, event):
-        WidgetMenuBar.onBrowserEvent(self, event)
-        event.stopPropagation()  # prevent opening the chat dialog
-
-    @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL
-
-    def setUrl(self, url):
-        """Set the URL of the contact avatar."""
-        self.items[0].setHTML('<img src="%s" />' % url)
-
-
-class ContactBox(VerticalPanel, ClickHandler, DragLabel):
-
-    def __init__(self, parent, jid_):
-        """
-        @param parent (ContactPanel): ContactPanel hosting this box
-        @param jid_ (jid.JID): contact JID
-        """
-        VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
-        ClickHandler.__init__(self)
-        DragLabel.__init__(self, jid_, "CONTACT", parent.host)
-        self.jid = jid_.bare
-        self.label = ContactLabel(parent.host, self.jid)
-        self.avatar = ContactMenuBar(self, parent.host) if parent.handle_menu else Image()
-        self.updateAvatar(parent.host.getAvatarURL(self.jid))
-        self.add(self.avatar)
-        self.add(self.label)
-        self.addClickListener(self)
-
-    def addMenus(self, menu_bar):
-        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': unicode(self.jid)})
-        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': unicode(self.jid)})
-
-    def setAlert(self, alert):
-        """Show a visual indicator
-
-        @param alert: True if alert indicator show be shown"""
-        self.label.setAlert(alert)
-
-    def updateAvatar(self, url):
-        """Update the avatar.
-
-        @param url (unicode): image url
-        """
-        self.avatar.setUrl(url)
-
-    def updateNick(self, new_nick):
-        """Update the nickname.
-
-        @param new_nick (unicode): new nickname to use
-        """
-        self.label.updateNick(new_nick)
-
-    def onClick(self, sender):
-        try:
-            self.parent.onClick(self.jid)
-        except AttributeError:
-            pass
-        else:
-            self.setAlert(False)
--- a/src/browser/sat_browser/blog.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/blog.py	Thu Feb 26 18:10:54 2015 +0100
@@ -41,10 +41,10 @@
 from time import time
 
 import html_tools
-import base_panels
 import dialog
-import base_widget
 import richtext
+import editor_widget
+import libervia_widget
 from constants import Const as C
 from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.tools import jid
@@ -233,7 +233,7 @@
                 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 = editor_widget.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
         self.bubble.addStyleName("bubble")
         try:
             self.toggle_syntax_button.removeFromParent()
@@ -355,7 +355,7 @@
             self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
 
 
-class MicroblogPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget):
+class MicroblogPanel(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget):
     warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know"
     warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"
     # FIXME: all the generic parts must be moved to quick_frontends
@@ -367,7 +367,7 @@
         """
         self.setAcceptedGroup(accepted_groups)
         quick_widgets.QuickWidget.__init__(self, host, self.target, C.PROF_KEY_NONE)
-        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
+        libervia_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
         self.entries = {}
         self.comments = {}
         self.selected_entry = None
@@ -735,7 +735,7 @@
         return False
 
 
-base_widget.LiberviaWidget.addDropKey("GROUP", MicroblogPanel.onGroupDrop)
+libervia_widget.LiberviaWidget.addDropKey("GROUP", MicroblogPanel.onGroupDrop)
 
 # Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group
-base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, None))
+libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, None))
--- a/src/browser/sat_browser/chat.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/chat.py	Thu Feb 26 18:10:54 2015 +0100
@@ -39,11 +39,12 @@
 from time import time
 
 import html_tools
-import base_panels
-import panels
+import libervia_widget
+import base_panel
+import contact_panel
+import editor_widget
 import card_game
 import radiocol
-import base_widget
 import contact_list
 from constants import Const as C
 import plugin_xep_0085
@@ -73,7 +74,7 @@
         self.setStyleName('chatText')
 
 
-class Chat(QuickChat, base_widget.LiberviaWidget, KeyboardHandler):
+class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler):
 
     def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
         """Panel used for conversation (one 2 one or group chat)
@@ -91,19 +92,19 @@
             host.plugins['otr'].infoTextCallback(target, cb)
         header_info = header_info_cb if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
 
-        base_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True)
+        libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True)
         self._body = AbsolutePanel()
         self._body.setStyleName('chatPanel_body')
         chat_area = HorizontalPanel()
         chat_area.setStyleName('chatArea')
         if type_ == C.CHAT_GROUP:
-            self.occupants_list = base_panels.OccupantsList()
+            self.occupants_list = contact_panel.OccupantsList()
             self.occupants_initialised = False
             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)
+        self.content_scroll = base_panel.ScrollPanelWrapper(self.content)
         chat_area.add(self.content_scroll)
         chat_area.setCellWidth(self.content_scroll, '100%')
         self.vpanel.add(self._body)
@@ -153,7 +154,7 @@
         if hasattr(self, 'message_box'):
             self.message_box.setVisible(enable_box)
         elif enable_box:
-            self.message_box = panels.MessageBox(self.host)
+            self.message_box = editor_widget.MessageBox(self.host)
             self.message_box.onSelectedChange(self)
             self.message_box.addKeyboardListener(self)
             self.vpanel.add(self.message_box)
@@ -207,7 +208,7 @@
         self.state_machine._onEvent("active")
 
     def onQuit(self):
-        base_widget.LiberviaWidget.onQuit(self)
+        libervia_widget.LiberviaWidget.onQuit(self)
         if self.type == C.CHAT_GROUP:
             self.host.bridge.call('mucLeave', None, unicode(self.target.bare))
 
@@ -363,4 +364,4 @@
 
 
 quick_widgets.register(QuickChat, Chat)
-base_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
+libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
--- a/src/browser/sat_browser/contact_group.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/contact_group.py	Thu Feb 26 18:10:54 2015 +0100
@@ -28,8 +28,8 @@
 
 import dialog
 import list_manager
+import contact_panel
 import contact_list
-import base_panels
 
 
 unicode = str  # FIXME: pyjamas workaround
@@ -186,13 +186,13 @@
         """Add the contact list to the DockPanel."""
         self.toggle = Button("", self.toggleContacts)
         self.toggle.addStyleName("toggleAssignedContacts")
-        self.contacts = base_panels.ContactsPanel(self.host)
+        self.contacts = contact_panel.ContactsPanel(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
+        panel = VerticalPanel()
+        panel.add(self.toggle)
+        panel.add(self.contacts)
+        return panel
 
     def toggleContacts(self, sender=None, showAll=None):
         """Toggle the button to show contacts and the contact list.
--- a/src/browser/sat_browser/contact_list.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/contact_list.py	Thu Feb 26 18:10:54 2015 +0100
@@ -31,8 +31,8 @@
 from __pyjamas__ import doc
 
 from constants import Const as C
-import base_widget
-import base_panels
+import libervia_widget
+import contact_panel
 import blog
 import chat
 
@@ -70,7 +70,7 @@
     widget.presence_style = style
 
 
-class GroupLabel(base_widget.DragLabel, Label, ClickHandler):
+class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler):
     def __init__(self, host, group):
         """
 
@@ -80,7 +80,7 @@
         self.group = group
         Label.__init__(self, group)  # , Element=DOM.createElement('div')
         self.setStyleName('group')
-        base_widget.DragLabel.__init__(self, group, "GROUP", host)
+        libervia_widget.DragLabel.__init__(self, group, "GROUP", host)
         ClickHandler.__init__(self)
         self.addClickListener(self)
 
@@ -133,13 +133,13 @@
         return self._groups
 
 
-class ContactsPanel(base_panels.ContactsPanel):
+class ContactsPanel(contact_panel.ContactsPanel):
     """The contact list that is displayed on the left side."""
 
     def __init__(self, parent):
         def on_click(contact_jid):
             self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE)
-        base_panels.ContactsPanel.__init__(self, parent, on_click=on_click, handle_menu=True)
+        contact_panel.ContactsPanel.__init__(self, parent, on_click=on_click, handle_menu=True)
 
     def setState(self, jid_, type_, state):
         """Change the appearance of the contact, according to the state
@@ -161,12 +161,12 @@
             contact_box.setAlert(state)
 
 
-class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler):
+class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler):
 
     def __init__(self, host, text):
         Label.__init__(self, text)  # , Element=DOM.createElement('div')
         self.setStyleName('contactTitle')
-        base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host)
+        libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host)
         ClickHandler.__init__(self)
         self.addClickListener(self)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_panel.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,215 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+""" Contacts / jids related panels """
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import jid
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+
+import html_tools
+import contact_widget
+from constants import Const as C
+
+
+# FIXME: must be removed
+class Occupant(HTML):
+    """Occupant of a MUC room"""
+
+    def __init__(self, nick, state=None, special=""):
+        """
+        @param nick: the user nickname
+        @param state: the user chate state (XEP-0085)
+        @param special: a string of symbols (e.g: for activities)
+        """
+        HTML.__init__(self, StyleName="occupant")
+        self.nick = nick
+        self._state = state
+        self.special = special
+        self._refresh()
+
+    def __str__(self):
+        return self.nick
+
+    def setState(self, state):
+        self._state = state
+        self._refresh()
+
+    def addSpecial(self, special):
+        """@param special: unicode"""
+        if special not in self.special:
+            self.special += special
+            self._refresh()
+
+    def removeSpecials(self, special):
+        """@param special: unicode or list"""
+        if not isinstance(special, list):
+            special = [special]
+        for symbol in special:
+            self.special = self.special.replace(symbol, "")
+            self._refresh()
+
+    def _refresh(self):
+        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
+        special = "" if len(self.special) == 0 else " %s" % self.special
+        self.setHTML("%s%s%s" % (html_tools.html_sanitize(self.nick), special, state))
+
+
+class ContactsPanel(VerticalPanel):
+    """ContactList graphic representation
+
+    Special features like popup menu panel or changing the contact states must be done in a sub-class.
+    """
+
+    def __init__(self, parent, on_click=None, handle_menu=True):
+        """
+        @param on_click (callable): click callback (used if ContactBox is created)
+        @param handle_menu (bool): if True, bind a popup menu to the avatar (used if ContactBox is created)
+        """ # FIXME
+        VerticalPanel.__init__(self)
+        self._parent = parent
+        self.host = parent.host
+        self._contacts = {} # entity jid to ContactBox map
+        self.click_listener = None
+        self.handle_menu = handle_menu
+
+        if on_click is not None:
+            self.onClick = on_click
+
+    def display(self, jids):
+        """Display a contact in the list.
+
+        @param jids (list[jid.JID]): jids to display (the order is kept)
+        @param name (unicode): optional name of the contact
+        """
+        # FIXME: we do a full clear and add boxes after, we should only remove recently hidden boxes and add new ones, and re-order
+        current = [box.jid for box in self.children if isinstance(box, contact_widget.ContactBox)]
+        if current == jids:
+            # the display doesn't change
+            return
+        self.clear()
+        for jid_ in jids:
+            assert isinstance(jid_, jid.JID)
+            box = self.getContactBox(jid_)
+            VerticalPanel.append(self, box)
+
+    def isContactPresent(self, contact_jid):
+        """Return True if a contact is present in the panel"""
+        return contact_jid in self._contacts
+
+    def getContacts(self):
+        return self._contacts
+
+    def getContactBox(self, contact_jid):
+        """get the Contactbox of a contact
+
+        if the Contactbox doesn't exists, it will be created
+        @param contact_jid (jid.JID): the contact
+        @return: ContactBox instance
+        """
+        try:
+            return self._contacts[contact_jid.bare]
+        except KeyError:
+            box = contact_widget.ContactBox(self, contact_jid)
+            self._contacts[contact_jid.bare] = box
+            return box
+
+    def updateAvatar(self, jid_, url):
+        """Update the avatar of the given contact
+
+        @param jid_ (jid.JID): contact jid
+        @param url (unicode): image url
+        """
+        try:
+            self.getContactBox(jid_).updateAvatar(url)
+        except TypeError:
+            pass
+
+    def updateNick(self, jid_, new_nick):
+        """Update the avatar of the given contact
+
+        @param jid_ (jid.JID): contact jid
+        @param new_nick (unicode): new nick of the contact
+        """
+        try:
+            self.getContactBox(jid_).updateNick(new_nick)
+        except TypeError:
+            pass
+
+
+
+# FIXME: must be removed and ContactsPanel must be used instead
+class OccupantsList(AbsolutePanel):
+    """Panel user to show occupants of a room"""
+
+    def __init__(self):
+        AbsolutePanel.__init__(self)
+        self.occupants_list = {}
+        self.setStyleName('occupantsList')
+
+    def addOccupant(self, nick):
+        if nick in self.occupants_list:
+            return
+        _occupant = Occupant(nick)
+        self.occupants_list[nick] = _occupant
+        self.add(_occupant)
+
+    def removeOccupant(self, nick):
+        try:
+            self.remove(self.occupants_list[nick])
+        except KeyError:
+            log.error("trying to remove an unexisting nick")
+
+    def getOccupantBox(self, nick):
+        """Get the widget element of the given nick.
+
+        @return: Occupant
+        """
+        try:
+            return self.occupants_list[nick]
+        except KeyError:
+            return None
+
+    def clear(self):
+        self.occupants_list.clear()
+        AbsolutePanel.clear(self)
+
+    def updateSpecials(self, occupants=[], html=""):
+        """Set the specified html "symbol" to the listed occupants,
+        and eventually remove it from the others (if they got it).
+        This is used for example to visualize who is playing a game.
+        @param occupants: list of the occupants that need the symbol
+        @param html: unicode symbol (actually one character or more)
+        or a list to assign different symbols of the same family.
+        """
+        index = 0
+        special = html
+        for occupant in self.occupants_list.keys():
+            if occupant in occupants:
+                if isinstance(html, list):
+                    special = html[index]
+                    index = (index + 1) % len(html)
+                self.occupants_list[occupant].addSpecial(special)
+            else:
+                self.occupants_list[occupant].removeSpecials(html)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_widget.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,136 @@
+#!/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.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Image import Image
+from pyjamas.ui.ClickListener import ClickHandler
+from constants import Const as C
+import html_tools
+import base_widget
+import libervia_widget
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+class ContactLabel(HTML):
+    """Display a contact in HTML, selecting best display (jid/nick/etc)"""
+
+    def __init__(self, host, jid_):
+        # TODO: add a listener for nick changes
+        HTML.__init__(self)
+        self.host = host
+        self.jid = jid_.bare
+        self.nick = self.host.contact_lists[C.PROF_KEY_NONE].getCache(self.jid, "nick")
+        self.alert = False
+        self.refresh()
+        self.setStyleName('contactLabel')
+
+    def refresh(self):
+        alert_html = "<strong>(*)</strong>&nbsp;" if self.alert else ""
+        contact_html = html_tools.html_sanitize(self.nick or unicode(self.jid))
+        html = "%(alert)s%(contact)s" % {'alert': alert_html,
+                                         'contact': contact_html}
+        self.setHTML(html)
+
+    def updateNick(self, new_nick):
+        """Change the current nick
+
+        @param new_nick(unicode): new nick to use
+        """
+        self.nick = new_nick
+        self.refresh()
+
+    def setAlert(self, alert):
+        """Show a visual indicator
+
+        @param alert: True if alert must be shown
+        """
+        self.alert = alert
+        self.refresh()
+
+
+class ContactMenuBar(base_widget.WidgetMenuBar):
+
+    def onBrowserEvent(self, event):
+        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
+        event.stopPropagation()  # prevent opening the chat dialog
+
+    @classmethod
+    def getCategoryHTML(cls, menu_name_i18n, type_):
+        return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL
+
+    def setUrl(self, url):
+        """Set the URL of the contact avatar."""
+        self.items[0].setHTML('<img src="%s" />' % url)
+
+
+class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel):
+
+    def __init__(self, parent, jid_):
+        """
+        @param parent (ContactPanel): ContactPanel hosting this box
+        @param jid_ (jid.JID): contact JID
+        """
+        VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
+        ClickHandler.__init__(self)
+        libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", parent.host)
+        self.jid = jid_.bare
+        self.label = ContactLabel(parent.host, self.jid)
+        self.avatar = ContactMenuBar(self, parent.host) if parent.handle_menu else Image()
+        self.updateAvatar(parent.host.getAvatarURL(self.jid))
+        self.add(self.avatar)
+        self.add(self.label)
+        self.addClickListener(self)
+
+    def addMenus(self, menu_bar):
+        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': unicode(self.jid)})
+        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': unicode(self.jid)})
+
+    def setAlert(self, alert):
+        """Show a visual indicator
+
+        @param alert: True if alert indicator show be shown"""
+        self.label.setAlert(alert)
+
+    def updateAvatar(self, url):
+        """Update the avatar.
+
+        @param url (unicode): image url
+        """
+        self.avatar.setUrl(url)
+
+    def updateNick(self, new_nick):
+        """Update the nickname.
+
+        @param new_nick (unicode): new nickname to use
+        """
+        self.label.updateNick(new_nick)
+
+    def onClick(self, sender):
+        try:
+            self.parent.onClick(self.jid)
+        except AttributeError:
+            pass
+        else:
+            self.setAlert(False)
--- a/src/browser/sat_browser/dialog.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/dialog.py	Thu Feb 26 18:10:54 2015 +0100
@@ -40,7 +40,7 @@
 from pyjamas.ui.MouseListener import MouseWheelHandler
 from pyjamas import Window
 
-import base_panels
+import base_panel
 
 
 # List here the patterns that are not allowed in contact group names
@@ -207,7 +207,7 @@
         self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC)
         self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
 
-        self.stack_panel = base_panels.ToggleStackPanel(Width="100%")
+        self.stack_panel = base_panel.ToggleStackPanel(Width="100%")
         self.stack_panel.add(self.room_panel, visible=visible[0])
         self.stack_panel.add(self.contact_panel, visible=visible[1])
         self.stack_panel.addStackChangeListener(self)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/editor_widget.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,390 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import strings
+
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+
+import html_tools
+
+
+class MessageBox(TextArea):
+    """A basic text area for entering messages"""
+
+    def __init__(self, host):
+        TextArea.__init__(self)
+        self.host = host
+        self.size = (0, 0)
+        self.setStyleName('messageBox')
+        self.addKeyboardListener(self)
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+
+    def onBrowserEvent(self, event):
+        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
+        #     so the TextBox's cancelKey doens't work. This is a workaround
+        #     FIXME: fix the bug upstream
+        self.currentEvent = event
+        TextArea.onBrowserEvent(self, event)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        _txt = self.getText()
+
+        def history_cb(text):
+            self.setText(text)
+            Timer(5, lambda timer: self.setCursorPos(len(text)))
+
+        if keycode == KEY_ENTER:
+            if _txt:
+                self.host.selected_widget.onTextEntered(_txt)
+                self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
+            self.setText('')
+            sender.cancelKey()
+        elif keycode == KEY_UP:
+            self.host._updateInputHistory(_txt, -1, history_cb)
+        elif keycode == KEY_DOWN:
+            self.host._updateInputHistory(_txt, +1, history_cb)
+        else:
+            self._onComposing()
+
+    def _onComposing(self):
+        """Callback when the user is composing a text."""
+        self.host.selected_widget.state_machine._onEvent("composing")
+
+    def onMouseUp(self, sender, x, y):
+        size = (self.getOffsetWidth(), self.getOffsetHeight())
+        if size != self.size:
+            self.size = size
+            self.host.resize()
+
+    def onSelectedChange(self, selected):
+        self._selected_cache = selected
+
+
+class BaseTextEditor(object):
+    """Basic definition of a text editor. The method edit gets a boolean parameter which
+    should be set to True when you want to edit the text and False to only display it."""
+
+    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
+        """
+        Remark when inheriting this class: since the setContent method could be
+        overwritten by the child class, you should consider calling this __init__
+        after all the parameters affecting this setContent method have been set.
+        @param content: dict with at least a 'text' key
+        @param strproc: method to be applied on strings to clean the content
+        @param modifiedCb: method to be called when the text has been modified.
+        If this method returns:
+        - True: the modification will be saved and afterEditCb called;
+        - False: the modification won't be saved and afterEditCb called;
+        - None: the modification won't be saved and afterEditCb not called.
+        @param afterEditCb: method to be called when the edition is done
+        """
+        if content is None:
+            content = {'text': ''}
+        assert('text' in content)
+        if strproc is None:
+            def strproc(text):
+                try:
+                    return text.strip()
+                except (TypeError, AttributeError):
+                    return text
+        self.strproc = strproc
+        self.__modifiedCb = modifiedCb
+        self._afterEditCb = afterEditCb
+        self.initialized = False
+        self.edit_listeners = []
+        self.setContent(content)
+
+    def setContent(self, content=None):
+        """Set the editable content. The displayed content, which is set from the child class, could differ.
+        @param content: dict with at least a 'text' key
+        """
+        if content is None:
+            content = {'text': ''}
+        elif not isinstance(content, dict):
+            content = {'text': content}
+        assert('text' in content)
+        self._original_content = {}
+        for key in content:
+            self._original_content[key] = self.strproc(content[key])
+
+    def getContent(self):
+        """Get the current edited or editable content.
+        @return: dict with at least a 'text' key
+        """
+        raise NotImplementedError
+
+    def setOriginalContent(self, content):
+        """Use this method with care! Content initialization should normally be
+        done with self.setContent. This method exists to let you trick the editor,
+        e.g. for self.modified to return True also when nothing has been modified.
+        @param content: dict
+        """
+        self._original_content = content
+
+    def getOriginalContent(self):
+        """
+        @return the original content before modification (dict)
+        """
+        return self._original_content
+
+    def modified(self, content=None):
+        """Check if the content has been modified.
+        Remark: we don't use the direct comparison because we want to ignore empty elements
+        @content: content to be check against the original content or None to use the current content
+        @return: True if the content has been modified.
+        """
+        if content is None:
+            content = self.getContent()
+        # the following method returns True if one non empty element exists in a but not in b
+        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
+        # the following method returns True if the values for the common keys are not equals
+        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
+        # finally the combination of both to return True if a difference is found
+        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
+
+        return diff(content, self._original_content)
+
+    def edit(self, edit, abort=False, sync=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        @param sync: set to True to cancel the edition after the content has been saved somewhere else
+        """
+        if edit:
+            if not self.initialized:
+                self.syncToEditor()  # e.g.: use the selected target and unibox content
+            self.setFocus(True)
+            if abort:
+                content = self.getContent()
+                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
+                    self.edit(False, True, sync)
+                    return
+            if sync:
+                self.syncFromEditor(content)  # e.g.: save the content to unibox
+                return
+        else:
+            if not self.initialized:
+                return
+            content = self.getContent()
+            if abort:
+                self._afterEditCb(content)
+                return
+            if self.__modifiedCb and self.modified(content):
+                result = self.__modifiedCb(content)  # e.g.: send a message or update something
+                if result is not None:
+                    if self._afterEditCb:
+                        self._afterEditCb(content)  # e.g.: restore the display mode
+                    if result is True:
+                        self.setContent(content)
+            elif self._afterEditCb:
+                self._afterEditCb(content)
+
+        self.initialized = True
+
+    def setFocus(self, focus):
+        """
+        @param focus: set to True to focus the editor
+        """
+        raise NotImplementedError
+
+    def syncToEditor(self):
+        pass
+
+    def syncFromEditor(self, content):
+        pass
+
+    def abortEdition(self, content):
+        return True
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        self.edit_listeners.append(listener)
+
+
+class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
+    """Base class for manage a simple text editor."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        """
+        @param content
+        @param modifiedCb
+        @param afterEditCb
+        @param options: dict with the following value:
+        - no_xhtml: set to True to clean any xhtml content.
+        - enhance_display: if True, the display text will be enhanced with strings.addURLToText
+        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
+        - listen_focus: set to True to terminate the edition when the focus is lost.
+        - listen_click: set to True to start the edition when you click on the widget.
+        """
+        self.options = {'no_xhtml': False,
+                        'enhance_display': True,
+                        'listen_keyboard': True,
+                        'listen_focus': False,
+                        'listen_click': False
+                        }
+        if options:
+            self.options.update(options)
+        self.__shift_down = False
+        if self.options['listen_focus']:
+            FocusHandler.__init__(self)
+        if self.options['listen_click']:
+            ClickHandler.__init__(self)
+        KeyboardHandler.__init__(self)
+        strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
+        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
+        self.textarea = self.display = None
+
+    def setContent(self, content=None):
+        BaseTextEditor.setContent(self, content)
+
+    def getContent(self):
+        raise NotImplementedError
+
+    def edit(self, edit, abort=False, sync=False):
+        BaseTextEditor.edit(self, edit)
+        if edit:
+            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
+                self.textarea.addFocusListener(self)
+            if self.options['listen_click']:
+                self.display.clearClickListener()
+            if self not in self.textarea._keyboardListeners:
+                self.textarea.addKeyboardListener(self)
+        else:
+            self.setDisplayContent()
+            if self.options['listen_focus']:
+                try:
+                    self.textarea.removeFocusListener(self)
+                except ValueError:
+                    pass
+            if self.options['listen_click'] and self not in self.display._clickListeners:
+                self.display.addClickListener(self)
+            try:
+                self.textarea.removeKeyboardListener(self)
+            except ValueError:
+                pass
+
+    def setDisplayContent(self):
+        text = self._original_content['text']
+        if not self.options['no_xhtml']:
+            text = strings.addURLToImage(text)
+        if self.options['enhance_display']:
+            text = strings.addURLToText(text)
+        self.display.setHTML(html_tools.convertNewLinesToXHTML(text))
+
+    def setFocus(self, focus):
+        raise NotImplementedError
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        for listener in self.edit_listeners:
+            listener(self.textarea, keycode)
+        if not self.options['listen_keyboard']:
+            return
+        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
+            self.__shift_down = True
+            return
+        if keycode == KEY_ENTER:  # finish the edition
+            self.textarea.setFocus(False)
+            if not self.options['listen_focus']:
+                self.edit(False)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        if keycode == KEY_SHIFT:
+            self.__shift_down = False
+
+    def onLostFocus(self, sender):
+        """Finish the edition when focus is lost"""
+        if self.options['listen_focus']:
+            self.edit(False)
+
+    def onClick(self, sender=None):
+        """Start the edition when the widget is clicked"""
+        if self.options['listen_click']:
+            self.edit(True)
+
+    def onBrowserEvent(self, event):
+        if self.options['listen_focus']:
+            FocusHandler.onBrowserEvent(self, event)
+        if self.options['listen_click']:
+            ClickHandler.onBrowserEvent(self, event)
+        KeyboardHandler.onBrowserEvent(self, event)
+
+
+class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        HTML.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = self.display = self
+
+    def getContent(self):
+        text = DOM.getInnerHTML(self.getElement())
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setHTML(self._original_content['text'])
+        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus:
+            self.getElement().focus()
+        else:
+            self.getElement().blur()
+
+
+class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with a TextArea for editing, HTML for display."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        SimplePanel.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = TextArea()
+        self.display = HTML()
+
+    def getContent(self):
+        text = self.textarea.getText()
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setText(self._original_content['text'])
+        self.setWidget(self.textarea if edit else self.display)
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus and self.isAttached():
+            self.textarea.setCursorPos(len(self.textarea.getText()))
+        self.textarea.setFocus(focus)
--- a/src/browser/sat_browser/file_tools.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/file_tools.py	Thu Feb 26 18:10:54 2015 +0100
@@ -20,7 +20,7 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from constants import Const as C
-from sat.core.i18n import D_
+from sat.core.i18n import _, D_
 from pyjamas.ui.FileUpload import FileUpload
 from pyjamas.ui.FormPanel import FormPanel
 from pyjamas import Window
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/libervia_widget.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,831 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""Libervia base widget"""
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _
+from sat.core import exceptions
+from sat_frontends.quick_frontend import quick_widgets
+
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Widget import Widget
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui import HasAlignment
+from pyjamas.ui.DragWidget import DragWidget
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas import DOM
+from pyjamas import Window
+from __pyjamas__ import doc
+
+import dialog
+import base_menu
+import base_widget
+import base_panel
+
+
+# FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell).
+# TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules
+
+
+### Drag n Drop ###
+
+
+class DragLabel(DragWidget):
+
+    def __init__(self, text, type_, host=None):
+        """Base of Drag n Drop mecanism in Libervia
+
+        @param text: data embedded with in drag n drop operation
+        @param type_: type of data that we are dragging
+        @param host: if not None, the host will be use to highlight BorderWidgets
+        """
+        DragWidget.__init__(self)
+        self.host = host
+        self._text = text
+        self.type_ = type_
+
+    def onDragStart(self, event):
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self._text, self.type_))
+        dt.setDragImage(self.getElement(), 15, 15)
+        if self.host is not None:
+            current_panel = self.host.tab_panel.getCurrentPanel()
+            for widget in current_panel.widgets:
+                if isinstance(widget, BorderWidget):
+                    widget.addStyleName('borderWidgetOnDrag')
+
+    def onDragEnd(self, event):
+        if self.host is not None:
+            current_panel = self.host.tab_panel.getCurrentPanel()
+            for widget in current_panel.widgets:
+                if isinstance(widget, BorderWidget):
+                    widget.removeStyleName('borderWidgetOnDrag')
+
+
+class LiberviaDragWidget(DragLabel):
+    """ A DragLabel which keep the widget being dragged as class value """
+    current = None  # widget currently dragged
+
+    def __init__(self, text, type_, widget):
+        DragLabel.__init__(self, text, type_, widget.host)
+        self.widget = widget
+
+    def onDragStart(self, event):
+        LiberviaDragWidget.current = self.widget
+        DragLabel.onDragStart(self, event)
+
+    def onDragEnd(self, event):
+        DragLabel.onDragEnd(self, event)
+        LiberviaDragWidget.current = None
+
+class DropCell(DropWidget):
+    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
+    drop_keys = {}
+
+    def __init__(self, host):
+        DropWidget.__init__(self)
+        self.host = host
+        self.setStyleName('dropCell')
+
+    @classmethod
+    def addDropKey(cls, key, cb):
+        """Add a association between a key and a class to create on drop.
+
+        @param key: key to be associated (e.g. "CONTACT", "CHAT")
+        @param cb: a callable (either a class or method) returning a
+            LiberviaWidget instance
+        """
+        DropCell.drop_keys[key] = cb
+
+    def onDragEnter(self, event):
+        if self == LiberviaDragWidget.current:
+            return
+        self.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
+            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
+            # We check that we are inside widget's box, and we don't remove the style in this case because
+            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
+            # don't want that
+            self.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def _getCellAndRow(self, grid, event):
+        """Return cell and row index where the event is occuring"""
+        cell = grid.getEventTargetCell(event)
+        row = DOM.getParent(cell)
+        return (row.rowIndex, cell.cellIndex)
+
+    def onDrop(self, event):
+        """
+        @raise NoLiberviaWidgetException: something else than a LiberviaWidget
+            has been returned by the callback.
+        """
+        self.removeStyleName('dragover')
+        DOM.eventPreventDefault(event)
+        dt = event.dataTransfer
+        # 'text', 'text/plain', and 'Text' are equivalent.
+        try:
+            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
+            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
+                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
+            # item_type = dt.getData("type")
+            log.debug("message: %s" % item)
+            log.debug("type: %s" % item_type)
+        except:
+            log.debug("no message found")
+            item = '&nbsp;'
+            item_type = None
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            if self == _new_panel:  # We can't drop on ourself
+                return
+            # we need to remove the widget from the panel as it will be inserted elsewhere
+            widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
+            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
+            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
+            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
+                # the dropped widget is the only one in the same row
+                # as the target widget (self), we don't do anything
+                return
+            widgets_panel.removeWidget(_new_panel)
+        elif item_type in self.drop_keys:
+            _new_panel = self.drop_keys[item_type](self.host, item)
+            if not isinstance(_new_panel, LiberviaWidget):
+                raise base_widget.NoLiberviaWidgetException
+        else:
+            log.warning("unmanaged item type")
+            return
+        if isinstance(self, LiberviaWidget):
+            # self.host.unregisterWidget(self) # FIXME
+            self.onQuit()
+            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)"""
+        if isinstance(self, quick_widgets.QuickWidget):
+            self.host.widgets.deleteWidget(self)
+
+
+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 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.parent.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        self.parent.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def onDrop(self, event):
+        DOM.eventPreventDefault(event)
+        self.parent.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.getParent(WidgetsPanel, expect=True).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)
+
+
+### Libervia Widget ###
+
+
+class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
+
+    def __init__(self, parent, host, title, info=None):
+        """
+        @param parent (LiberviaWidget): LiberWidget instance
+        @param host (SatWebFrontend): SatWebFrontend instance
+        @param title (Label, HTML): text widget instance
+        @param info (Widget): text widget instance
+        """
+        AbsolutePanel.__init__(self)
+        self.add(title)
+        if info:
+            # FIXME: temporary design to display the info near the menu
+            button_group_wrapper = HorizontalPanel()
+            button_group_wrapper.add(info)
+        else:
+            button_group_wrapper = SimplePanel()
+        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
+        button_group = base_widget.WidgetMenuBar(parent, host)
+        button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.MenuCmd(parent, 'onSetting'))
+        button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.MenuCmd(parent, 'onClose'))
+        button_group_wrapper.add(button_group)
+        self.add(button_group_wrapper)
+        self.addStyleName('widgetHeader')
+        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
+
+
+class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
+    """Libervia's widget which can replace itself with a dropped widget on DnD"""
+
+    def __init__(self, host, title='', info=None, selectable=False):
+        """Init the widget
+
+        @param host (SatWebFrontend): SatWebFrontend instance
+        @param title (str): title shown in the header of the widget
+        @param info (str, callable): info shown in the header of the widget
+        @param selectable (bool): True is widget can be selected by user
+        """
+        VerticalPanel.__init__(self)
+        DropCell.__init__(self, host)
+        ClickHandler.__init__(self)
+        self._selectable = selectable
+        self._title_id = HTMLPanel.createUniqueId()
+        self._setting_button_id = HTMLPanel.createUniqueId()
+        self._close_button_id = HTMLPanel.createUniqueId()
+        self._title = Label(title)
+        self._title.setStyleName('widgetHeader_title')
+        if info is not None:
+            if isinstance(info, str):
+                self._info = HTML(info)
+            else:  # the info will be set by a callback
+                assert callable(info)
+                self._info = HTML()
+                info(self._info.setHTML)
+            self._info.setStyleName('widgetHeader_info')
+        else:
+            self._info = None
+        header = WidgetHeader(self, host, self._title, self._info)
+        self.add(header)
+        self.setSize('100%', '100%')
+        self.addStyleName('widget')
+        if self._selectable:
+            self.addClickListener(self)
+
+            # FIXME
+            # 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) # FIXME
+
+    def getDebugName(self):
+        return "%s (%s)" % (self, self._title.getText())
+
+    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 (raise an error if not found)
+        @return: the parent/ancestor or None if it has not been found
+        @raise exceptions.InternalError: expect is True and no parent is 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:
+            raise exceptions.InternalError("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 """
+        widgets_panel = self.getParent(WidgetsPanel, expect=True)
+        widgets_panel.removeWidget(self)
+        self.onQuit()
+        self.host.widgets.deleteWidget(self)
+
+    def onQuit(self):
+        """ Called when the widget is actually ending """
+        pass
+
+    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.getParent(WidgetsPanel, expect=True)
+        row, col = widpanel.getIndex(self)
+        body = VerticalPanel()
+
+        # colspan & rowspan
+        colspan = widpanel.getColSpan(row, col)
+        rowspan = widpanel.getRowSpan(row, col)
+
+        def onColSpanChange(value):
+            widpanel.setColSpan(row, col, value)
+
+        def onRowSpanChange(value):
+            widpanel.setRowSpan(row, col, value)
+        colspan_setter = dialog.IntSetter("Columns span", colspan)
+        colspan_setter.addValueChangeListener(onColSpanChange)
+        colspan_setter.setWidth('100%')
+        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
+        rowspan_setter.addValueChangeListener(onRowSpanChange)
+        rowspan_setter.setWidth('100%')
+        body.add(colspan_setter)
+        body.add(rowspan_setter)
+
+        # size
+        width_str = self.getWidth()
+        if width_str.endswith('px'):
+            width = int(width_str[:-2])
+        else:
+            width = 0
+        height_str = self.getHeight()
+        if height_str.endswith('px'):
+            height = int(height_str[:-2])
+        else:
+            height = 0
+
+        def onWidthChange(value):
+            if not value:
+                self.setWidth('100%')
+            else:
+                self.setWidth('%dpx' % value)
+
+        def onHeightChange(value):
+            if not value:
+                self.setHeight('100%')
+            else:
+                self.setHeight('%dpx' % value)
+        width_setter = dialog.IntSetter("width (0=auto)", width)
+        width_setter.addValueChangeListener(onWidthChange)
+        width_setter.setWidth('100%')
+        height_setter = dialog.IntSetter("height (0=auto)", height)
+        height_setter.addValueChangeListener(onHeightChange)
+        height_setter.setHeight('100%')
+        body.add(width_setter)
+        body.add(height_setter)
+
+        # reset
+        def onReset(sender):
+            colspan_setter.setValue(1)
+            rowspan_setter.setValue(1)
+            width_setter.setValue(0)
+            height_setter.setValue(0)
+
+        reset_bt = Button("Reset", onReset)
+        body.add(reset_bt)
+        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
+
+        _dialog = dialog.GenericDialog("Widget setting", body)
+        _dialog.show()
+
+    def setTitle(self, text):
+        """change the title in the header of the widget
+        @param text: text of the new title"""
+        self._title.setText(text)
+
+    def setHeaderInfo(self, text):
+        """change the info in the header of the widget
+        @param text: text of the new title"""
+        try:
+            self._info.setHTML(text)
+        except TypeError:
+            log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
+
+    def isSelectable(self):
+        return self._selectable
+
+    def setSelectable(self, selectable):
+        if not self._selectable:
+            try:
+                self.removeClickListener(self)
+            except ValueError:
+                pass
+        if self.selectable and not self in self._clickListeners:
+            self.addClickListener(self)
+        self._selectable = selectable
+
+    def getWarningData(self):
+        """ Return exposition warning level when this widget is selected and something is sent to it
+        This method should be overriden by children
+        @return: tuple (warning level type/HTML msg). Type can be one of:
+            - PUBLIC
+            - GROUP
+            - ONE2ONE
+            - MISC
+            - NONE
+        """
+        if not self._selectable:
+            log.error("getWarningLevel must not be called for an unselectable widget")
+            raise Exception
+        # TODO: cleaner warning types (more general constants)
+        return ("NONE", None)
+
+    def setWidget(self, widget, scrollable=True):
+        """Set the widget that will be in the body of the LiberviaWidget
+        @param widget: widget to put in the body
+        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
+        if scrollable:
+            _scrollpanelwrapper = base_panel.ScrollPanelWrapper()
+            _scrollpanelwrapper.setStyleName('widgetBody')
+            _scrollpanelwrapper.setWidget(widget)
+            body_wid = _scrollpanelwrapper
+        else:
+            body_wid = widget
+        self.add(body_wid)
+        self.setCellHeight(body_wid, '100%')
+
+    def doDetachChildren(self):
+        # We need to force the use of a panel subclass method here,
+        # for the same reason as doAttachChildren
+        VerticalPanel.doDetachChildren(self)
+
+    def doAttachChildren(self):
+        # We need to force the use of a panel subclass method here, else
+        # the event will not propagate to children
+        VerticalPanel.doAttachChildren(self)
+
+    def matchEntity(self, item):
+        """Check if this widget corresponds to the given entity.
+
+        This method should be overwritten by child classes.
+        @return: True if the widget matches the entity"""
+        raise NotImplementedError
+
+    def addMenus(self, menu_bar):
+        """Add menus to the header.
+
+        This method can be overwritten by child classes.
+        @param menu_bar (GenericMenuBar): menu bar of the widget's header
+        """
+        pass
+
+
+# XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import
+
+
+class WidgetsPanel(base_panel.ScrollPanelWrapper):
+    """The panel wanaging the widgets indide a tab"""
+
+    def __init__(self, host, locked=False):
+        """
+
+        @param host (SatWebFrontend): host instance
+        @param locked (bool): If True, the tab containing self will not be
+            removed when there are no more widget inside self. If False, the
+            tab will be removed with self's last widget.
+        """
+        base_panel.ScrollPanelWrapper.__init__(self)
+        self.setSize('100%', '100%')
+        self.host = host
+        self.locked = locked
+        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 in a raw
+
+    @property
+    def widgets(self):
+        return iter(self.flextable)
+
+    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:  # FIXME: except without exception specified !
+        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
+            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 MainTabPanel(TabPanel, ClickHandler):
+    """The panel managing the tabs"""
+
+    def __init__(self, host):
+        TabPanel.__init__(self)
+        ClickHandler.__init__(self)
+        self.host = host
+        self.setStyleName('liberviaTabPanel')
+        self.addStyleName('mainTabPanel')
+        Window.addWindowResizeListener(self)
+
+        self.tabBar.addTab(u'✚', True)
+
+    def onTabSelected(self, sender, tabIndex):
+        if tabIndex < self.getWidgetCount():
+            TabPanel.onTabSelected(self, sender, tabIndex)
+            return
+        # user clicked the "+" tab
+        default_label = _(u'new tab')
+        try:
+            label = Window.prompt(_(u'Name of the new tab'), default_label)
+            if not label:
+                label = default_label
+        except:  # this happens when the user prevents the page to open the prompt dialog
+            label = default_label
+        self.addWidgetsTab(label, select=True)
+
+    def getCurrentPanel(self):
+        """ Get the panel of the currently selected tab
+
+        @return: WidgetsPanel
+        """
+        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 addTab(self, widget, label, select=False):
+        """Create a new tab for the given widget.
+
+        @param widget (Widget): widget to associate to the tab
+        @param label (unicode): label of the tab
+        @param select (bool): True to select the added tab
+        """
+        TabPanel.add(self, widget, DropTab(self, label), False)
+        if select:
+            self.selectTab(self.getWidgetCount() - 1)
+
+    def addWidgetsTab(self, label, select=False, locked=False):
+        """Create a new tab for containing LiberviaWidgets.
+
+        @param label (unicode): label of the tab
+        @param select (bool): True to select the added tab
+        @param locked (bool): If True, the tab will not be removed when there
+            are no more widget inside. If False, the tab will be removed with
+            the last widget.
+        @return: WidgetsPanel
+        """
+        widgets_panel = WidgetsPanel(self, locked=locked)
+        self.addTab(widgets_panel, label, select)
+        return widgets_panel
+
+    def onWidgetPanelRemove(self, panel):
+        """ Called when a child WidgetsPanel is empty and need to be removed """
+        widget_index = self.getWidgetIndex(panel)
+        self.remove(panel)
+        widgets_count = self.getWidgetCount()
+        self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
+
+
--- a/src/browser/sat_browser/list_manager.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/list_manager.py	Thu Feb 26 18:10:54 2015 +0100
@@ -26,8 +26,9 @@
 from pyjamas.ui.DragWidget import DragWidget
 from pyjamas.Timer import Timer
 
-import base_panels
+import base_panel
 import base_widget
+import libervia_widget
 
 from sat_frontends.tools import jid
 
@@ -250,7 +251,7 @@
         @param callback (callable): common callback for all menu items, takes in
             the button widget and the item key.
         """
-        self.popup_menu = base_panels.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]})
+        self.popup_menu = base_panel.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]})
 
 
 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget):
@@ -377,7 +378,7 @@
 DELETE = 3
 
 
-class ListPanel(FlowPanel, base_widget.DropCell):
+class ListPanel(FlowPanel, libervia_widget.DropCell):
     """Panel used for listing items sharing the same key. The key is showed as
     a Button to which you can bind a popup menu and the items are represented
     with a sequence of DragAutoCompleteTextBox."""
@@ -401,7 +402,7 @@
                     "CONTACT_TITLE": lambda host, item: self.addItem('@@'),
                     "CONTACT_TEXTBOX": setTargetDropCell
                     }
-        base_widget.DropCell.__init__(self, None)
+        libervia_widget.DropCell.__init__(self, None)
         self.drop_keys = drop_cbs
         self.style = style
         self.addStyleName(self.style["keyPanel"])
@@ -411,7 +412,7 @@
 
     def onDrop(self, event):
         try:
-            base_widget.DropCell.onDrop(self, event)
+            libervia_widget.DropCell.onDrop(self, event)
         except base_widget.NoLiberviaWidgetException:
             pass
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/main_panel.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,311 @@
+#!/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/>.
+
+"""Panels used as main basis"""
+
+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
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.Timer import Timer
+from pyjamas import Window
+from __pyjamas__ import doc
+
+
+import base_menu
+import menu
+import dialog
+import base_widget
+import libervia_widget
+import editor_widget
+import contact_list
+from constants import Const as C
+
+
+### Warning notification (visibility of message, and other warning data) ###
+
+
+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)
+
+
+### Status ###
+
+
+class StatusPanel(editor_widget.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
+        editor_widget.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(editor_widget.HTMLTextEditor.getContent(self))
+
+    def setContent(self, content):
+        content = self.__cleanContent(content)
+        editor_widget.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 PresenceStatusMenuBar(base_widget.WidgetMenuBar):
+    def __init__(self, parent):
+        styles = {'menu_bar': 'presence-button'}
+        base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles)
+        self.button = self.addCategory(u"◉", u"◉", '')
+        for presence, presence_i18n in C.PRESENCE.items():
+            html = u'<span class="%s">◉</span> %s' % (contact_list.buildPresenceStyle(presence), presence_i18n)
+            self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True)
+        self.parent_panel = parent
+
+    def changePresenceCb(self, presence):
+        """Callback to notice the backend of a new presence set by the user.
+        @param presence (str): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
+        """
+        self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status)
+
+    @classmethod
+    def getCategoryHTML(cls, menu_name_i18n, type_):
+        return menu_name_i18n
+
+
+class PresenceStatusPanel(HorizontalPanel, ClickHandler):
+
+    def __init__(self, host, presence="", status=""):
+        self.host = host
+        HorizontalPanel.__init__(self, Width='100%')
+        self.menu = PresenceStatusMenuBar(self)
+        self.status_panel = StatusPanel(host, status=status)
+        self.setPresence(presence)
+
+        panel = HorizontalPanel()
+        panel.add(self.menu)
+        panel.add(self.status_panel)
+        panel.setCellVerticalAlignment(self.menu, '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_list.setPresenceStyle(self.menu.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)
+
+
+### Panels managing the main area ###
+
+
+class MainPanel(AbsolutePanel):
+    """The panel which take the whole screen"""
+
+    def __init__(self, host):
+        self.host = host
+        AbsolutePanel.__init__(self)
+
+        # menu
+        self.menu = menu.MainMenuPanel(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)
+
+        # tabs
+        self.tab_panel = libervia_widget.MainTabPanel(host)
+        self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True)
+
+        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 addContactList(self, contact_list):
+        self._contacts.add(contact_list)
+
+    def _contactsSwitch(self, btn=None):
+        """ (Un)hide contacts panel """
+        if btn is None:
+            btn = self.contacts_switch
+        clist = self.host.contact_list
+        clist.setVisible(not clist.getVisible())
+        btn.setText(u"«" if clist.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()
+        self.host.contact_panel.refresh()
+
+
--- a/src/browser/sat_browser/menu.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/menu.py	Thu Feb 26 18:10:54 2015 +0100
@@ -32,7 +32,7 @@
 import file_tools
 import xmlui
 import chat
-import panels
+import widget
 import dialog
 import contact_group
 import base_menu
@@ -100,7 +100,7 @@
 
     # General menu
     def onWebWidget(self):
-        web_widget = self.host.displayWidget(panels.WebPanel, C.WEB_PANEL_DEFAULT_URL)
+        web_widget = self.host.displayWidget(widget.WebWidget, C.WEB_PANEL_DEFAULT_URL)
         self.host.setSelected(web_widget)
 
     def onDisconnect(self):
--- a/src/browser/sat_browser/panels.py	Thu Feb 26 13:10:46 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,565 +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
-
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.Frame import Frame
-from pyjamas.Timer import Timer
-from pyjamas import Window
-from pyjamas import DOM
-from __pyjamas__ import doc
-
-
-import base_panels
-import base_menu
-import menu
-import dialog
-import base_widget
-import contact_list
-from constants import Const as C
-from sat_frontends.quick_frontend import quick_widgets
-
-
-# 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.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true'
-#         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)
-
-    def onBrowserEvent(self, event):
-        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
-        #     so the TextBox's cancelKey doens't work. This is a workaround
-        #     FIXME: fix the bug upstream
-        self.currentEvent = event
-        TextArea.onBrowserEvent(self, event)
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        _txt = self.getText()
-
-        def history_cb(text):
-            self.setText(text)
-            Timer(5, lambda timer: self.setCursorPos(len(text)))
-
-        if keycode == KEY_ENTER:
-            if _txt:
-                self.host.selected_widget.onTextEntered(_txt)
-                self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
-            self.setText('')
-            sender.cancelKey()
-        elif keycode == KEY_UP:
-            self.host._updateInputHistory(_txt, -1, history_cb)
-        elif keycode == KEY_DOWN:
-            self.host._updateInputHistory(_txt, +1, history_cb)
-        else:
-            self._onComposing()
-
-    def _onComposing(self):
-        """Callback when the user is composing a text."""
-        self.host.selected_widget.state_machine._onEvent("composing")
-
-    def onMouseUp(self, sender, x, y):
-        size = (self.getOffsetWidth(), self.getOffsetHeight())
-        if size != self.size:
-            self.size = size
-            self.host.resize()
-
-    def onSelectedChange(self, selected):
-        self._selected_cache = selected
-
-
-# class 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')
-#         # FIXME
-#         # 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): # FIXME
-#         #     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 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 PresenceStatusMenuBar(base_widget.WidgetMenuBar):
-    def __init__(self, parent):
-        styles = {'menu_bar': 'presence-button'}
-        base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles)
-        self.button = self.addCategory(u"◉", u"◉", '')
-        for presence, presence_i18n in C.PRESENCE.items():
-            html = u'<span class="%s">◉</span> %s' % (contact_list.buildPresenceStyle(presence), presence_i18n)
-            self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True)
-        self.parent_panel = parent
-
-    def changePresenceCb(self, presence):
-        """Callback to notice the backend of a new presence set by the user.
-        @param presence (str): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
-        """
-        self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status)
-
-    @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return menu_name_i18n
-
-
-class PresenceStatusPanel(HorizontalPanel, ClickHandler):
-
-    def __init__(self, host, presence="", status=""):
-        self.host = host
-        HorizontalPanel.__init__(self, Width='100%')
-        self.menu = PresenceStatusMenuBar(self)
-        self.status_panel = StatusPanel(host, status=status)
-        self.setPresence(presence)
-
-        panel = HorizontalPanel()
-        panel.add(self.menu)
-        panel.add(self.status_panel)
-        panel.setCellVerticalAlignment(self.menu, '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_list.setPresenceStyle(self.menu.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 WebPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget):
-    """ (mini)browser like widget """
-
-    def __init__(self, host, target, show_url=True, profiles=None):
-        """
-        @param host: SatWebFrontend instance
-        @param target: url to open
-        """
-        quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE)
-        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(target or "")
-        self._url.setWidth('100%')
-        if show_url:
-            hpanel = HorizontalPanel()
-            hpanel.add(self._url)
-            btn = Button("Go", self.onUrlClick)
-            hpanel.setCellWidth(self._url, "100%")
-            hpanel.add(btn)
-            self._vpanel.add(hpanel)
-            self._vpanel.setCellHeight(hpanel, '20px')
-        self._frame = Frame(target 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):
-        url = self._url.getText()
-        scheme_end = url.find(':')
-        scheme = "" if scheme_end == -1 else url[:scheme_end]
-        if scheme not in C.WEB_PANEL_SCHEMES:
-            url = "http://" + url
-        self._frame.setUrl(url)
-
-
-class MainPanel(AbsolutePanel):
-
-    def __init__(self, host):
-        self.host = host
-        AbsolutePanel.__init__(self)
-
-        # menu
-        self.menu = menu.MainMenuPanel(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)
-
-        # tabs
-        self.tab_panel = base_widget.MainTabPanel(host)
-        self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True)
-
-        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 addContactList(self, contact_list):
-        self._contacts.add(contact_list)
-
-    def _contactsSwitch(self, btn=None):
-        """ (Un)hide contacts panel """
-        if btn is None:
-            btn = self.contacts_switch
-        clist = self.host.contact_list
-        clist.setVisible(not clist.getVisible())
-        btn.setText(u"«" if clist.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()
-        self.host.contact_panel.refresh()
--- a/src/browser/sat_browser/richtext.py	Thu Feb 26 13:10:46 2015 +0100
+++ b/src/browser/sat_browser/richtext.py	Thu Feb 26 18:10:54 2015 +0100
@@ -34,14 +34,15 @@
 
 from constants import Const as C
 import dialog
-import base_panels
+import base_panel
+import editor_widget
 import list_manager
 import html_tools
 import blog
 import chat
 
 
-class RichTextEditor(base_panels.BaseTextEditor, FlexTable):
+class RichTextEditor(editor_widget.BaseTextEditor, FlexTable):
     """Panel for the rich text editor."""
 
     def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None):
@@ -64,7 +65,7 @@
         if isinstance(style, dict):
             self.style.update(style)
         self._prepareUI()
-        base_panels.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
+        editor_widget.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
 
     def __readOptions(self, options):
         """Set the internal flags according to the given options."""
@@ -92,7 +93,7 @@
     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)
+        editor_widget.BaseTextEditor.addEditListener(self, listener)
         if hasattr(self, 'display'):
             self.display.addEditListener(listener)
 
@@ -109,7 +110,7 @@
         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
+            self.display = editor_widget.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'):
@@ -126,7 +127,7 @@
             return
 
         if not self.no_title and not hasattr(self, 'title_panel'):
-            self.title_panel = base_panels.TitlePanel()
+            self.title_panel = base_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)
@@ -286,10 +287,10 @@
         """
         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
+        editor_widget.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)
+            return  # self.abortEdition is called by editor_widget.BaseTextEditor.edit
+        self.setWysiwyg(False, init=True)  # after editor_widget.BaseTextEditor (it affects self.getContent)
         if sync:
             return
         # the following must NOT be done at each UI refresh!
@@ -319,7 +320,7 @@
             self.display.edit(False)
 
     def setDisplayContent(self):
-        """Set the content of the base_panels.HTMLTextEditor which is used for display/wysiwyg"""
+        """Set the content of the editor_widget.HTMLTextEditor which is used for display/wysiwyg"""
         content = self._original_content
         text = content['text']
         if 'title' in content and content['title']:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/widget.py	Thu Feb 26 18:10:54 2015 +0100
@@ -0,0 +1,244 @@
+#!/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.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Frame import Frame
+from pyjamas import DOM
+
+
+import dialog
+import libervia_widget
+from constants import Const as C
+from sat_frontends.quick_frontend import quick_widgets
+
+
+# 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.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true'
+#         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 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')
+#         # FIXME
+#         # 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): # FIXME
+#         #     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 WebWidget(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget):
+    """ (mini)browser like widget """
+
+    def __init__(self, host, target, show_url=True, profiles=None):
+        """
+        @param host: SatWebFrontend instance
+        @param target: url to open
+        """
+        quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE)
+        libervia_widget.LiberviaWidget.__init__(self, host)
+        self._vpanel = VerticalPanel()
+        self._vpanel.setSize('100%', '100%')
+        self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
+        self._url.setText(target or "")
+        self._url.setWidth('100%')
+        if show_url:
+            hpanel = HorizontalPanel()
+            hpanel.add(self._url)
+            btn = Button("Go", self.onUrlClick)
+            hpanel.setCellWidth(self._url, "100%")
+            hpanel.add(btn)
+            self._vpanel.add(hpanel)
+            self._vpanel.setCellHeight(hpanel, '20px')
+        self._frame = Frame(target 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):
+        url = self._url.getText()
+        scheme_end = url.find(':')
+        scheme = "" if scheme_end == -1 else url[:scheme_end]
+        if scheme not in C.WEB_PANEL_SCHEMES:
+            url = "http://" + url
+        self._frame.setUrl(url)