changeset 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents 60dfa2f5d61f
children 7cf32aeeebdb
files frontends/src/bridge/DBus.py frontends/src/primitivus/chat.py frontends/src/primitivus/constants.py frontends/src/primitivus/contact_list.py frontends/src/primitivus/keys.py frontends/src/primitivus/primitivus frontends/src/primitivus/profile_manager.py frontends/src/primitivus/progress.py frontends/src/primitivus/widget.py frontends/src/primitivus/xmlui.py frontends/src/quick_frontend/constants.py frontends/src/quick_frontend/quick_app.py frontends/src/quick_frontend/quick_chat.py frontends/src/quick_frontend/quick_chat_list.py frontends/src/quick_frontend/quick_contact_list.py frontends/src/quick_frontend/quick_utils.py frontends/src/quick_frontend/quick_widgets.py frontends/src/tools/jid.py frontends/src/tools/xmlui.py
diffstat 19 files changed, 1579 insertions(+), 965 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/DBus.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/bridge/DBus.py	Wed Dec 10 19:00:09 2014 +0100
@@ -144,8 +144,9 @@
     def confirmationAnswer(self, id, accepted, data, profile):
         return self.db_core_iface.confirmationAnswer(id, accepted, data, profile)
 
-    def delContact(self, entity_jid, profile_key="@DEFAULT@"):
-        return self.db_core_iface.delContact(entity_jid, profile_key)
+    def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None):
+        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.delContact(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def discoInfos(self, entity_jid, profile_key, callback=None, errback=None):
         error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
@@ -161,8 +162,9 @@
     def getConfig(self, section, name):
         return unicode(self.db_core_iface.getConfig(section, name))
 
-    def getContacts(self, profile_key="@DEFAULT@"):
-        return self.db_core_iface.getContacts(profile_key)
+    def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.getContacts(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def getContactsFromGroup(self, group, profile_key="@DEFAULT@"):
         return self.db_core_iface.getContactsFromGroup(group, profile_key)
@@ -255,5 +257,6 @@
     def subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
         return self.db_core_iface.subscription(sub_type, entity, profile_key)
 
-    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
-        return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key)
+    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None):
+        error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
--- a/frontends/src/primitivus/chat.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/chat.py	Wed Dec 10 19:00:09 2014 +0100
@@ -23,11 +23,12 @@
 import urwid
 from urwid_satext import sat_widgets
 from urwid_satext.files_management import FileDialog
+from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.quick_frontend.quick_chat import QuickChat
 from sat_frontends.primitivus.card_game import CardGame
-from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
 from sat_frontends.primitivus.constants import Const as C
 from sat_frontends.primitivus.keys import action_key_map as a_key
+from sat_frontends.primitivus.widget import PrimitivusWidget
 import time
 from sat_frontends.tools.jid  import JID
 
@@ -80,19 +81,27 @@
         return txt_widget
 
 
-class Chat(urwid.WidgetWrap, QuickChat):
+class Chat(PrimitivusWidget, QuickChat):
 
-    def __init__(self, target, host, type_='one2one'):
-        self.target = target
-        QuickChat.__init__(self, target, host, type_)
+    def __init__(self, host, target, type_='one2one', profiles=None):
+        QuickChat.__init__(self, host, target, type_, profiles=profiles)
         self.content = urwid.SimpleListWalker([])
         self.text_list = urwid.ListBox(self.content)
         self.chat_widget = urwid.Frame(self.text_list)
         self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
         self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
         self.pile = urwid.Pile([self.chat_colums])
-        urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile))
-        self.setType(type_)
+        PrimitivusWidget.__init__(self, self.pile, self.target)
+
+        # we must adapt the behavious with the type
+        if type_ == 'one2one':
+            self.historyPrint(profile=self.profile)
+        elif type_ == 'group':
+            if len(self.chat_colums.contents) == 1:
+                present_widget = self._buildPresentList()
+                self.present_panel = sat_widgets.VerticalSeparator(present_widget)
+                self._appendPresentPanel()
+
         self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00  %Y")) #struct_time of day changing time
         self.show_timestamp = True
         self.show_short_nick = False
@@ -104,9 +113,9 @@
             if self.type == 'group':
                 widgets = [widget for (widget, options) in self.chat_colums.contents]
                 if self.present_panel in widgets:
-                    self.__removePresentPanel()
+                    self._removePresentPanel()
                 else:
-                    self.__appendPresentPanel()
+                    self._appendPresentPanel()
         elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp
             self.show_timestamp = not self.show_timestamp
             for wid in self.content:
@@ -115,10 +124,6 @@
             self.show_short_nick = not self.show_short_nick
             for wid in self.content:
                 wid._invalidate()
-        elif key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
-            show = not isinstance(self._w, sat_widgets.LabelLine)
-            self.showDecoration(show)
-            self._invalidate()
         elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance
             if self.subject:
                 self.show_title = (self.show_title + 1) % 3
@@ -130,7 +135,6 @@
                     self.chat_widget.header = None
                 self._invalidate()
 
-
         return super(Chat, self).keypress(size, key)
 
     def getMenu(self):
@@ -141,56 +145,20 @@
             game = _("Game")
             menu.addMenu(game, "Tarot", self.onTarotRequest)
         elif self.type == 'one2one':
-            self.host.addMenus(menu, C.MENU_SINGLE, {'jid': unescapePrivate(self.target)})
+            self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target})
             menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest)
         return menu
 
-    def setType(self, type_):
-        QuickChat.setType(self, type_)
-        if type_ == 'one2one':
-            self.historyPrint(profile=self.host.profile)
-        elif type_ == 'group':
-            if len(self.chat_colums.contents) == 1:
-                present_widget = self.__buildPresentList()
-                self.present_panel = sat_widgets.VerticalSeparator(present_widget)
-                self.__appendPresentPanel()
-
-    def __getDecoration(self, widget):
-        return sat_widgets.LabelLine(widget, self.__getSurrendedText())
-
-    def __getSurrendedText(self):
-        """Get the text to be displayed as the dialog title."""
-        if not hasattr(self, "surrended_text"):
-            self.__setSurrendedText()
-        return self.surrended_text
-
-    def __setSurrendedText(self, state=None):
-        """Set the text to be displayed as the dialog title
-        @param stat: chat state of the contact
-        """
-        text = unicode(unescapePrivate(self.target))
-        if state:
-            text += " (" + state + ")"
-        self.surrended_text = sat_widgets.SurroundedText(text)
-
-    def showDecoration(self, show=True):
-        """Show/Hide the decoration around the chat window"""
-        if show:
-            main_widget = self.__getDecoration(self.pile)
-        else:
-            main_widget = self.pile
-        self._w = main_widget
-
-    def updateChatState(self, state, nick=None):
-        """Set the chat state (XEP-0085) of the contact. Leave nick to None
-        to set the state for a one2one conversation, or give a nickname or
-        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
-        @param state: the new chat state
-        @param nick: None for one2one, the MUC user nick or C.ALL_OCCUPANTS
-        """
-        if nick:
-            assert(self.type == 'group')
-            occupants = self.occupants if nick == C.ALL_OCCUPANTS else [nick]
+    def updateChatState(self, from_jid, state):
+        if self.type == C.CHAT_GROUP:
+            if from_jid == C.ENTITY_ALL:
+                occupants = self.occupants
+            else:
+                nick = from_jid.resource
+                if not nick:
+                    log.debug("no nick found for chatstate")
+                    return
+                occupants = [nick]
             options = self.present_wid.getAllValues()
             for index in xrange(0, len(options)):
                 nick = options[index].value
@@ -199,46 +167,43 @@
             self.present_wid.changeValues(options)
             self.host.redraw()
         else:
-            assert(self.type == 'one2one')
-            self.__setSurrendedText(state)
-            self.showDecoration()
-            self.host.redraw()
+            self.title_dynamic = '({})'.format(state)
 
     def _presentClicked(self, list_wid, clicked_wid):
-        assert(self.type == 'group')
+        assert self.type == 'group'
         nick = clicked_wid.getValue().value
         if nick == self.getUserNick():
-            #We ignore click on our own nick
+            #We ignore clicks on our own nick
             return
-        #we have a click on a nick, we add the private conversation to the contact_list
+        contact_list = self.host.contact_lists[self.profile]
         full_jid = JID("%s/%s" % (self.target.bare, nick))
-        new_jid = escapePrivate(full_jid)
-        if new_jid not in self.host.contact_list:
-            self.host.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
+
+        #we have a click on a nick, we need to create the widget if it doesn't exists
+        self.getOrCreatePrivateWidget(full_jid)
 
         #now we select the new window
-        self.host.contact_list.setFocus(full_jid, True)
+        contact_list.setFocus(full_jid, True)
 
-    def __buildPresentList(self):
+    def _buildPresentList(self):
         self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked)
         return self.present_wid
 
-    def __appendPresentPanel(self):
+    def _appendPresentPanel(self):
         self.chat_colums.contents.append((self.present_panel,('weight', 2, False)))
 
-    def __removePresentPanel(self):
+    def _removePresentPanel(self):
         for widget, options in self.chat_colums.contents:
             if widget is self.present_panel:
                 self.chat_colums.contents.remove((widget, options))
                 break
 
-    def __appendGamePanel(self, widget):
+    def _appendGamePanel(self, widget):
         assert (len(self.pile.contents) == 1)
         self.pile.contents.insert(0,(widget,('weight', 1)))
         self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1))))
         self.host.redraw()
 
-    def __removeGamePanel(self):
+    def _removeGamePanel(self):
         assert (len(self.pile.contents) == 3)
         del self.pile.contents[0]
         self.host.redraw()
@@ -290,13 +255,16 @@
             self.text_list.focus_position = len(self.content) - 1  # scroll down
         self.host.redraw()
 
+    def onPrivateCreated(self, widget):
+        self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
+
     def printMessage(self, from_jid, msg, profile, timestamp=None):
         assert isinstance(from_jid, JID)
         try:
             jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp)
         except TypeError:
+            # None is returned, the message is managed
             return
-
         new_text = ChatText(self, timestamp, nick, mymess, msg)
 
         if timestamp and self.content:
@@ -357,7 +325,7 @@
         """Configure the chat window to start a game"""
         if game_type=="Tarot":
             self.tarot_wid = CardGame(self, referee, players, self.nick)
-            self.__appendGamePanel(self.tarot_wid)
+            self._appendGamePanel(self.tarot_wid)
 
     def getGame(self, game_type):
         """Return class managing the game type"""
@@ -371,7 +339,7 @@
         if len(self.occupants) != 4:
             self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp))
         else:
-            self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile)
+            self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.profile)
 
     def onSendFileRequest(self, menu):
         # TODO: move this to core with dynamic menus
@@ -388,11 +356,14 @@
             self.host.showDialog(_(u"File has a unicode error in its name, it's not yet managed by SàT"), title=_("Can't send file"), type_="error")
             return
         #FIXME: check last_resource: what if self.target.resource exists ?
-        last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile)
+        last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.profile)
         if last_resource:
             full_jid = JID("%s/%s" % (self.target.bare, last_resource))
         else:
             full_jid = self.target
-        progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.host.profile)
+        progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile)
         self.host.addProgress(progress_id,filepath)
         self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent"))
+
+
+quick_widgets.register(QuickChat, Chat)
--- a/frontends/src/primitivus/constants.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/constants.py	Wed Dec 10 19:00:09 2014 +0100
@@ -68,6 +68,8 @@
                ('show_dnd_focus', 'dark red, bold', 'default'),
                ('show_xa', 'dark red', 'default'),
                ('show_xa_focus', 'dark red, bold', 'default'),
+               ('resource', 'light blue', 'default'),
+               ('resource_main', 'dark blue', 'default'),
                ('status', 'yellow', 'default'),
                ('status_focus', 'yellow, bold', 'default'),
                ('param_selected','default, bold', 'dark red'),
@@ -85,3 +87,15 @@
 
     CONFIG_SECTION = APP_NAME.lower()
     CONFIG_OPT_KEY_PREFIX = "KEY_"
+
+    MENU_ID_MAIN = "MAIN_MENU"
+    MENU_ID_WIDGET = "WIDGET_MENU"
+
+    MODE_NORMAL = 'NORMAL'
+    MODE_INSERTION = 'INSERTION'
+    MODE_COMMAND = 'COMMAND'
+
+    GROUP_DATA_FOLDED = 'folded'
+
+    # contacts and contact list
+    ALERT_HEADER='(*) '
--- a/frontends/src/primitivus/contact_list.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/contact_list.py	Wed Dec 10 19:00:09 2014 +0100
@@ -21,36 +21,25 @@
 import urwid
 from urwid_satext import sat_widgets
 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
-from sat_frontends.quick_frontend.quick_utils import unescapePrivate
-from sat_frontends.tools.jid import JID
 from sat_frontends.primitivus.status import StatusBar
 from sat_frontends.primitivus.constants import Const as C
 from sat_frontends.primitivus.keys import action_key_map as a_key
+from sat_frontends.primitivus.widget import PrimitivusWidget
+from sat_frontends.tools import jid
 from sat.core import log as logging
 log = logging.getLogger(__name__)
 
 
-class ContactList(urwid.WidgetWrap, QuickContactList):
+class ContactList(PrimitivusWidget, QuickContactList):
     signals = ['click','change']
 
-    def __init__(self, host, on_click=None, on_change=None, user_data=None):
-        QuickContactList.__init__(self)
-        self.host = host
-        self.selected = None
-        self.groups={}
-        self.alert_jid=set()
-        self.show_status = False
-        self.show_disconnected = False
-        self.show_empty_groups = True
-        # TODO: this may lead to two successive UI refresh and needs an optimization
-        self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=host.profile, callback=self.showEmptyGroups)
-        self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=host.profile, callback=self.showOfflineContacts)
+    def __init__(self, host, on_click=None, on_change=None, user_data=None, profile=None):
+        QuickContactList.__init__(self, host, profile)
 
         #we now build the widget
-        self.host.status_bar = StatusBar(host)
-        self.frame = sat_widgets.FocusFrame(self.__buildList(), None, self.host.status_bar)
-        self.main_widget = sat_widgets.LabelLine(self.frame, sat_widgets.SurroundedText(_("Contacts")))
-        urwid.WidgetWrap.__init__(self, self.main_widget)
+        self.status_bar = StatusBar(host)
+        self.frame = sat_widgets.FocusFrame(self._buildList(), None, self.status_bar)
+        PrimitivusWidget.__init__(self, self.frame, _(u'Contacts'))
         if on_click:
             urwid.connect_signal(self, 'click', on_click, user_data)
         if on_change:
@@ -59,16 +48,14 @@
     def update(self):
         """Update display, keep focus"""
         widget, position = self.frame.body.get_focus()
-        self.frame.body = self.__buildList()
+        self.frame.body = self._buildList()
         if position:
             try:
                 self.frame.body.focus_position = position
             except IndexError:
                 pass
-        self.host.redraw()
-
-    def update_jid(self, jid):
-        self.update()
+        self._invalidate()
+        self.host.redraw() # FIXME: check if can be avoided
 
     def keypress(self, size, key):
         # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
@@ -81,18 +68,18 @@
             self.show_status = not self.show_status
             self.update()
         elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts
-            self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.host.profile)
+            self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.profile)
+        elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources
+            self.showResources(not self.show_resources)
+            self.update()
         return super(ContactList, self).keypress(size, key)
 
-    def __contains__(self, jid):
-        for group in self.groups:
-            if jid.bare in self.groups[group][1]:
-                return True
-        return False
+    # modify the contact list
 
     def setFocus(self, text, select=False):
         """give focus to the first element that matches the given text. You can also
         pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode).
+
         @param text: contact group name, contact or muc userhost, muc private dialog jid
         @param select: if True, the element is also clicked
         """
@@ -103,12 +90,8 @@
                     # contact group
                     value = widget.getValue()
                 elif isinstance(widget, sat_widgets.SelectableText):
-                    if  widget.data.startswith(C.PRIVATE_PREFIX):
-                        # muc private dialog
-                        value = widget.getValue()
-                    else:
-                        # contact or muc
-                        value = widget.data
+                    # contact or muc
+                    value = widget.data
                 else:
                     # Divider instance
                     continue
@@ -116,243 +99,207 @@
                 if text.strip() == value.strip():
                     self.frame.body.focus_position = idx
                     if select:
-                        self.__contactClicked(widget, True)
+                        self._contactClicked(False, widget, True)
                     return
             except AttributeError:
                 pass
             idx += 1
 
-    def putAlert(self, jid):
-        """Put an alert on the jid to get attention from user (e.g. for new message)"""
-        self.alert_jid.add(jid.bare)
+        log.debug(u"Not element found for {} in setFocus".format(text))
+
+    def specialResourceVisible(self, entity):
+        """Assure a resource of a special entity is visible and clickable
+
+        Mainly used to display private conversation in MUC rooms
+        @param entity: full jid of the resource to show
+        """
+        assert isinstance(entity, jid.JID)
+        if entity not in self._special_extras:
+            self._special_extras.add(entity)
+            self.update()
+
+    # events
+
+    def _groupClicked(self, group_wid):
+        group = group_wid.getValue()
+        data = self.getGroupData(group)
+        data[C.GROUP_DATA_FOLDED] =  not data.setdefault(C.GROUP_DATA_FOLDED, False)
+        self.setFocus(group)
         self.update()
 
-    def __groupClicked(self, group_wid):
-        group = self.groups[group_wid.getValue()]
-        group[0] = not group[0]
+    def _contactClicked(self, use_bare_jid, contact_wid, selected):
+        """Method called when a contact is clicked
+
+        @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget.
+            If True, all jids in self._alerts with the same bare jid has contact_wid.data will be removed
+        @param contact_wid: widget of the contact, must have the entity set in data attribute
+        @param selected: boolean returned by the widget, telling if it is selected
+        """
+        entity = contact_wid.data
+        if use_bare_jid:
+            to_remove = set()
+            for alert_entity in self._alerts:
+                if alert_entity.bare == entity.bare:
+                    to_remove.add(alert_entity)
+            self._alerts.difference_update(to_remove)
+        else:
+            self._alerts.discard(entity)
+        self.host.modeHint(C.MODE_INSERTION)
         self.update()
-        self.setFocus(group_wid.getValue())
+        self._emit('click', entity)
+
+    # Methods to build the widget
+
+    def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None):
+        """Build one contact markup data
 
-    def __contactClicked(self, contact_wid, selected):
-        self.selected = contact_wid.data
-        for widget in self.frame.body.body:
-            if widget.__class__ == sat_widgets.SelectableText:
-                widget.setState(widget.data == self.selected, invisible=True)
-        if self.selected in self.alert_jid:
-            self.alert_jid.remove(self.selected)
-        self.host.modeHint('INSERTION')
-        self.update()
-        self._emit('click')
+        @param entity (jid.JID): entity to build
+        @param keys (iterable): value to markup, in preferred order.
+            The first available key will be used.
+            If key starts with "cache_", it will be checked in cache,
+            else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
+            If nothing full or keys is None, full entity is used.
+        @param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons
+        @param with_alert (bool): if True, show alert if entity is in self._alerts
+        @param with_show_attr (bool): if True, show color corresponding to presence status
+        @param markup_prepend (list): markup to prepend to the generated one before building the widget
+        @param markup_append (list): markup to append to the generated one before building the widget
+        @return (list): markup data are expected by Urwid text widgets
+        """
+        markup = []
+        if use_bare_jid:
+            alerts = {entity.bare for entity in self._alerts}
+            selected = {entity.bare for entity in self._selected}
+        else:
+            alerts = self._alerts
+            selected = self._selected
+        if keys is None:
+            entity_txt = entity
+        else:
+            cache = self.getCache(entity)
+            for key in keys:
+                if key.startswith('cache_'):
+                    entity_txt = cache.get(key[6:])
+                else:
+                    entity_txt = getattr(entity, key)
+                if entity_txt:
+                    break
+            if not entity_txt:
+                entity_txt = entity
 
-    def __buildContact(self, content, contacts):
-        """Add contact representation in widget list
+        if with_show_attr:
+            show = self.getCache(entity, C.PRESENCE_SHOW)
+            if show is None:
+                show = C.PRESENCE_UNAVAILABLE
+            show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default'))
+            markup.insert(0, u"{} ".format(show_icon))
+        else:
+            entity_attr = 'default'
+
+        if with_alert and entity in alerts:
+            entity_attr = 'alert'
+            header = C.ALERT_HEADER
+        else:
+            header = ''
+
+        markup.append((entity_attr, entity_txt))
+        if markup_prepend:
+            markup.insert(0, markup_prepend)
+        if markup_append:
+            markup.extend(markup_append)
+
+        widget = sat_widgets.SelectableText(markup,
+                                            selected = entity in selected,
+                                            header = header)
+        widget.data = entity
+        widget.comp = entity_txt.lower() # value to use for sorting
+        urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid])
+        return widget
+
+    def _buildEntities(self, content, entities):
+        """Add entity representation in widget list
+
         @param content: widget list, e.g. SimpleListWalker
-        @param contacts (list): list of JID userhosts"""
-        if not contacts:
+        @param entities (iterable): iterable of JID to display
+        """
+        if not entities:
             return
         widgets = []  # list of built widgets
 
-        for contact in contacts:
-            if contact.startswith(C.PRIVATE_PREFIX):
-                contact_disp = ('alert' if contact in self.alert_jid else "show_normal", unescapePrivate(contact))
-                show_icon = ''
-                status = ''
+        for entity in entities:
+            if entity in self._specials or not self.entityToShow(entity):
+                continue
+            markup_extra = []
+            if self.show_resources:
+                for resource in self.getCache(entity, C.CONTACT_RESOURCES):
+                    resource_disp = ('resource_main' if resource == self.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n  " + resource)
+                    markup_extra.append(resource_disp)
+                    if self.show_status:
+                        status = self.getCache(jid.JID('%s/%s' % (entity, resource)), 'status')
+                        status_disp = ('status', "\n    " + status) if status else ""
+                        markup_extra.append(status_disp)
+
+
             else:
-                jid = JID(contact)
-                name = self.getCache(jid, 'name')
-                nick = self.getCache(jid, 'nick')
-                status = self.getCache(jid, 'status')
-                show = self.getCache(jid, 'show')
-                if show is None:
-                    show = "unavailable"
-                if not self.contactToShow(contact):
-                    continue
-                show_icon, show_attr = C.PRESENCE.get(show, ('', 'default'))
-                contact_disp = ('alert' if contact in self.alert_jid else show_attr, nick or name or jid.node or jid.bare)
-            display = [show_icon + " ", contact_disp]
-            if self.show_status:
-                status_disp = ('status', "\n  " + status) if status else ""
-                display.append(status_disp)
-            header = '(*) ' if contact in self.alert_jid else ''
-            widget = sat_widgets.SelectableText(display,
-                                                selected=contact == self.selected,
-                                                header=header)
-            widget.data = contact
-            widget.comp = contact_disp[1].lower()  # value to use for sorting
+                if self.show_status:
+                    status = self.getCache(entity, 'status')
+                    status_disp = ('status', "\n  " + status) if status else ""
+                    markup_extra.append(status_disp)
+            widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), use_bare_jid=True, markup_append=markup_extra)
             widgets.append(widget)
 
         widgets.sort(key=lambda widget: widget.comp)
 
         for widget in widgets:
             content.append(widget)
-            urwid.connect_signal(widget, 'change', self.__contactClicked)
 
-    def __buildSpecials(self, content):
+    def _buildSpecials(self, content):
         """Build the special entities"""
-        specials = self.specials.keys()
+        specials = list(self._specials)
         specials.sort()
-        for special in specials:
-            jid=JID(special)
-            name = self.getCache(jid, 'name')
-            nick = self.getCache(jid, 'nick')
-            special_disp = ('alert' if special in self.alert_jid else 'default', nick or name or jid.node or jid.bare)
-            display = [ "  " , special_disp]
-            header = '(*) ' if special in self.alert_jid else ''
-            widget = sat_widgets.SelectableText(display,
-                                                selected = special==self.selected,
-                                                header=header)
-            widget.data = special
+        extra_shown = set()
+        for entity in specials:
+            # the special widgets
+            widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False)
             content.append(widget)
-            urwid.connect_signal(widget, 'change', self.__contactClicked)
 
-    def __buildList(self):
+            # resources which must be displayed (e.g. MUC private conversations)
+            extras = [extra for extra in self._special_extras if extra.bare == entity.bare]
+            extras.sort()
+            for extra in extras:
+                widget = self._buildEntityWidget(extra, ('resource',), markup_prepend = '  ')
+                content.append(widget)
+                extra_shown.add(extra)
+
+        # entities which must be visible but not resource of current special entities
+        for extra in self._special_extras.difference(extra_shown):
+            widget = self._buildEntityWidget(extra, ('resource',))
+            content.append(widget)
+
+    def _buildList(self):
         """Build the main contact list widget"""
         content = urwid.SimpleListWalker([])
 
-        self.__buildSpecials(content)
-        if self.specials:
+        self._buildSpecials(content)
+        if self._specials:
             content.append(urwid.Divider('='))
 
-        group_keys = self.groups.keys()
-        group_keys.sort(key=lambda x: x.lower() if x else x)
-        for key in group_keys:
-            unfolded = self.groups[key][0]
-            contacts = list(self.groups[key][1])
-            if key is not None and (self.nonEmptyGroup(contacts) or self.show_empty_groups):
-                header = '[-]' if unfolded else '[+]'
-                widget = sat_widgets.ClickableText(key, header=header + ' ')
+        groups = list(self._groups)
+        groups.sort(key=lambda x: x.lower() if x else x)
+        for group in groups:
+            data = self.getGroupData(group)
+            folded = data.get(C.GROUP_DATA_FOLDED, False)
+            jids = list(data['jids'])
+            if group is not None and (self.anyEntityToShow(jids) or self.show_empty_groups):
+                header = '[-]' if not folded else '[+]'
+                widget = sat_widgets.ClickableText(group, header=header + ' ')
                 content.append(widget)
-                urwid.connect_signal(widget, 'click', self.__groupClicked)
-            if unfolded:
-                self.__buildContact(content, contacts)
-        return urwid.ListBox(content)
-
-    def contactToShow(self, contact):
-        """Tell if the contact should be showed or hidden.
-
-        @param contact (str): JID userhost of the contact
-        @return: True if that contact should be showed in the list"""
-        show = self.getCache(JID(contact), 'show')
-        return (show is not None and show != "unavailable") or \
-            self.show_disconnected or contact in self.alert_jid or contact == self.selected
-
-    def nonEmptyGroup(self, contacts):
-        """Tell if a contact group contains some contacts to show.
-
-        @param contacts (list[str]): list of JID userhosts
-        @return: bool
-        """
-        for contact in contacts:
-            if self.contactToShow(contact):
-                return True
-        return False
-
-    def unselectAll(self):
-        """Unselect all contacts"""
-        self.selected = None
-        for widget in self.frame.body.body:
-            if widget.__class__ == sat_widgets.SelectableText:
-                widget.setState(False, invisible=True)
-
-    def getContact(self):
-        """Return contact currently selected"""
-        return self.selected
-
-    def clearContacts(self):
-        """clear all the contact list"""
-        QuickContactList.clearContacts(self)
-        self.groups={}
-        self.selected = None
-        self.unselectAll()
-        self.update()
-
-    def replace(self, jid, groups=None, attributes=None):
-        """Add a contact to the list if doesn't exist, else update it.
-
-        This method can be called with groups=None for the purpose of updating
-        the contact's attributes (e.g. nickname). In that case, the groups
-        attribute must not be set to the default group but ignored. If not,
-        you may move your contact from its actual group(s) to the default one.
-
-        None value for 'groups' has a different meaning than [None] which is for the default group.
+                urwid.connect_signal(widget, 'click', self._groupClicked)
+            if not folded:
+                self._buildEntities(content, jids)
+        not_in_roster = set(self._cache).difference(self._roster).difference(self._specials)
+        if not_in_roster:
+            content.append(urwid.Divider('-'))
+            self._buildEntities(content, not_in_roster)
 
-        @param jid (JID)
-        @param groups (list): list of groups or None to ignore the groups membership.
-        @param attributes (dict)
-        """
-        QuickContactList.replace(self, jid, groups, attributes)  # eventually change the nickname
-        if jid.bare in self.specials:
-            return
-        if groups is None:
-            self.update()
-            return
-        assert isinstance(jid, JID)
-        assert isinstance(groups, list)
-        if groups == []:
-            groups = [None]  # [None] is the default group
-        for group in [group for group in self.groups if group not in groups]:
-            try:  # remove the contact from a previous group
-                self.groups[group][1].remove(jid.bare)
-            except KeyError:
-                pass
-        for group in groups:
-            if group not in self.groups:
-                self.groups[group] = [True, set()]  # [unfold, list_of_contacts]
-            self.groups[group][1].add(jid.bare)
-        self.update()
-
-    def remove(self, jid):
-        """remove a contact from the list"""
-        QuickContactList.remove(self, jid)
-        groups_to_remove = []
-        for group in self.groups:
-            contacts = self.groups[group][1]
-            if jid.bare in contacts:
-                contacts.remove(jid.bare)
-                if not len(contacts):
-                    groups_to_remove.append(group)
-        for group in groups_to_remove:
-            del self.groups[group]
-        self.update()
-
-    def add(self, jid, param_groups=None):
-        """add a contact to the list"""
-        self.replace(jid, param_groups if param_groups else [None])
-
-    def setSpecial(self, special_jid, special_type, show=False):
-        """Set entity as a special
-        @param special_jid: jid of the entity
-        @param special_type: special type (e.g.: "MUC")
-        @param show: True to display the dialog to chat with this entity
-        """
-        QuickContactList.setSpecial(self, special_jid, special_type, show)
-        if None in self.groups:
-            folded, group_jids = self.groups[None]
-            for group_jid in group_jids:
-                if JID(group_jid).bare == special_jid.bare:
-                    group_jids.remove(group_jid)
-                    break
-        self.update()
-        if show:
-            # also display the dialog for this room
-            self.setFocus(special_jid, True)
-            self.host.redraw()
-
-    def updatePresence(self, jid, show, priority, statuses):
-        #XXX: for the moment, we ignore presence updates for special entities
-        if jid.bare not in self.specials:
-            QuickContactList.updatePresence(self, jid, show, priority, statuses)
-
-    def showOfflineContacts(self, show):
-        show = C.bool(show)
-        if self.show_disconnected == show:
-            return
-        self.show_disconnected = show
-        self.update()
-
-    def showEmptyGroups(self, show):
-        show = C.bool(show)
-        if self.show_empty_groups == show:
-            return
-        self.show_empty_groups = show
-        self.update()
+        return urwid.ListBox(content)
--- a/frontends/src/primitivus/keys.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/keys.py	Wed Dec 10 19:00:09 2014 +0100
@@ -42,15 +42,18 @@
         ("menu_global", "APP_QUIT"): 'ctrl x',
         ("menu_global", "ROOM_JOIN"): 'meta j',
 
+        # primitivus widgets
+        ("primitivus_widget", "DECORATION_HIDE"): "meta l",
+
         # contact list
         ("contact_list", "STATUS_HIDE"): "meta s",
         ("contact_list", "DISCONNECTED_HIDE"): "meta d",
+        ("contact_list", "RESOURCES_HIDE"): "meta r",
 
         # chat panel
         ("chat_panel", "OCCUPANTS_HIDE"): "meta p",
         ("chat_panel", "TIMESTAMP_HIDE"): "meta t",
         ("chat_panel", "SHORT_NICKNAME"): "meta n",
-        ("chat_panel", "DECORATION_HIDE"): "meta l",
         ("chat_panel", "SUBJECT_SWITCH"): "meta s",
 
         #card game
--- a/frontends/src/primitivus/primitivus	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/primitivus	Wed Dec 10 19:00:09 2014 +0100
@@ -28,8 +28,8 @@
 from urwid_satext import sat_widgets
 from urwid_satext.files_management import FileDialog
 from sat_frontends.quick_frontend.quick_app import QuickApp
-from sat_frontends.quick_frontend.quick_chat_list import QuickChatList
-from sat_frontends.quick_frontend.quick_utils import getNewPath, unescapePrivate
+from sat_frontends.quick_frontend.quick_utils import getNewPath
+from sat_frontends.quick_frontend import quick_chat
 from sat_frontends.primitivus.profile_manager import ProfileManager
 from sat_frontends.primitivus.contact_list import ContactList
 from sat_frontends.primitivus.chat import Chat
@@ -39,35 +39,28 @@
 from sat_frontends.primitivus.keys import action_key_map as a_key
 from sat_frontends.primitivus import config
 from sat_frontends.tools.misc import InputHistory
-from sat_frontends.constants import Const as commonConst # FIXME
-from sat_frontends.tools.jid import JID
+from sat_frontends.tools import jid
 from os.path import join
 import signal
 
 
-class ChatList(QuickChatList):
-    """This class manage the list of chat windows"""
-
-    def createChat(self, target):
-        return Chat(target, self.host)
-
 
 class EditBar(sat_widgets.ModalEdit):
     """
     The modal edit bar where you would enter messages and commands.
     """
 
-    def __init__(self, app):
-        modes = {None: ('NORMAL', u''),
-                 a_key['MODE_INSERTION']: ('INSERTION', u'> '),
-                 a_key['MODE_COMMAND']: ('COMMAND', u':')} #XXX: captions *MUST* be unicode
+    def __init__(self, host):
+        modes = {None: (C.MODE_NORMAL, u''),
+                 a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '),
+                 a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode
         super(EditBar, self).__init__(modes)
-        self.app = app
+        self.host = host
         self.setCompletionMethod(self._text_completion)
         urwid.connect_signal(self, 'click', self.onTextEntered)
 
     def _text_completion(self, text, completion_data, mode):
-        if mode == 'INSERTION':
+        if mode == C.MODE_INSERTION:
             return self._nick_completion(text, completion_data)
         else:
             return text
@@ -75,9 +68,9 @@
     def _nick_completion(self, text, completion_data):
         """Completion method which complete pseudo in group chat
         for params, see AdvancedEdit"""
-        contact = self.app.contact_list.getContact() ###Based on the fact that there is currently only one contact selectable at once
+        contact = self.host.contact_list.getContact() ###Based on the fact that there is currently only one contact selectable at once
         if contact:
-            chat = self.app.chat_wins[contact]
+            chat = self.host.chat_wins[contact]
             if chat.type != "group":
                 return text
             space = text.rfind(" ")
@@ -98,18 +91,17 @@
 
     def onTextEntered(self, editBar):
         """Called when text is entered in the main edit bar"""
-        if self.mode == 'INSERTION':
-            contact = self.app.contact_list.getContact() ###Based on the fact that there is currently only one contact selectableat once
-            if contact:
-                chat = self.app.chat_wins[contact]
-                self.app.sendMessage(contact,
+        if self.mode == C.MODE_INSERTION:
+            if isinstance(self.host.selected_widget, quick_chat.QuickChat):
+                chat_widget = self.host.selected_widget
+                self.host.sendMessage(chat_widget.target,
                                      editBar.get_edit_text(),
-                                     mess_type = "groupchat" if chat.type == 'group' else "chat",
-                                     errback=lambda failure: self.app.notify(_("Error while sending message (%s)") % failure),
-                                     profile_key=self.app.profile
+                                     mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
+                                     errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
+                                     profile_key=chat_widget.profile
                                     )
                 editBar.set_edit_text('')
-        elif self.mode == 'COMMAND':
+        elif self.mode == C.MODE_COMMAND:
             self.commandHandler()
 
     def commandHandler(self):
@@ -118,42 +110,44 @@
         tokens = self.get_edit_text().split(' ')
         command, args = tokens[0], tokens[1:]
         if command == 'quit':
-            self.app.onExit()
+            self.host.onExit()
             raise urwid.ExitMainLoop()
         elif command == 'messages':
             wid = sat_widgets.GenericList(logging.memoryGet())
-            self.app.addWindow(wid)
-        elif command == 'presence':
-            values = [value for value in commonConst.PRESENCE.keys()]
-            values = [value if value else 'online' for value in values]  # the empty value actually means 'online'
-            if args and args[0] in values:
-                presence = '' if args[0] == 'online' else args[0]
-                self.app.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
-            else:
-                self.app.status_bar.onPresenceClick()
-        elif command == 'status':
-            if args:
-                self.app.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
-            else:
-                self.app.status_bar.onStatusClick()
+            self.host.selectWidget(wid)
+        # elif command == 'presence':
+        #     values = [value for value in commonConst.PRESENCE.keys()]
+        #     values = [value if value else 'online' for value in values]  # the empty value actually means 'online'
+        #     if args and args[0] in values:
+        #         presence = '' if args[0] == 'online' else args[0]
+        #         self.host.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
+        #     else:
+        #         self.host.status_bar.onPresenceClick()
+        # elif command == 'status':
+        #     if args:
+        #         self.host.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
+        #     else:
+        #         self.host.status_bar.onStatusClick()
         elif command == 'history':
-            try:
-                limit = int(args[0])
-            except (IndexError, ValueError):
-                limit = 50
-            win = self.app.chat_wins[JID(self.app.contact_list.selected).bare]
-            win.clearHistory()
-            if limit > 0:
-                win.historyPrint(size=limit, profile=self.app.profile)
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat):
+                try:
+                    limit = int(args[0])
+                except (IndexError, ValueError):
+                    limit = 50
+                widget.clearHistory()
+                if limit > 0:
+                    widget.historyPrint(size=limit, profile=widget.profile)
         elif command == 'search':
-            pattern = " ".join(args)
-            if not pattern:
-                self.app.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
-            win = self.app.chat_wins[JID(self.app.contact_list.selected).bare]
-            win.clearHistory()
-            win.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, timestamp=0)
-            win.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=self.app.profile)
-            win.printInfo(D_("Type ':history <lines>' to reset the chat history"))
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat):
+                pattern = " ".join(args)
+                if not pattern:
+                    self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
+                widget.clearHistory()
+                widget.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, timestamp=0)
+                widget.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
+                widget.printInfo(D_("Type ':history <lines>' to reset the chat history"))
         else:
             return
         self.set_edit_text('')
@@ -167,22 +161,24 @@
         and move the index of the temporary history stack."""
         if key == a_key['MODAL_ESCAPE']:
             # first save the text to the current mode, then change to NORMAL
-            self.app._updateInputHistory(self.get_edit_text(), mode=self.mode)
-            self.app._updateInputHistory(mode='NORMAL')
-        if self._mode == 'NORMAL' and key in self._modes:
-            self.app._updateInputHistory(mode=self._modes[key][0])
+            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
+            self.host._updateInputHistory(mode=C.MODE_NORMAL)
+        if self._mode == C.MODE_NORMAL and key in self._modes:
+            self.host._updateInputHistory(mode=self._modes[key][0])
         if key == a_key['HISTORY_PREV']:
-            self.app._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
+            self.host._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
             return
         elif key == a_key['HISTORY_NEXT']:
-            self.app._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
+            self.host._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
             return
         elif key == a_key['EDIT_ENTER']:
-            self.app._updateInputHistory(self.get_edit_text(), mode=self.mode)
+            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
         else:
-            contact = self.app.contact_list.getContact()
-            if contact:
-                self.app.bridge.chatStateComposing(unescapePrivate(contact), self.app.profile)
+            if (self._mode == C.MODE_INSERTION
+                and isinstance(self.host.selected_widget, quick_chat.QuickChat)
+                and key not in sat_widgets.FOCUS_KEYS):
+                self.host.bridge.chatStateComposing(self.host.selected_widget.target, self.host.selected_widget.profile)
+
         return super(EditBar, self).keypress(size, key)
 
 
@@ -287,12 +283,13 @@
         self.main_widget = ProfileManager(self)
         self.loop = urwid.MainLoop(self.main_widget, C.PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
 
+
         ##misc setup##
-        self.chat_wins = ChatList(self)
         self.notif_bar = sat_widgets.NotificationBar()
         urwid.connect_signal(self.notif_bar, 'change', self.onNotification)
-        self.progress_wid = Progress(self)
-        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.addWindow(self.progress_wid))
+
+        self.progress_wid = self.widgets.getOrCreateWidget(Progress, None, on_new_widget=None)
+        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.selectWidget(self.progress_wid))
         self.__saved_overlay = None
 
         self.x_notify = Notify()
@@ -351,7 +348,7 @@
                 self._early_popup = popup
             else:
                 self.showPopUp(popup)
-        super(PrimitivusApp, self).postInit()
+        super(PrimitivusApp, self).postInit(self.main_widget)
 
     def inputFilter(self, input_, raw):
         if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
@@ -387,14 +384,14 @@
 
         elif input_ == a_key['DEBUG'] and 'D' in self.bridge.getVersion(): #Debug only for dev versions
             self.debug()
-        elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact_list
+        elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
             try:
                 for wid, options in self.center_part.contents:
-                    if self.contact_list is wid:
+                    if self.contact_lists_pile is wid:
                         self.center_part.contents.remove((wid, options))
                         break
                 else:
-                    self.center_part.contents.insert(0, (self.contact_list, ('weight', 2, False)))
+                    self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
             except AttributeError:
                 #The main widget is not built (probably in Profile Manager)
                 pass
@@ -413,17 +410,18 @@
         except AttributeError:
             return input_
 
-    def addMenus(self, menu, type_, menu_data=None):
+    def addMenus(self, menu, type_filter, menu_data=None):
         """Add cached menus to instance
         @param menu: sat_widgets.Menu instance
-        @param type_: menu type like is sat.core.sat_main.importMenu
+        @param type_filter: menu type like is sat.core.sat_main.importMenu
         @param menu_data: data to send with these menus
 
         """
-        menus = self.profiles[self.profile]['menus'].get(type_,[])
         def add_menu_cb(callback_id):
-            self.launchAction(callback_id, menu_data, profile_key = self.profile)
-        for id_, path, path_i18n  in menus:
+            self.launchAction(callback_id, menu_data, profile=self.current_profile)
+        for id_, type_, path, path_i18n  in self.bridge.getMenus("", C.NO_SECURITY_LIMIT ):
+            if type_ != type_filter:
+                continue
             if len(path) != 2:
                 raise NotImplementedError("Menu with a path != 2 are not implemented yet")
             menu.addMenu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
@@ -445,23 +443,28 @@
         #FIXME: do this in a more generic way (in quickapp)
         self.addMenus(menu, C.MENU_GLOBAL)
 
-        menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)])
+        menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
         return menu_roller
 
     def _buildMainWidget(self):
-        self.contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw())
-        #self.center_part = urwid.Columns([('weight',2,self.contact_list),('weight',8,Chat('',self))])
-        self.center_part = urwid.Columns([('weight', 2, self.contact_list), ('weight', 8, urwid.Filler(urwid.Text('')))])
+        self.contact_lists_pile = urwid.Pile([])
+        #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
+        self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
 
         self.editBar = EditBar(self)
         self.menu_roller = self._buildMenuRoller()
         self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
         return self.main_widget
 
-    def plug_profile_1(self, profile_key='@DEFAULT@'):
+    def addContactList(self, profile):
+        contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile)
+        self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
+        self.contact_lists[profile] = contact_list
+        return contact_list
+
+    def plugging_profiles(self):
         self.loop.widget = self._buildMainWidget()
         self.redraw()
-        QuickApp.plug_profile_1(self, profile_key)
         try:
             # if a popup arrived before main widget is build, we need to show it now
             self.showPopUp(self._early_popup)
@@ -492,15 +495,32 @@
         self.notif_bar.addMessage(message)
         self.redraw()
 
-    def addWindow(self, widget):
-        """Display a window if possible,
+    def newWidget(self, widget):
+        """Display a widget if possible,
+
         else add it in the notification bar queue
-        @param widget: BoxWidget"""
-        assert(len(self.center_part.widget_list)<=2)
+        @param widget: BoxWidget
+        """
+        self.selectWidget(widget)
+
+    def selectWidget(self, widget):
+        assert len(self.center_part.widget_list)<=2
         wid_idx = len(self.center_part.widget_list)-1
         self.center_part.widget_list[wid_idx] = widget
-        self.menu_roller.removeMenu(_('Chat menu'))
-        self.contact_list.unselectAll()
+        try:
+            self.menu_roller.removeMenu(C.MENU_ID_WIDGET)
+        except KeyError:
+            log.debug("No menu to delete")
+        self.selected_widget = widget
+        self.visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
+        for contact_list in self.contact_lists.itervalues():
+            contact_list.unselectAll()
+
+        for wid in self.visible_widgets:
+            if isinstance(wid, Chat):
+                contact_list = self.contact_lists[wid.profile]
+                contact_list.select(wid.target)
+
         self.redraw()
 
     def removeWindow(self):
@@ -522,23 +542,29 @@
         """Set the progression shown in notification bar"""
         self.notif_bar.setProgress(percentage)
 
-    def contactSelected(self, contact_list):
-        contact = contact_list.getContact()
-        if contact:
-            assert(len(self.center_part.widget_list)==2)
-            self.center_part.widget_list[1] = self.chat_wins[contact]
-            self.menu_roller.addMenu(_('Chat menu'), self.chat_wins[contact].getMenu())
+    def contactSelected(self, contact_list, entity):
+        if entity.resource:
+            # we have clicked on a private MUC conversation
+            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
+        else:
+            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
+        self.selectWidget(chat_widget)
+        self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
 
-    def newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile):
-        QuickApp.newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile)
+    def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
+        QuickApp.newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile)
 
-        if not from_jid in self.contact_list and from_jid.bare != self.profiles[profile]['whoami'].bare:
+        if not from_jid in self.contact_lists[profile] and from_jid.bare != self.profiles[profile].whoami.bare:
             #XXX: needed to show entities which haven't sent any
             #     presence information and which are not in roster
-            self.contact_list.replace(from_jid, [C.GROUP_NOT_IN_ROSTER])
-
-        if self.contact_list.selected is None or JID(self.contact_list.selected).bare != from_jid.bare:
-            self.contact_list.putAlert(from_jid)
+            self.contact_lists[profile].setContact(from_jid)
+        visible = False
+        for widget in self.visible_widgets:
+            if isinstance(widget, Chat) and widget.manageMessage(from_jid, type_):
+                visible = True
+                break
+        if not visible:
+            self.contact_lists[profile].setAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
 
     def _dialogOkCb(self, widget, data):
         self.removePopUp()
@@ -552,7 +578,6 @@
         answer_data = [data[1]] if data[1] else []
         answer_cb(False, *answer_data)
 
-
     def showDialog(self, message, title="", type_="info", answer_cb = None, answer_data = None):
         if type_ == 'info':
             popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
@@ -578,11 +603,15 @@
         else:
             self.main_widget.show('notif_bar')
 
-    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
+    def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
         """ Launch a dynamic action
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
-        @param profile_key: %(doc_profile_key)s
+        @param callback: if not None and 'validated' key is present, it will be called with the following parameters:
+            - callback_id
+            - data
+            - profile
+        @param profile: %(doc_profile)s
 
         """
         if data is None:
@@ -593,24 +622,20 @@
                 # action was a one shot, nothing to do
                 pass
             elif "xmlui" in data:
-                ui = xmlui.create(self, xml_data=data['xmlui'])
+                ui = xmlui.create(self, xml_data=data['xmlui'], callback=callback, profile=profile)
                 ui.show()
-            elif "authenticated_profile" in data:
-                assert("caller" in data)
-                if data["caller"] == "profile_manager":
-                    assert(isinstance(self.main_widget, ProfileManager))
-                    self.main_widget.getXMPPParams(data['authenticated_profile'])
-                elif data["caller"] == "plug_profile":
-                    self.plug_profile_1(data['authenticated_profile'])
-                else:
-                    raise NotImplementedError
+            elif 'validated' in data:
+                pass # this key is managed below
             else:
                 self.showPopUp(sat_widgets.Alert(_("Error"), _(u"Unmanaged action result"), ok_cb=self.removePopUp))
 
+            if callback and 'validated' in data:
+                callback(callback_id, data, profile)
+
         def action_eb(failure):
             self.showPopUp(sat_widgets.Alert(failure.fullname, failure.message, ok_cb=self.removePopUp))
 
-        self.bridge.launchAction(callback_id, data, profile_key, callback=action_cb, errback=action_eb)
+        self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=action_eb)
 
     def askConfirmationHandler(self, confirmation_id, confirmation_type, data, profile):
         answer_data={}
@@ -684,23 +709,30 @@
             log.error (_("FIXME FIXME FIXME: type [%s] not implemented") % type_)
             raise NotImplementedError
 
+
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
+        super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile)
+        self.contact_lists[profile].setFocus(jid.JID(room_jid_s), True)
+
+
+
     ##DIALOGS CALLBACKS##
     def onJoinRoom(self, button, edit):
         self.removePopUp()
-        room_jid = JID(edit.get_edit_text())
+        room_jid = jid.JID(edit.get_edit_text())
         if room_jid.is_valid():
-            self.bridge.joinMUC(room_jid, self.profiles[self.profile]['whoami'].node, {}, self.profile)
+            self.bridge.joinMUC(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile)
         else:
-            message = _("'%s' is an invalid JID !") % room_jid
+            message = _("'%s' is an invalid jid.JID !") % room_jid
             log.error (message)
             self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp))
 
     #MENU EVENTS#
     def onConnectRequest(self, menu):
-        QuickApp.asyncConnect(self, self.profile)
+        QuickApp.asyncConnect(self, self.current_profile)
 
     def onDisconnectRequest(self, menu):
-        self.bridge.disconnect(self.profile)
+        self.bridge.disconnect(self.current_profile)
 
     def onParam(self, menu):
         def success(params):
@@ -709,7 +741,7 @@
 
         def failure(error):
             self.showPopUp(sat_widgets.Alert(_("Error"), _("Can't get parameters (%s)") % error, ok_cb=self.removePopUp))
-        self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.profile, callback=success, errback=failure)
+        self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
 
     def onExitRequest(self, menu):
         QuickApp.onExit(self)
@@ -725,12 +757,12 @@
 
     #MISC CALLBACKS#
 
-    def setStatusOnline(self, online=True, show="", statuses={}):
+    def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
         if not online or not statuses:
-            self.status_bar.setPresenceStatus(show if online else 'unavailable', '')
+            self.contact_lists[profile].status_bar.setPresenceStatus(show if online else 'unavailable', '')
             return
         try:
-            self.status_bar.setPresenceStatus(show, statuses['default'])
+            self.contact_lists[profile].status_bar.setPresenceStatus(show, statuses['default'])
         except (KeyError, TypeError):
             pass
 
--- a/frontends/src/primitivus/profile_manager.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/profile_manager.py	Wed Dec 10 19:00:09 2014 +0100
@@ -18,29 +18,69 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import _
+from sat.core import log as logging
+log = logging.getLogger(__name__)
 from sat_frontends.primitivus.constants import Const as C
+from sat_frontends.primitivus.keys import action_key_map as a_key
+from urwid_satext import sat_widgets
 import urwid
-from urwid_satext.sat_widgets import AdvancedEdit, Password, List, InputDialog, ConfirmDialog, Alert
-from sat_frontends.primitivus.keys import action_key_map as a_key
+
+class ProfileRecord(object):
+
+    def __init__(self, profile=None, login=None, password=None):
+        self._profile = profile
+        self._login = login
+        self._password = password
+
+    @property
+    def profile(self):
+        return self._profile
+
+    @profile.setter
+    def profile(self, value):
+        self._profile = value
+        # if we change the profile,
+        # we must have no login/password until backend give them
+        self._login = self._password = None
+
+    @property
+    def login(self):
+        return self._login
+
+    @login.setter
+    def login(self, value):
+        self._login = value
+
+    @property
+    def password(self):
+        return self._password
+
+    @password.setter
+    def password(self, value):
+        self._password = value
 
 
 class ProfileManager(urwid.WidgetWrap):
+    """Class with manage profiles creation/deletion/connection"""
 
-    def __init__(self, host):
+    def __init__(self, host, autoconnect=None):
+        """Create the manager
+
+        @param host: %(doc_host)s
+        @param autoconnect(iterable): list of profiles to connect automatically
+        """
         self.host = host
-        #profiles list
+        self._autoconnect = bool(autoconnect)
+        self.current = ProfileRecord()
         profiles = self.host.bridge.getProfilesList()
         profiles.sort()
 
         #login & password box must be created before list because of onProfileChange
-        self.login_wid = AdvancedEdit(_('Login:'), align='center')
-        self.pass_wid = Password(_('Password:'), align='center')
+        self.login_wid = sat_widgets.AdvancedEdit(_('Login:'), align='center')
+        self.pass_wid = sat_widgets.Password(_('Password:'), align='center')
 
-        self.selected_profile = None  # allow to reselect the previous selection until the profile is authenticated
-        style = ['single']
-        if self.host.options.profile:
-            style.append('no_first_select')
-        self.list_profile = List(profiles, style=style, align='center', on_change=self.onProfileChange)
+        style = ['no_first_select']
+        self.list_profile = sat_widgets.List(profiles, style=style, align='center', on_change=self.onProfileChange)
 
         #new & delete buttons
         buttons = [urwid.Button(_("New"), self.onNewProfile),
@@ -51,14 +91,29 @@
         divider = urwid.Divider('-')
 
         #connect button
-        connect_button = urwid.Button(_("Connect"), self.onConnectProfile)
+        connect_button = sat_widgets.CustomButton(_("Connect"), self.onConnectProfiles, align='center')
 
         #we now build the widget
-        list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button])
+        list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile, divider, self.login_wid, self.pass_wid, connect_button])
         frame_body = urwid.ListBox(list_walker)
         frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title'))
         self.main_widget = urwid.LineBox(frame)
         urwid.WidgetWrap.__init__(self, self.main_widget)
+        if self._autoconnect:
+            self.autoconnect(autoconnect)
+
+    def autoconnect(self, profile_keys):
+        """Automatically connect profiles
+
+        @param profile_keys(iterable): list of profile keys to connect
+        """
+        if not profile_keys:
+            log.warning("No profile given to autoconnect")
+            return
+        self._autoconnect = True
+        self._autoconnect_profiles=[]
+        self._do_autoconnect(profile_keys)
+
 
     def keypress(self, size, key):
         if key == a_key['APP_QUIT']:
@@ -80,11 +135,41 @@
                     return
         return super(ProfileManager, self).keypress(size, key)
 
-    def __refillProfiles(self):
+    def _do_autoconnect(self, profile_keys):
+        """Connect automatically given profiles
+
+        @param profile_kes(iterable): profiles to connect
+        """
+        assert self._autoconnect
+
+        def authenticate_cb(callback_id, data, profile):
+
+            if C.bool(data['validated']):
+                self._autoconnect_profiles.append(profile)
+                if len(self._autoconnect_profiles) == len(profile_keys):
+                    # all the profiles have been validated
+                    self.host.plug_profiles(self._autoconnect_profiles)
+            else:
+                # a profile is not validated, we go to manual mode
+                self._autoconnect=False
+
+        for profile_key in profile_keys:
+            profile = self.host.bridge.getProfileName(profile_key)
+            if not profile:
+                self._autoconnect = False # manual mode
+                msg = _("Trying to plug an unknown profile key ({})".format(profile_key))
+                log.warning(msg)
+                popup = sat_widgets.Alert(_("Profile plugging in error"), msg, ok_cb=self.host.removePopUp)
+                self.host.showPopUp(popup)
+                break
+            self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=profile)
+
+    def refillProfiles(self):
         """Update the list of profiles"""
         profiles = self.host.bridge.getProfilesList()
         profiles.sort()
         self.list_profile.changeValues(profiles)
+        self.host.redraw()
 
     def cancelDialog(self, button):
         self.host.removePopUp()
@@ -92,105 +177,120 @@
     def newProfile(self, button, edit):
         """Create the profile"""
         name = edit.get_edit_text()
-        self.host.bridge.asyncCreateProfile(name, callback=lambda: self._newProfileCreated(name), errback=self._profileCreationFailure)
+        self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
 
-    def _newProfileCreated(self, name):
-        self.__refillProfiles()
-        #We select the profile created in the list
-        self.list_profile.selectValue(name)
+    def newProfileCreated(self, profile):
         self.host.removePopUp()
+        self.refillProfiles()
+        self.list_profile.selectValue(profile)
+        self.current.profile=profile
+        self.getConnectionParams(profile)
         self.host.redraw()
 
-    def _profileCreationFailure(self, reason):
+    def profileCreationFailure(self, reason):
         self.host.removePopUp()
         if reason == "ConflictError":
             message = _("A profile with this name already exists")
         elif reason == "CancelError":
             message = _("Profile creation cancelled by backend")
+        elif reason == "ValueError":
+            message = _("You profile name is not valid") # TODO: print a more informative message (empty name, name starting with '@')
         else:
-            message = _("Unknown reason (%s)") % reason
-        popup = Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
+            message = _("Can't create profile ({})").format(reason)
+        popup = sat_widgets.Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
         self.host.showPopUp(popup)
 
     def deleteProfile(self, button):
-        profile_name = self.list_profile.getSelectedValue()
-        if profile_name:
-            self.host.bridge.asyncDeleteProfile(profile_name, callback=self.__refillProfiles)
+        if self.current.profile:
+            self.host.bridge.asyncDeleteProfile(self.current.profile, callback=self.refillProfiles)
+            self.resetFields()
         self.host.removePopUp()
 
     def onNewProfile(self, e):
-        pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
+        pop_up_widget = sat_widgets.InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
         self.host.showPopUp(pop_up_widget)
 
     def onDeleteProfile(self, e):
-        pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
-        self.host.showPopUp(pop_up_widget)
+        if self.current.profile:
+            pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the profile {} ?").format(self.current.profile), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
+            self.host.showPopUp(pop_up_widget)
 
-    def getXMPPParams(self, profile):
-        """This is called from PrimitivusApp.launchAction when the profile has been authenticated.
+    def resetFields(self):
+        """Set profile to None, and reset fields"""
+        self.current.profile=None
+        self.login_wid.set_edit_text("")
+        self.pass_wid.set_edit_text("")
+        self.list_profile.unselectAll(invisible=True)
+
+    def getConnectionParams(self, profile):
+        """Get login and password and display them
 
         @param profile: %(doc_profile)s
         """
         def setJID(jabberID):
             self.login_wid.set_edit_text(jabberID)
-            self.host.redraw()
+            self.current.login = jabberID
+            self.host.redraw() # FIXME: redraw should be avoided
 
         def setPassword(password):
             self.pass_wid.set_edit_text(password)
+            self.current.password = password
             self.host.redraw()
 
-        self.list_profile.selectValue(profile, move_focus=False)
-        self.selected_profile = profile
         self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError)
         self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError)
 
+    def updateConnectionParams(self):
+        """Check if connection parameters have changed, and update them if so"""
+        if self.current.profile:
+            login = self.login_wid.get_edit_text()
+            password = self.pass_wid.get_edit_text()
+            if login != self.current.login and self.current.login is not None:
+                self.current.login = login
+                self.host.bridge.setParam("JabberID", login, "Connection", profile_key=self.current.profile)
+                log.info("login updated for profile [{}]".format(self.current.profile))
+            if password != self.current.password and self.current.password is not None:
+                self.current.password = password
+                self.host.bridge.setParam("Password", password, "Connection", profile_key=self.current.profile)
+                log.info("password updated for profile [{}]".format(self.current.profile))
+
     def onProfileChange(self, list_wid):
         """This is called when a profile is selected in the profile list.
 
         @param list_wid: the List widget who sent the event
         """
-        profile_name = list_wid.getSelectedValue()
-        if not profile_name or profile_name == self.selected_profile:
-            return  # avoid infinite loop
-        if self.selected_profile:
-            list_wid.selectValue(self.selected_profile, move_focus=False)
-        else:
-            list_wid.unselectAll(invisible=True)
-        self.host.redraw()
-        self.host.profile = profile_name  # FIXME: EXTREMELY DIRTY, needed for sat_frontends.tools.xmlui.XMLUI._xmluiLaunchAction
-        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'profile_manager'}, profile_key=profile_name)
+        self.updateConnectionParams()
+        focused = list_wid.focus
+        selected = focused.getState()
+        if not selected: # profile was just unselected
+            return
+        focused.setState(False, invisible=True) # we don't want the widget to be selected until we are sure we can access it
+        def authenticate_cb(callback_id, data, profile):
+            if C.bool(data['validated']):
+                self.current.profile = profile
+                focused.setState(True, invisible=True)
+                self.getConnectionParams(profile)
+                self.host.redraw()
+        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text)
 
-    def onConnectProfile(self, button):
-        profile_name = self.list_profile.getSelectedValue()
-        assert(profile_name == self.selected_profile)  # if not, there's a bug somewhere...
-        if not profile_name:
-            pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog)
+    def onConnectProfiles(self, button):
+        """Connect the profiles and start the main widget
+
+        @param button: the connect button
+        """
+        if self._autoconnect:
+            pop_up_widget = sat_widgets.Alert(_('Internal error'), _('You can connect manually and automatically at the same time'), ok_cb=self.cancelDialog)
             self.host.showPopUp(pop_up_widget)
-        elif profile_name[0] == '@':
-            pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog)
+            return
+        self.updateConnectionParams()
+        profiles = self.list_profile.getSelectedValues()
+        if not profiles:
+            pop_up_widget = sat_widgets.Alert(_('No profile selected'), _('You need to create and select at least one profile before connecting'), ok_cb=self.cancelDialog)
             self.host.showPopUp(pop_up_widget)
         else:
-            profile = self.host.bridge.getProfileName(profile_name)
-            assert(profile)
-            #TODO: move this to quick_app
-            self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
-                                            callback=lambda old_jid: self.__old_jidReceived(old_jid, profile), errback=self.getParamError)
-
-    def __old_jidReceived(self, old_jid, profile):
-        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile,
-                                        callback=lambda old_pass: self.__old_passReceived(old_jid, old_pass, profile), errback=self.getParamError)
+            # All profiles in the list are already validated, so we can plug them directly
+            self.host.plug_profiles(profiles)
 
-    def __old_passReceived(self, old_jid, old_pass, profile):
-        """Check if we have new jid/pass, save them if it is the case, and plug profile"""
-        new_jid = self.login_wid.get_edit_text()
-        new_pass = self.pass_wid.get_edit_text()
-
-        if old_jid != new_jid:
-            self.host.bridge.setParam("JabberID", new_jid, "Connection", profile_key=profile)
-        if old_pass != new_pass:
-            self.host.bridge.setParam("Password", new_pass, "Connection", profile_key=profile)
-        self.host.plug_profile(profile)
-
-    def getParamError(self, ignore):
-        popup = Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
+    def getParamError(self, dummy):
+        popup = sat_widgets.Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
         self.host.showPopUp(popup)
--- a/frontends/src/primitivus/progress.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/progress.py	Wed Dec 10 19:00:09 2014 +0100
@@ -20,11 +20,15 @@
 from sat.core.i18n import _
 import urwid
 from urwid_satext import sat_widgets
+from sat_frontends.quick_frontend import quick_widgets
 
 
-class Progress(urwid.WidgetWrap):
+class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
+    PROFILES_ALLOW_NONE = True
 
-    def __init__(self, host):
+    def __init__(self, host, target, profiles):
+        assert target is None and profiles is None
+        quick_widgets.QuickWidget.__init__(self, host, target)
         self.host = host
         self.progress_list = urwid.SimpleListWalker([])
         self.progress_dict = {}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/primitivus/widget.py	Wed Dec 10 19:00:09 2014 +0100
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Primitivus: a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+import urwid
+from urwid_satext import sat_widgets
+from sat_frontends.primitivus.keys import action_key_map as a_key
+
+
+class PrimitivusWidget(urwid.WidgetWrap):
+    """Base widget for Primitivus"""
+
+    def __init__(self, w, title=''):
+        self._title = title
+        self._title_dynamic = None
+        self._original_widget = w
+        urwid.WidgetWrap.__init__(self, self._getDecoration(w))
+
+    @property
+    def title(self):
+        """Text shown in title bar of the widget"""
+
+        # profiles currently managed by frontend
+        try:
+            all_profiles = self.host.profiles
+        except AttributeError:
+            all_profiles = []
+
+        # profiles managed by the widget
+        try:
+            profiles = self.profiles
+        except AttributeError:
+            try:
+                profiles = [self.profile]
+            except AttributeError:
+                profiles = []
+
+        title_elts = []
+        if self._title:
+            title_elts.append(self._title)
+        if self._title_dynamic:
+            title_elts.append(self._title_dynamic)
+        if len(all_profiles)>1 and profiles:
+            title_elts.append(u'[{}]'.format(u', '.join(profiles)))
+        return sat_widgets.SurroundedText(u' '.join(title_elts))
+
+    @title.setter
+    def title(self, value):
+        self._title = value
+        if self.decorationVisible:
+            self.showDecoration()
+
+    @property
+    def title_dynamic(self):
+        """Dynamic part of title"""
+        return self._title_dynamic
+
+    @title_dynamic.setter
+    def title_dynamic(self, value):
+        self._title_dynamic = value
+        if self.decorationVisible:
+            self.showDecoration()
+
+    @property
+    def decorationVisible(self):
+        """True if the decoration is visible"""
+        return isinstance(self._w, sat_widgets.LabelLine)
+
+
+    def keypress(self, size, key):
+        if key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
+            show = not self.decorationVisible
+            self.showDecoration(show)
+        else:
+            return super(PrimitivusWidget, self).keypress(size, key)
+
+    def _getDecoration(self, widget):
+        return sat_widgets.LabelLine(widget, self.title)
+
+    def showDecoration(self, show=True):
+        """Show/Hide the decoration around the window"""
+        self._w = self._getDecoration(self._original_widget) if show else self._original_widget
+
+    def getMenu(self):
+        raise NotImplementedError
--- a/frontends/src/primitivus/xmlui.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/xmlui.py	Wed Dec 10 19:00:09 2014 +0100
@@ -26,6 +26,7 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat_frontends.primitivus.constants import Const as C
+from sat_frontends.primitivus.widget import PrimitivusWidget
 from sat_frontends.tools import xmlui
 
 
@@ -355,14 +356,14 @@
             return cls
 
 
-class XMLUIPanel(xmlui.XMLUIPanel, urwid.WidgetWrap):
+class XMLUIPanel(xmlui.XMLUIPanel, PrimitivusWidget):
     widget_factory = WidgetFactory()
 
-    def __init__(self, host, parsed_xml, title = None, flags = None):
+    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
         self.widget_factory._xmlui_main = self
         self._dest = None
-        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags)
-        urwid.WidgetWrap.__init__(self, self.main_cont)
+        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags, callback, profile)
+        PrimitivusWidget.__init__(self, self.main_cont, self.xmlui_title)
 
     def constructUI(self, parsed_dom):
         def postTreat():
@@ -412,11 +413,10 @@
             raise ValueError('Invalid show_type [%s]' % show_type)
 
         self._dest = show_type
-        decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or ''))
         if show_type == 'popup':
-            self.host.showPopUp(decorated, valign=valign)
+            self.host.showPopUp(self, valign=valign)
         elif show_type == 'window':
-            self.host.addWindow(decorated)
+            self.host.newWidget(self)
         else:
             assert(False)
         self.host.redraw()
--- a/frontends/src/quick_frontend/constants.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/quick_frontend/constants.py	Wed Dec 10 19:00:09 2014 +0100
@@ -22,4 +22,15 @@
 
 class Const(constants.Const):
 
-    PRIVATE_PREFIX = "@private@"
+    #Chats
+    CHAT_ONE2ONE = 'one2one'
+    CHAT_GROUP = 'group'
+
+    #Contact list
+    CONTACT_GROUPS = 'groups'
+    CONTACT_RESOURCES = 'resources'
+    CONTACT_MAIN_RESOURCE = 'main_resource'
+    CONTACT_SPECIAL = 'special'
+    CONTACT_SPECIAL_GROUP = 'group' # group chat special entity
+    CONTACT_SPECIAL_ALLOWED = (CONTACT_SPECIAL_GROUP,) # set of allowed values for special flag
+    CONTACT_DATA_FORBIDDEN = {CONTACT_GROUPS, CONTACT_RESOURCES, CONTACT_MAIN_RESOURCE} # set of forbidden names for contact data
--- a/frontends/src/quick_frontend/quick_app.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/quick_frontend/quick_app.py	Wed Dec 10 19:00:09 2014 +0100
@@ -21,23 +21,128 @@
 import sys
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat_frontends.tools.jid import JID
+from sat.core import exceptions
 from sat_frontends.bridge.DBus import DBusBridgeFrontend
-from sat.core import exceptions
-from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager
+from sat_frontends.quick_frontend import quick_chat
 from optparse import OptionParser
 
 from sat_frontends.quick_frontend.constants import Const as C
 
 
+class ProfileManager(object):
+    """Class managing all data relative to one profile, and plugging in mechanism"""
+    host = None
+    bridge = None
+
+    def __init__(self, profile):
+        self.profile = profile
+        self.whoami = None
+        self.data = {}
+
+    def __getitem__(self, key):
+        return self.data[key]
+
+    def __setitem__(self, key, value):
+        self.data[key] = value
+
+    def plug(self):
+        """Plug the profile to the host"""
+        # we get the essential params
+        self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile,
+                                   callback=self._plug_profile_jid, errback=self._getParamError)
+
+    def _plug_profile_jid(self, _jid):
+        self.whoami = jid.JID(_jid)
+        self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile,
+                                   callback=self._plug_profile_autoconnect, errback=self._getParamError)
+
+    def _plug_profile_autoconnect(self, value_str):
+        autoconnect = C.bool(value_str)
+        if autoconnect and not self.bridge.isConnected(self.profile):
+            self.host.asyncConnect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect())
+        else:
+            self._plug_profile_afterconnect()
+
+    def _plug_profile_afterconnect(self):
+        # Profile can be connected or not
+        # TODO: watched plugin
+        contact_list = self.host.addContactList(self.profile)
+
+        if not self.bridge.isConnected(self.profile):
+            self.host.setStatusOnline(False, profile=self.profile)
+        else:
+            self.host.setStatusOnline(True, profile=self.profile)
+
+            contact_list.fill()
+
+            #The waiting subscription requests
+            waitingSub = self.bridge.getWaitingSub(self.profile)
+            for sub in waitingSub:
+                self.host.subscribeHandler(waitingSub[sub], sub, self.profile)
+
+            #Now we open the MUC window where we already are:
+            for room_args in self.bridge.getRoomsJoined(self.profile):
+                self.host.roomJoinedHandler(*room_args, profile=self.profile)
+
+            for subject_args in self.bridge.getRoomsSubjects(self.profile):
+                self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
+
+            #Finaly, we get the waiting confirmation requests
+            for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile):
+                self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
+
+    def _getParamError(self, ignore):
+        log.error(_("Can't get profile parameter"))
+
+
+class ProfilesManager(object):
+    """Class managing collection of profiles"""
+
+    def __init__(self):
+        self._profiles = {}
+
+    def __contains__(self, profile):
+        return profile in self._profiles
+
+    def __iter__(self):
+        return self._profiles.iterkeys()
+
+    def __getitem__(self, profile):
+        return self._profiles[profile]
+
+    def __len__(self):
+        return len(self._profiles)
+
+    def plug(self, profile):
+        if profile in self._profiles:
+            raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile))
+        self._profiles[profile] = ProfileManager(profile)
+        self._profiles[profile].plug()
+
+    def unplug(self, profile):
+        if profile not in self._profiles:
+            raise ValueError('The profile [{}] is not plugged'.format(profile))
+        del self._profiles[profile]
+
+    def chooseOneProfile(self):
+        return self._profiles.keys()[0]
+
 class QuickApp(object):
     """This class contain the main methods needed for the frontend"""
 
-    def __init__(self, single_profile=True):
-        self.profiles = {}
-        self.single_profile = single_profile
+    def __init__(self):
+        ProfileManager.host = self
+        self.profiles = ProfilesManager()
+        self.contact_lists = {}
+        self.widgets = QuickWidgetsManager(self)
         self.check_options()
 
+        # widgets
+        self.visible_widgets = set() # widgets currently visible (must be filled by frontend)
+        self.selected_widget = None # widget currently selected (must be filled by frontend)
+
         ## bridge ##
         try:
             self.bridge = DBusBridgeFrontend()
@@ -47,6 +152,7 @@
         except exceptions.BridgeInitError:
             print(_(u"Can't init bridge"))
             sys.exit(1)
+        ProfileManager.bridge = self.bridge
         self.registerSignal("connected")
         self.registerSignal("disconnected")
         self.registerSignal("newContact")
@@ -84,10 +190,18 @@
         self.registerSignal("quizGameTimerRestarted", iface="plugin")
         self.registerSignal("chatStateReceived", iface="plugin")
 
-        self.current_action_ids = set()
-        self.current_action_ids_cb = {}
+        self.current_action_ids = set() # FIXME: to be removed
+        self.current_action_ids_cb = {} # FIXME: to be removed
         self.media_dir = self.bridge.getConfig('', 'media_dir')
 
+    @property
+    def current_profile(self):
+        """Profile that a user would expect to use"""
+        try:
+            return self.selected_widget.profile
+        except (TypeError, AttributeError):
+            return self.profiles.chooseOneProfile()
+
     def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
         """Register a handler for a signal
 
@@ -115,15 +229,15 @@
 
     def check_profile(self, profile):
         """Tell if the profile is currently followed by the application"""
-        return profile in self.profiles.keys()
+        return profile in self.profiles
 
-    def postInit(self):
-        """Must be called after initialization is done, do all automatic task (auto plug profile)"""
+    def postInit(self, profile_manager):
+        """Must be called after initialization is done, do all automatic task (auto plug profile)
+
+        @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
+        """
         if self.options.profile:
-            if not self.bridge.getProfileName(self.options.profile):
-                log.error(_("Trying to plug an unknown profile (%s)" % self.options.profile))
-            else:
-                self.plug_profile(self.options.profile)
+            profile_manager.autoconnect([self.options.profile])
 
     def check_options(self):
         """Check command line options"""
@@ -132,7 +246,7 @@
 
         %prog --help for options list
         """)
-        parser = OptionParser(usage=usage)
+        parser = OptionParser(usage=usage) # TODO: use argparse
 
         parser.add_option("-p", "--profile", help=_("Select the profile to use"))
 
@@ -141,46 +255,6 @@
             self.options.profile = self.options.profile.decode('utf-8')
         return args
 
-    def _getParamError(self, ignore):
-        log.error(_("Can't get profile parameter"))
-
-    def plug_profile(self, profile_key='@DEFAULT@'):
-        """Tell application which profile must be used"""
-        if self.single_profile and self.profiles:
-            log.error(_('There is already one profile plugged (we are in single profile mode) !'))
-            return
-        profile = self.bridge.getProfileName(profile_key)
-        if not profile:
-            log.error(_("The profile asked doesn't exist"))
-            return
-        if profile in self.profiles:
-            log.warning(_("The profile is already plugged"))
-            return
-        self.profiles[profile] = {}
-        if self.single_profile:
-            self.profile = profile # FIXME: must be refactored (multi profiles are not managed correclty)
-        raw_menus = self.bridge.getMenus("", C.NO_SECURITY_LIMIT )
-        menus = self.profiles[profile]['menus'] = {}
-        for raw_menu in raw_menus:
-            id_, type_, path, path_i18n  = raw_menu
-            menus_data = menus.setdefault(type_, [])
-            menus_data.append((id_, path, path_i18n))
-        self.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'plug_profile'}, profile_key=profile)
-
-    def plug_profile_1(self, profile):
-        ###now we get the essential params###
-        self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
-                                   callback=lambda _jid: self.plug_profile_2(_jid, profile), errback=self._getParamError)
-
-    def plug_profile_2(self, _jid, profile):
-        self.profiles[profile]['whoami'] = JID(_jid)
-        self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=profile,
-                                   callback=lambda value: self.plug_profile_3(value == "true", profile), errback=self._getParamError)
-
-    def plug_profile_3(self, autoconnect, profile):
-        self.bridge.asyncGetParamA("Watched", "Misc", profile_key=profile,
-                                   callback=lambda watched: self.plug_profile_4(watched, autoconnect, profile), errback=self._getParamError)
-
     def asyncConnect(self, profile, callback=None, errback=None):
         if not callback:
             callback = lambda dummy: None
@@ -188,134 +262,74 @@
             def errback(failure):
                 log.error(_(u"Can't connect profile [%s]") % failure)
                 if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
-                    self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile_key=profile)
+                    self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
                 else:
                     self.showDialog(failure.message, failure.fullname, 'error')
         self.bridge.asyncConnect(profile, callback=callback, errback=errback)
 
-    def plug_profile_4(self, watched, autoconnect, profile):
-        if autoconnect and not self.bridge.isConnected(profile):
-            #Does the user want autoconnection ?
-            self.asyncConnect(profile, callback=lambda dummy: self.plug_profile_5(watched, autoconnect, profile))
-        else:
-            self.plug_profile_5(watched, autoconnect, profile)
-
-    def plug_profile_5(self, watched, autoconnect, profile):
-        self.profiles[profile]['watched'] = watched.split()  # TODO: put this in a plugin
-
-        ## misc ##
-        self.profiles[profile]['onlineContact'] = set()  # FIXME: temporary
-
-        #TODO: manage multi-profiles here
-        if not self.bridge.isConnected(profile):
-            self.setStatusOnline(False)
-        else:
-            self.setStatusOnline(True)
-
-            ### now we fill the contact list ###
-            for contact in self.bridge.getContacts(profile):
-                self.newContactHandler(*contact, profile=profile)
+    def plug_profiles(self, profiles):
+        """Tell application which profiles must be used
 
-            presences = self.bridge.getPresenceStatuses(profile)
-            for contact in presences:
-                for res in presences[contact]:
-                    jabber_id = ('%s/%s' % (JID(contact).bare, res)) if res else contact
-                    show = presences[contact][res][0]
-                    priority = presences[contact][res][1]
-                    statuses = presences[contact][res][2]
-                    self.presenceUpdateHandler(jabber_id, show, priority, statuses, profile)
-                data = self.bridge.getEntityData(contact, ['avatar', 'nick'], profile)
-                for key in ('avatar', 'nick'):
-                    if key in data:
-                        self.entityDataUpdatedHandler(contact, key, data[key], profile)
+        @param profiles: list of valid profile names
+        """
+        self.plugging_profiles()
+        for profile in profiles:
+            self.profiles.plug(profile)
 
-            #The waiting subscription requests
-            waitingSub = self.bridge.getWaitingSub(profile)
-            for sub in waitingSub:
-                self.subscribeHandler(waitingSub[sub], sub, profile)
+    def plugging_profiles(self):
+        """Method to subclass to manage frontend specific things to do
 
-            #Now we open the MUC window where we already are:
-            for room_args in self.bridge.getRoomsJoined(profile):
-                self.roomJoinedHandler(*room_args, profile=profile)
-
-            for subject_args in self.bridge.getRoomsSubjects(profile):
-                self.roomNewSubjectHandler(*subject_args, profile=profile)
-
-            #Finaly, we get the waiting confirmation requests
-            for confirm_id, confirm_type, data in self.bridge.getWaitingConf(profile):
-                self.askConfirmationHandler(confirm_id, confirm_type, data, profile)
+        will be called when profiles are choosen and are to be plugged soon
+        """
+        raise NotImplementedError
 
     def unplug_profile(self, profile):
         """Tell the application to not follow anymore the profile"""
         if not profile in self.profiles:
-            log.warning(_("This profile is not plugged"))
-            return
+            raise ValueError("The profile [{}] is not plugged".format(profile))
         self.profiles.remove(profile)
 
     def clear_profile(self):
         self.profiles.clear()
 
+    def newWidget(self, widget):
+        raise NotImplementedError
+
     def connectedHandler(self, profile):
         """called when the connection is made"""
         log.debug(_("Connected"))
-        self.setStatusOnline(True)
+        self.setStatusOnline(True, profile=profile)
 
     def disconnectedHandler(self, profile):
         """called when the connection is closed"""
         log.debug(_("Disconnected"))
-        self.contact_list.clearContacts()
-        self.setStatusOnline(False)
+        self.contact_lists[profile].clearContacts()
+        self.setStatusOnline(False, profile=profile)
 
     def newContactHandler(self, JabberId, attributes, groups, profile):
-        entity = JID(JabberId)
+        entity = jid.JID(JabberId)
         _groups = list(groups)
-        self.contact_list.replace(entity, _groups, attributes)
+        self.contact_lists[profile].setContact(entity, _groups, attributes, in_roster=True)
 
     def _newMessage(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
-        """newMessage premanagement: a dirty hack to manage private messages
-
-        if a private MUC message is detected, from_jid or to_jid is prefixed and resource is escaped
-        """
-        # FIXME: must be refactored for 0.6
-        from_jid = JID(from_jid_s)
-        to_jid = JID(to_jid_s)
-
-        from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
-        win = to_jid if from_me else from_jid
-
-        if ((type_ != "groupchat" and self.contact_list.getSpecial(win) == "MUC") and
-            (type_ != C.MESS_TYPE_INFO or (type_ == C.MESS_TYPE_INFO and win.resource))):
-            #we have a private message in a MUC room
-            #XXX: normaly we use bare jid as key, here we need the full jid
-            #     so we cheat by replacing the "/" before the resource by
-            #     a "@", so the jid is invalid,
-            new_jid = escapePrivate(win)
-            if from_me:
-                to_jid = new_jid
-            else:
-                from_jid = new_jid
-            if new_jid not in self.contact_list:
-                self.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
-
+        from_jid = jid.JID(from_jid_s)
+        to_jid = jid.JID(to_jid_s)
         self.newMessageHandler(from_jid, to_jid, msg, type_, extra, profile)
 
     def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
-        from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
-        win = to_jid if from_me else from_jid
+        from_me = from_jid.bare == self.profiles[profile].whoami.bare
+        target = to_jid if from_me else from_jid
 
-        self.current_action_ids = set()
-        self.current_action_ids_cb = {}
+        chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
+
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, profile=profile)
 
-        timestamp = extra.get('archive')
-        if type_ == C.MESS_TYPE_INFO:
-            self.chat_wins[win.bare].printInfo(msg, timestamp=float(timestamp) if timestamp else None)
-        else:
-            self.chat_wins[win.bare].printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
+        self.current_action_ids = set() # FIXME: to be removed
+        self.current_action_ids_cb = {} # FIXME: to be removed
 
-    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key="@NONE@"):
-        if to_jid.startswith(C.PRIVATE_PREFIX):
-            to_jid = unescapePrivate(to_jid)
-            mess_type = "chat"
+        chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
+
+    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
         if callback is None:
             callback = lambda: None
         if errback is None:
@@ -326,100 +340,78 @@
         assert alert_type in ['INFO', 'ERROR']
         self.showDialog(unicode(msg), unicode(title), alert_type.lower())
 
-    def setStatusOnline(self, online=True, show="", statuses={}):
+    def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
         raise NotImplementedError
 
-    def presenceUpdateHandler(self, jabber_id, show, priority, statuses, profile):
+    def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
 
-        log.debug(_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
-              % {'jid': jabber_id, 'show': show, 'priority': priority, 'statuses': statuses, 'profile': profile})
-        from_jid = JID(jabber_id)
+        log.debug(_("presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
+              % {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile})
+        entity = jid.JID(entity_s)
 
-        if from_jid == self.profiles[profile]['whoami']:
+        if entity == self.profiles[profile].whoami:
             if show == "unavailable":
-                self.setStatusOnline(False)
+                self.setStatusOnline(False, profile=profile)
             else:
-                self.setStatusOnline(True, show, statuses)
+                self.setStatusOnline(True, show, statuses, profile=profile)
             return
 
-        presences = self.profiles[profile].setdefault('presences', {})
-
-        if show != 'unavailable':
-
-            #FIXME: must be moved in a plugin
-            if from_jid.bare in self.profiles[profile].get('watched',[]) and not from_jid.bare in self.profiles[profile]['onlineContact']:
-                self.showAlert(_("Watched jid [%s] is connected !") % from_jid.bare)
+        # #FIXME: must be moved in a plugin
+        # if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']:
+        #     self.showAlert(_("Watched jid [%s] is connected !") % entity.bare)
 
-            presences[jabber_id] = {'show': show, 'priority': priority, 'statuses': statuses}
-            self.profiles[profile].setdefault('onlineContact',set()).add(from_jid)  # FIXME onlineContact is useless with CM, must be removed
-
-            #TODO: vcard data (avatar)
-
-        if show == "unavailable" and from_jid in self.profiles[profile].get('onlineContact',set()):
-            try:
-                del presences[jabber_id]
-            except KeyError:
-                pass
-            self.profiles[profile]['onlineContact'].remove(from_jid)
+        self.contact_lists[profile].updatePresence(entity, show, priority, statuses)
 
-        # check if the contact is connected with another resource, use the one with highest priority
-        jids = [jid for jid in presences if JID(jid).bare == from_jid.bare]
-        if jids:
-            max_jid = max(jids, key=lambda jid: presences[jid]['priority'])
-            data = presences[max_jid]
-            max_priority = data['priority']
-            if show == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
-                from_jid = JID(max_jid)
-                show, priority, statuses = data['show'], data['priority'], data['statuses']
-        if not jids or priority >= max_priority:
-            # case 1: not jids means all resources are disconnected, send the 'unavailable' presence
-            # case 2: update (or confirm) with the values of the resource which takes precedence
-            self.contact_list.updatePresence(from_jid, show, priority, statuses)
-
-    def roomJoinedHandler(self, room_jid, room_nicks, user_nick, profile):
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
         """Called when a MUC room is joined"""
-        log.debug(_("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s") % {'room_jid': room_jid, 'profile': profile, 'users': room_nicks})
-        self.chat_wins[room_jid].setUserNick(user_nick)
-        self.chat_wins[room_jid].setType("group")
-        self.chat_wins[room_jid].id = room_jid
-        self.chat_wins[room_jid].setPresents(list(set([user_nick] + room_nicks)))
-        self.contact_list.setSpecial(JID(room_jid), "MUC", show=True)
+        log.debug("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
+        chat_widget.setUserNick(user_nick)
+        chat_widget.id = room_jid # FIXME: to be removed
+        chat_widget.setPresents(list(set([user_nick] + room_nicks)))
+        self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
 
     def roomLeftHandler(self, room_jid_s, profile):
         """Called when a MUC room is left"""
-        log.debug(_("Room [%(room_jid)s] left by %(profile)s") % {'room_jid': room_jid_s, 'profile': profile})
+        log.debug("Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile})
         del self.chat_wins[room_jid_s]
-        self.contact_list.remove(JID(room_jid_s))
+        self.contact_lists[profile].remove(jid.JID(room_jid_s))
 
-    def roomUserJoinedHandler(self, room_jid, user_nick, user_data, profile):
+    def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
         """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].replaceUser(user_nick)
-            log.debug(_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
+        chat_widget.replaceUser(user_nick)
+        log.debug("user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
 
-    def roomUserLeftHandler(self, room_jid, user_nick, user_data, profile):
+    def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
         """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].removeUser(user_nick)
-            log.debug(_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
+        chat_widget.removeUser(user_nick)
+        log.debug("user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
 
-    def roomUserChangedNickHandler(self, room_jid, old_nick, new_nick, profile):
+    def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
         """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].changeUserNick(old_nick, new_nick)
-            log.debug(_("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]") % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
+        chat_widget.changeUserNick(old_nick, new_nick)
+        log.debug("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
 
-    def roomNewSubjectHandler(self, room_jid, subject, profile):
+    def roomNewSubjectHandler(self, room_jid_s, subject, profile):
         """Called when subject of MUC room change"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].setSubject(subject)
-            log.debug(_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid': room_jid, "subject": subject})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
+        chat_widget.setSubject(subject)
+        log.debug("new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject})
 
-    def tarotGameStartedHandler(self, room_jid, referee, players, profile):
+    def tarotGameStartedHandler(self, room_jid_s, referee, players, profile):
         log.debug(_("Tarot Game Started \o/"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].startGame("Tarot", referee, players)
-            log.debug(_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
+        chat_widget.startGame("Tarot", referee, players)
+        log.debug("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s" % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
 
     def tarotGameNewHandler(self, room_jid, hand, profile):
         log.debug(_("New Tarot Game"))
@@ -501,27 +493,17 @@
             self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestartedHandler(time_left)
 
     def chatStateReceivedHandler(self, from_jid_s, state, profile):
-        """Callback when a new chat state is received.
+        """Called when a new chat state is received.
+
         @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
         @param state: new state (string)
         @profile: current profile
         """
-
-        if from_jid_s == '@ALL@':
-            target = '@ALL@'
-            nick = C.ALL_OCCUPANTS
-        else:
-            from_jid = JID(from_jid_s)
-            target = from_jid.bare
-            nick = from_jid.resource
-
-        for bare in self.chat_wins.keys():
-            if target == '@ALL' or target == bare:
-                chat_win = self.chat_wins[bare]
-                if chat_win.type == 'one2one':
-                    chat_win.updateChatState(state)
-                elif chat_win.type == 'group':
-                    chat_win.updateChatState(state, nick=nick)
+        from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
+        for widget in self.visible_widgets:
+            if isinstance(widget, quick_chat.QuickChat):
+                if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
+                    widget.updateChatState(from_jid, state)
 
     def _subscribe_cb(self, answer, data):
         entity, profile = data
@@ -532,7 +514,7 @@
 
     def subscribeHandler(self, type, raw_jid, profile):
         """Called when a subsciption management signal is received"""
-        entity = JID(raw_jid)
+        entity = jid.JID(raw_jid)
         if type == "subscribed":
             # this is a subscription confirmation, we just have to inform user
             self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation'))
@@ -553,33 +535,27 @@
         log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
         if (namespace, name) == ("Connection", "JabberID"):
             log.debug(_("Changing JID to %s") % value)
-            self.profiles[profile]['whoami'] = JID(value)
+            self.profiles[profile].whoami = jid.JID(value)
         elif (namespace, name) == ("Misc", "Watched"):
             self.profiles[profile]['watched'] = value.split()
         elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS):
-            self.contact_list.showOfflineContacts(C.bool(value))
+            self.contact_lists[profile].showOfflineContacts(C.bool(value))
         elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS):
-            self.contact_list.showEmptyGroups(C.bool(value))
+            self.contact_lists[profile].showEmptyGroups(C.bool(value))
 
     def contactDeletedHandler(self, jid, profile):
-        target = JID(jid)
-        self.contact_list.remove(target)
-        try:
-            self.profiles[profile]['onlineContact'].remove(target.bare)
-        except KeyError:
-            pass
+        target = jid.JID(jid)
+        self.contact_lists[profile].remove(target)
 
-    def entityDataUpdatedHandler(self, jid_str, key, value, profile):
-        jid = JID(jid_str)
+    def entityDataUpdatedHandler(self, entity_s, key, value, profile):
+        entity = jid.JID(entity_s)
         if key == "nick":
-            if jid in self.contact_list:
-                self.contact_list.setCache(jid, 'nick', value)
-                self.contact_list.replace(jid)
+            if entity in self.contact_lists[profile]:
+                self.contact_lists[profile].setCache(entity, 'nick', value)
         elif key == "avatar":
-            if jid in self.contact_list:
+            if entity in self.contact_lists[profile]:
                 filename = self.bridge.getAvatarFile(value)
-                self.contact_list.setCache(jid, 'avatar', filename)
-                self.contact_list.replace(jid)
+                self.contact_lists[profile].setCache(entity, 'avatar', filename)
 
     def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
         raise NotImplementedError
@@ -587,22 +563,23 @@
     def actionResultHandler(self, type, id, data, profile):
         raise NotImplementedError
 
-    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
-        """ Launch a dynamic action
+    def launchAction(self, callback_id, data=None, callback=None, profile="@NONE@"):
+        """Launch a dynamic action
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
-        @param profile_key: %(doc_profile_key)s
+        @param callback: if not None and 'validated' key is present, it will be called with the following parameters:
+            - callback_id
+            - data
+            - profile_key
+        @param profile_key: %(doc_profile)s
 
         """
         raise NotImplementedError
 
     def onExit(self):
         """Must be called when the frontend is terminating"""
-        #TODO: mange multi-profile here
-        try:
-            if self.bridge.isConnected(self.profile):
-                if self.bridge.getParamA("autodisconnect", "Connection", profile_key=self.profile) == "true":
+        for profile in self.profiles:
+            if self.bridge.isConnected(profile):
+                if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)):
                     #The user wants autodisconnection
-                    self.bridge.disconnect(self.profile)
-        except:
-            pass
+                    self.bridge.disconnect(profile)
--- a/frontends/src/quick_frontend/quick_chat.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/quick_frontend/quick_chat.py	Wed Dec 10 19:00:09 2014 +0100
@@ -21,32 +21,75 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat_frontends.tools.jid  import JID
-from sat_frontends.quick_frontend.quick_utils import unescapePrivate
+from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.quick_frontend.constants import Const as C
 
 
-class QuickChat(object):
+class QuickChat(quick_widgets.QuickWidget):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+        """
+        @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
+        """
 
-    def __init__(self, target, host, type_='one2one'):
-        self.target = target
-        self.host = host
+        quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
+        assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
+        if type_ == C.CHAT_GROUP and target.resource:
+            raise ValueError("A group chat entity can't have a resource")
+        self.current_target = target
         self.type = type_
-        self.id = ""
+        self.id = "" # FIXME: to be removed
         self.nick = None
         self.occupants = set()
 
-    def setType(self, type_):
-        """Set the type of the chat
-        @param type: can be 'one2one' for single conversation or 'group' for chat à la IRC
+    def __str__(self):
+        return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
+
+    @staticmethod
+    def getWidgetHash(target, profile):
+        return (unicode(profile), target.bare)
+
+    @staticmethod
+    def getPrivateHash(target, profile):
+        """Get unique hash for private conversations
+
+        This method should be used with force_hash to get unique widget for private MUC conversations
         """
-        self.type = type_
+        return (unicode(profile), target)
+
+
+    def addTarget(self, target):
+        super(QuickChat, self).addTarget(target)
+        if target.resource:
+            self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead
+
+    @property
+    def target(self):
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    def manageMessage(self, entity, mess_type):
+        """Tell if this chat widget manage an entity and message type couple
+
+        @param entity (jid.JID): (full) jid of the sending entity
+        @param mess_type (str): message type as given by newMessage
+        @return (bool): True if this Chat Widget manage this couple
+        """
+        if self.type == C.CHAT_GROUP:
+            if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
+                return True
+        else:
+            if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
+                return True
+        return False
 
     def setPresents(self, nicks):
         """Set the users presents in the contact list for a group chat
         @param nicks: list of nicknames
         """
         log.debug (_("Adding users %s to room") % nicks)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to set presents nicks for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         self.occupants.update(nicks)
@@ -54,7 +97,7 @@
     def replaceUser(self, nick, show_info=True):
         """Add user if it is not in the group list"""
         log.debug (_("Replacing user %s") % nick)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to replace user for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         len_before = len(self.occupants)
@@ -65,7 +108,7 @@
     def removeUser(self, nick, show_info=True):
         """Remove a user from the group list"""
         log.debug(_("Removing user %s") % nick)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to remove user for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         self.occupants.remove(nick)
@@ -82,7 +125,7 @@
     def changeUserNick(self, old_nick, new_nick):
         """Change nick of a user in group list"""
         log.debug(_("Changing nick of user %(old_nick)s to %(new_nick)s") % {"old_nick": old_nick, "new_nick": new_nick})
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to change user nick for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
         self.removeUser(old_nick, show_info=False)
@@ -92,7 +135,7 @@
     def setSubject(self, subject):
         """Set title for a group chat"""
         log.debug(_("Setting subject to %s") % subject)
-        if self.type != "group":
+        if self.type != C.CHAT_GROUP:
             log.error (_("[INTERNAL] trying to set subject for a non group chat window"))
             raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
 
@@ -111,8 +154,8 @@
         def onHistory(history):
             for line in history:
                 timestamp, from_jid, to_jid, message, _type, extra = line
-                if ((self.type == 'group' and _type != 'groupchat') or
-                   (self.type == 'one2one' and _type == 'groupchat')):
+                if ((self.type == C.CHAT_GROUP and _type != C.MESS_TYPE_GROUPCHAT) or
+                   (self.type == C.CHAT_ONE2ONE and _type == C.MESS_TYPE_GROUPCHAT)):
                     continue
                 self.printMessage(JID(from_jid), message, profile, timestamp)
             self.afterHistoryPrint()
@@ -120,26 +163,47 @@
         def onHistoryError(err):
             log.error(_("Can't get history"))
 
-        if self.target.startswith(C.PRIVATE_PREFIX):
-            target = unescapePrivate(self.target)
-        else:
-            target = self.target.bare
+        target = self.target.bare
+
+        return self.host.bridge.getHistory(self.host.profiles[profile].whoami.bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)
 
-        return self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)
+    def _get_nick(self, entity):
+        """Return nick of this entity when possible"""
+        if self.type == C.CHAT_GROUP:
+            return entity.resource
+        contact_list = self.host.contact_lists[self.profile]
+        if entity.bare in contact_list:
+            return contact_list.getCache(entity,'nick') or contact_list.getCache(entity,'name') or entity.node or entity
+        return entity.node or entity
+
+    def onPrivateCreated(self, widget):
+        """Method called when a new widget for private conversation (MUC) is created"""
+        raise NotImplementedError
 
-    def _get_nick(self, jid):
-        """Return nick of this jid when possible"""
-        if self.target.startswith(C.PRIVATE_PREFIX):
-            unescaped = unescapePrivate(self.target)
-            if jid.startswith(C.PRIVATE_PREFIX) or unescaped.bare == jid.bare:
-                return unescaped.resource
-        return jid.resource if self.type == "group" else (self.host.contact_list.getCache(jid,'nick') or self.host.contact_list.getCache(jid,'name') or jid.node)
+    def getOrCreatePrivateWidget(self, entity):
+        """Create a widget for private conversation, or get it if it already exists
+
+        @param entity: full jid of the target
+        """
+        return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again
+
+    def newMessage(self, from_jid, target, msg, type_, extra, profile):
+        if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
+            # we have a private message, we forward it to a private conversation widget
+            chat_widget = self.getOrCreatePrivateWidget(target)
+            chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
+        else:
+            timestamp = extra.get('archive')
+            if type_ == C.MESS_TYPE_INFO:
+                self.printInfo(msg, timestamp=float(timestamp) if timestamp else None)
+            else:
+                self.printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
 
     def printMessage(self, from_jid, msg, profile, timestamp=None):
         """Print message in chat window. Must be implemented by child class"""
         jid = JID(from_jid)
         nick = self._get_nick(jid)
-        mymess = (jid.resource == self.nick) if self.type == "group" else (jid.bare == self.host.profiles[profile]['whoami'].bare) #mymess = True if message comes from local user
+        mymess = (jid.resource == self.nick) if self.type == C.CHAT_GROUP else (jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user
         if msg.startswith('/me '):
             self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', timestamp=timestamp)
             return
@@ -165,12 +229,11 @@
         #No need to raise an error as game are not mandatory
         log.warning(_('getGame is not implemented in this frontend'))
 
-    def updateChatState(self, state, nick=None):
-        """Set the chat state (XEP-0085) of the contact. Leave nick to None
-        to set the state for a one2one conversation, or give a nickname or
-        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
+    def updateChatState(self, from_jid, state):
+        """Set the chat state (XEP-0085) of the contact.
+
         @param state: the new chat state
-        @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS
         """
         raise NotImplementedError
 
+quick_widgets.register(QuickChat)
--- a/frontends/src/quick_frontend/quick_chat_list.py	Wed Dec 10 18:37:14 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# helper class for making a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat_frontends.tools.jid  import JID
-
-
-class QuickChatList(dict):
-    """This class is used to manage the list of chat windows.
-    It act as a dict, but create a chat window when the name is found for the first time."""
-
-    def __init__(self, host):
-        dict.__init__(self)
-        self.host = host
-
-    def __getitem__(self, to_jid):
-        target = JID(to_jid)
-        if not target.bare in self:
-            #we have to create the chat win
-            self[target.bare] = self.createChat(target)
-        return dict.__getitem__(self, target.bare)
-
-    def createChat(self, target):
-        raise NotImplementedError
--- a/frontends/src/quick_frontend/quick_contact_list.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/quick_frontend/quick_contact_list.py	Wed Dec 10 19:00:09 2014 +0100
@@ -20,41 +20,158 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
+from sat_frontends.quick_frontend.quick_widgets import QuickWidget
+from sat_frontends.quick_frontend.constants import Const as C
+from sat_frontends.tools import jid
 
 
-class QuickContactList(object):
+class QuickContactList(QuickWidget):
     """This class manage the visual representation of contacts"""
 
-    def __init__(self):
+    def __init__(self, host, profile):
         log.debug(_("Contact List init"))
+        super(QuickContactList, self).__init__(host, profile, profile)
+        # bare jids as keys, resources are used in data
         self._cache = {}
-        self.specials={}
+
+        # special entities (groupchat, gateways, etc), bare jids
+        self._specials = set()
+        # extras are specials with full jids (e.g.: private MUC conversation)
+        self._special_extras = set()
+
+        # group data contain jids in groups and misc frontend data
+        self._groups = {} # groups to group data map
+
+        # contacts in roster (bare jids)
+        self._roster = set()
+
+        # entities with an alert (usually a waiting message), full jid
+        self._alerts = set()
+
+        # selected entities, full jid
+        self._selected = set()
+
+        # options
+        self.show_disconnected = False
+        self.show_empty_groups = True
+        self.show_resources = False
+        self.show_status = False
+        # TODO: this may lead to two successive UI refresh and needs an optimization
+        self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self.showEmptyGroups)
+        self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self.showOfflineContacts)
+
+    def __contains__(self, entity):
+        """Check if entity is in contact list
 
-    def update_jid(self, jid):
-        """Update the jid in the list when something changed"""
+        @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
+        """
+        if entity.resource:
+            try:
+                return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
+            except KeyError:
+                return False
+        return entity in self._cache
+
+    def fill(self):
+        """Get all contacts from backend, and fill the widget"""
+        def gotContacts(contacts):
+            for contact in contacts:
+                self.host.newContactHandler(*contact, profile=self.profile)
+
+            presences = self.host.bridge.getPresenceStatuses(self.profile)
+            for contact in presences:
+                for res in presences[contact]:
+                    jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
+                    show = presences[contact][res][0]
+                    priority = presences[contact][res][1]
+                    statuses = presences[contact][res][2]
+                    self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
+                data = self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile)
+                for key in ('avatar', 'nick'):
+                    if key in data:
+                        self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
+        self.host.bridge.getContacts(self.profile, callback=gotContacts)
+
+    def update(self):
+        """Update the display when something changed"""
         raise NotImplementedError
 
-    def getCache(self, jid, name):
+    def getCache(self, entity, name=None):
+        """Return a cache value for a contact
+
+        @param entity(entity.entity): entity of the contact from who we want data (resource is used if given)
+            if a resource specific information is requested:
+                - if no resource is given (bare jid), the main resource is used, according to priority
+                - if resource is given, it is used
+        @param name(unicode): name the data to get, or None to get everything
+        """
+        cache = self._cache[entity.bare]
+        if name is None:
+            return cache
         try:
-            jid_cache = self._cache[jid.bare]
-            if name == 'status': #XXX: we get the first status for 'status' key
-                return jid_cache['statuses'].get('default','')
-            return jid_cache[name]
-        except (KeyError, IndexError):
+            if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
+                # these data are related to the resource
+                if not entity.resource:
+                    main_resource = cache[C.CONTACT_MAIN_RESOURCE]
+                    cache = cache[C.CONTACT_RESOURCES][main_resource]
+                else:
+                    cache = cache[C.CONTACT_RESOURCES][entity.resource]
+
+                if name == 'status': #XXX: we get the first status for 'status' key
+                    # TODO: manage main language for statuses
+                    return cache[C.PRESENCE_STATUSES].get('default','')
+
+            return cache[name]
+        except KeyError:
             return None
 
-    def setCache(self, jid, name, value):
-        jid_cache = self._cache.setdefault(jid.bare, {})
-        jid_cache[name] = value
+    def setCache(self, entity, name, value):
+        """Set or update value for one data in cache
+
+        @param entity(JID): entity to update
+        @param name(unicode): value to set or update
+        """
+        self.setContact(entity, None, {name: value})
+
+    def setGroupData(self, group, name, value):
+        """Register a data for a group
+
+        @param group: a valid (existing) group name
+        @param name: name of the data (can't be "jids")
+        @param value: value to set
+        """
+        assert name is not 'jids'
+        self._groups[group][name] = value
 
-    def __contains__(self, jid):
-        raise NotImplementedError
+    def getGroupData(self, group, name=None):
+        """Return value associated to group data
+
+        @param group: a valid (existing) group name
+        @param name: name of the data or None to get the whole dict
+        @return: registered value
+        """
+        if name is None:
+            return self._groups[group]
+        return self._groups[group][name]
+
+    def setSpecial(self, entity, special_type):
+        """Set special flag on an entity
+
+        @param entity(jid.JID): jid of the special entity
+        @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag
+        """
+        assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
+        self.setCache(entity, C.CONTACT_SPECIAL, special_type)
 
     def clearContacts(self):
         """Clear all the contact list"""
-        self.specials.clear()
+        self.unselectAll()
+        self._cache.clear()
+        self._groups.clear()
+        self._specials.clear()
+        self.update()
 
-    def replace(self, jid, groups=None, attributes=None):
+    def setContact(self, entity, groups=None, attributes=None, in_roster=False):
         """Add a contact to the list if doesn't exist, else update it.
 
         This method can be called with groups=None for the purpose of updating
@@ -64,49 +181,177 @@
 
         None value for 'groups' has a different meaning than [None] which is for the default group.
 
-        @param jid (JID)
+        @param entity (jid.JID): entity to add or replace
         @param groups (list): list of groups or None to ignore the groups membership.
-        @param attributes (dict)
+        @param attributes (dict): attibutes of the added jid or to update
+        @param in_roster (bool): True if contact is from roster
         """
-        if attributes and 'name' in attributes:
-            self.setCache(jid, 'name', attributes['name'])
+        if attributes is None:
+            attributes = {}
+
+        entity_bare = entity.bare
+
+        if in_roster:
+            self._roster.add(entity_bare)
+
+        cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}})
+
+        assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes
+
+        # we set groups and fill self._groups accordingly
+        if groups is not None:
+            if not groups:
+                groups = [None]  # [None] is the default group
+            cache[C.CONTACT_GROUPS] = groups
+            for group in groups:
+                self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare)
+
+        # special entities management
+        if C.CONTACT_SPECIAL in attributes:
+            if attributes[C.CONTACT_SPECIAL] is None:
+                del attributes[C.CONTACT_SPECIAL]
+                self._specials.remove(entity_bare)
+            else:
+                self._specials.add(entity_bare)
+
+        # now the attribute we keep in cache
+        for attribute, value in attributes.iteritems():
+            cache[attribute] = value
+
+        # we can update the display
+        self.update()
+
+    def getContacts(self):
+        """Return contacts currently selected
+
+        @return (set): set of selected entities"""
+        return self._selected
+
+    def entityToShow(self, entity, check_resource=False):
+        """Tell if the contact should be showed or hidden.
 
-    def remove(self, jid):
-        """remove a contact from the list"""
+        @param contact (jid.JID): jid of the contact
+        @param check_resource (bool): True if resource must be significant
+        @return: True if that contact should be showed in the list
+        """
+        show = self.getCache(entity, C.PRESENCE_SHOW)
+
+        if check_resource:
+            alerts = self._alerts
+            selected = self._selected
+        else:
+            alerts = {alert.bare for alert in self._alerts}
+            selected = {selected.bare for selected in self._selected}
+        return ((show is not None and show != "unavailable")
+                or self.show_disconnected
+                or entity in alerts
+                or entity in selected)
+
+    def anyEntityToShow(self, entities, check_resources=False):
+        """Tell if in a list of entities, at least one should be shown
+
+        @param entities (list[jid.JID]): list of jids
+        @param check_resources (bool): True if resources must be significant
+        @return: bool
+        """
+        for entity in entities:
+            if self.entityToShow(entity, check_resources):
+                return True
+        return False
+
+    def remove(self, entity):
+        """remove a contact from the list
+
+        @param entity(jid.JID): jid of the entity to remove (bare jid is used)
+        """
+        entity_bare = entity.bare
         try:
-            del self.specials[jid.bare]
+            groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
         except KeyError:
-            pass
+            log.warning(_("Trying to delete an unknow entity [{}]").format(entity))
+        del self._cache[entity_bare]
+        for group in groups:
+            self._groups[group]['jids'].remove(entity_bare)
+        for set_ in (self._selected, self._alerts, self._specials, self._special_extras):
+            to_remove = set()
+            for set_entity in set_:
+                if set_entity.bare == entity.bare:
+                    to_remove.add(set_entity)
+            set_.difference_update(to_remove)
+        self.update()
 
     def add(self, jid, param_groups=None):
         """add a contact to the list"""
         raise NotImplementedError
 
-    def getSpecial(self, jid):
-        """Return special type of jid, or None if it's not special"""
-        return self.specials.get(jid.bare)
+    def updatePresence(self, entity, show, priority, statuses):
+        """Update entity's presence status
 
-    def setSpecial(self, jid, _type, show=False):
-        """Set entity as a special
-        @param jid: jid of the entity
-        @param _type: special type (e.g.: "MUC")
-        @param show: True to display the dialog to chat with this entity
-        """
-        self.specials[jid.bare] = _type
-
-    def updatePresence(self, jid, show, priority, statuses):
-        """Update entity's presence status
-        @param jid: entity to update's jid
+        @param entity(jid.JID): entity to update's entity
         @param show: availability
         @parap priority: resource's priority
-        @param statuses: dict of statuses"""
-        self.setCache(jid, 'show', show)
-        self.setCache(jid, 'prority', priority)
-        self.setCache(jid, 'statuses', statuses)
-        self.update_jid(jid)
+        @param statuses: dict of statuses
+        """
+        cache = self.getCache(entity)
+        if show == C.PRESENCE_UNAVAILABLE:
+            if not entity.resource:
+                cache[C.CONTACT_RESOURCES].clear()
+                cache[C.CONTACT_MAIN_RESOURCE]= None
+            else:
+                del cache[C.CONTACT_RESOURCES][entity.resource]
+                if not cache[C.CONTACT_RESOURCES]:
+                    cache[C.CONTACT_MAIN_RESOURCE] = None
+        else:
+            assert entity.resource
+            resources_data = cache[C.CONTACT_RESOURCES]
+            resource_data = resources_data.setdefault(entity.resource, {})
+            resource_data[C.PRESENCE_SHOW] = show
+            resource_data[C.PRESENCE_PRIORITY] = int(priority)
+            resource_data[C.PRESENCE_STATUSES] = statuses
+
+            priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
+            cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
+            self.update()
+
+    def unselectAll(self):
+        """Unselect all contacts"""
+        self._selected.clear()
+        self.update()
+
+    def select(self, entity):
+        """Select an entity
+
+        @param entity(jid.JID): entity to select (resource is significant)
+        """
+        log.debug("select %s" % entity)
+        self._selected.add(entity)
+        self.update()
+
+    def setAlert(self, entity):
+        """Set an alert on the entity (usually for a waiting message)
+
+        @param entity(jid.JID): entity which must displayed in alert mode (resource is significant)
+        """
+        self._alerts.add(entity)
+        self.update()
 
     def showOfflineContacts(self, show):
-        pass
+        show = C.bool(show)
+        if self.show_disconnected == show:
+            return
+        self.show_disconnected = show
+        self.update()
 
     def showEmptyGroups(self, show):
-        pass
+        show = C.bool(show)
+        if self.show_empty_groups == show:
+            return
+        self.show_empty_groups = show
+        self.update()
+
+    def showResources(self, show):
+        show = C.bool(show)
+        if self.show_resources == show:
+            return
+        self.show_resources = show
+        self.update()
--- a/frontends/src/quick_frontend/quick_utils.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/quick_frontend/quick_utils.py	Wed Dec 10 19:00:09 2014 +0100
@@ -17,20 +17,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from sat_frontends.tools.jid  import JID
 from os.path import exists, splitext
-from sat_frontends.quick_frontend.constants import Const
-
-def escapePrivate(ori_jid):
-    """Escape a private jid"""
-    return JID(Const.PRIVATE_PREFIX + ori_jid.bare + '@' + ori_jid.resource)
-
-def unescapePrivate(escaped_jid):
-    if not escaped_jid.startswith(Const.PRIVATE_PREFIX):
-        return escaped_jid
-    escaped_split = tuple(escaped_jid[len(Const.PRIVATE_PREFIX):].split('@'))
-    assert(len(escaped_split) == 3)
-    return JID("%s@%s/%s" % escaped_split)
 
 def getNewPath(path):
     """ Check if path exists, and find a non existant path if needed """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/quick_frontend/quick_widgets.py	Wed Dec 10 19:00:09 2014 +0100
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# helper class for making a SAT frontend
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat.core import exceptions
+
+
+classes_map = {}
+
+
+def register(base_cls, child_cls=None):
+    """Register a child class to use by default when a base class is needed
+
+    @param base_cls: "Quick..." base class (like QuickChat or QuickContact), must inherit from QuickWidget
+    @param child_cls: inherited class to use when Quick... class is requested, must inherit from base_cls.
+        Can be None if it's the base_cls itself which register
+    """
+    classes_map[base_cls] = child_cls
+
+
+class QuickWidgetsManager(object):
+    """This class is used to manage all the widgets of a frontend
+    A widget can be a window, a graphical thing, or someting else depending of the frontend"""
+
+    def __init__(self, host):
+        self.host = host
+        self._widgets = {}
+
+    def getOrCreateWidget(self, class_, target, *args, **kwargs):
+        """Get an existing widget or create a new one when necessary
+
+        If the widget is new, self.host.newWidget will be called with it.
+        @param class_(class): class of the widget to create
+        @param target: target depending of the widget, usually a JID instance
+        @param args(list): optional args to create a new instance of class_
+        @param kwargs(list): optional kwargs to create anew instance of class_
+            if 'profile' key is present, it will be popped and put in 'profiles'
+            if there is neither 'profile' nor 'profiles', None will be used for 'profiles'
+            if 'on_new_widget' is present it can have the following values:
+                'NEW_WIDGET' [default]: self.host.newWidget will be called on widget creation
+                [callable]: this method will be called instead of self.host.newWidget
+                None: do nothing
+            if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash
+        @return: a class_ instance, either new or already existing
+        """
+        # class management
+        try:
+            cls = classes_map[class_]
+        except KeyError:
+            cls = class_
+        if cls is None:
+            raise exceptions.InternalError("There is not class registered for {}".format(class_))
+
+        # arguments management
+        _args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args
+        _kwargs = kwargs or {}
+        if 'profiles' in _kwargs and 'profile' in _kwargs:
+            raise ValueError("You can't have 'profile' and 'profiles' keys at the same time")
+        try:
+            _kwargs['profiles'] = _kwargs.pop('profile')
+        except KeyError:
+            if not 'profiles' in _kwargs:
+                _kwargs['profiles'] = None
+
+        # we get the hash
+        try:
+            hash_ = _kwargs.pop('force_hash')
+        except KeyError:
+            hash_ = cls.getWidgetHash(target, _kwargs['profiles'])
+
+        # widget creation or retrieval
+        widgets_list = self._widgets.setdefault(cls, {}) # we sorts widgets by classes
+        if not cls.SINGLE:
+            widget = None # if the class is not SINGLE, we always create a new widget
+        else:
+            try:
+                widget = widgets_list[hash_]
+                widget.addTarget(target)
+            except KeyError:
+                widget = None
+
+        if widget is None:
+            # we need to create a new widget
+            try:
+                #on_new_widget tell what to do for the new widget creation
+                on_new_widget = _kwargs.pop('on_new_widget')
+            except KeyError:
+                on_new_widget = 'NEW_WIDGET'
+
+            log.debug(u"Creating new widget for target {} {}".format(target, cls))
+            widget = cls(*_args, **_kwargs)
+            widgets_list[hash_] = widget
+
+            if on_new_widget == 'NEW_WIDGET':
+                self.host.newWidget(widget)
+            elif callable(on_new_widget):
+                on_new_widget(widget)
+            else:
+                assert on_new_widget is None
+
+        return widget
+
+
+class QuickWidget(object):
+    """generic widget base"""
+    SINGLE=True # if True, there can be only one widget per target(s)
+    PROFILES_MULTIPLE=False
+    PROFILES_ALLOW_NONE=False
+
+    def __init__(self, host, target, profiles=None):
+        """
+        @param host: %(doc_host)s
+        @param target: target specific for this widget class
+        @param profiles: can be either:
+            - (unicode): used when widget class manage a unique profile
+            - (iterable): some widget class can manage several profiles, several at once can be specified here
+            - None: no profile is managed by this widget class (rare)
+        @raise: ValueError when (iterable) or None is given to profiles for a widget class which manage one unique profile.
+        """
+        self.host = host
+        self.targets = set()
+        self.addTarget(target)
+        self.profiles = set()
+        if isinstance(profiles, basestring):
+            self.addProfile(profiles)
+        elif profiles is None:
+            if not self.PROFILES_ALLOW_NONE:
+                raise ValueError("profiles can't have a value of None")
+        else:
+            if not self.PROFILES_MULTIPLE:
+                raise ValueError("multiple profiles are not allowed")
+            for profile in profiles:
+                self.addProfile(profile)
+
+    @property
+    def profile(self):
+        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
+        return list(self.profiles)[0]
+
+    def addTarget(self, target):
+        """Add a target if it doesn't already exists
+
+        @param target: target to add
+        """
+        self.targets.add(target)
+
+    def addProfile(self, profile):
+        """Add a profile is if doesn't already exists
+
+        @param profile: profile to add
+        """
+        if self.profiles and not self.PROFILES_MULTIPLE:
+            raise ValueError("multiple profiles are not allowed")
+        self.profiles.add(profile)
+
+    @staticmethod
+    def getWidgetHash(target, profiles):
+        """Return the hash associated with this target for this widget class
+
+        some widget classes can manage several target on the same instance
+        (e.g.: a chat widget with multiple resources on the same bare jid),
+        this method allow to return a hash associated to one or several targets
+        to retrieve the good instance. For example, a widget managing JID targets,
+        and all resource of the same bare jid would return the bare jid as hash.
+
+        @param target: target to check
+        @param profiles: profile(s) associated to target, see __init__ docstring
+        @return: a hash (can correspond to one or many targets or profiles, depending of widget class)
+        """
+        return unicode(target) # by defaut, there is one hash for one target
--- a/frontends/src/tools/jid.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/tools/jid.py	Wed Dec 10 19:00:09 2014 +0100
@@ -22,12 +22,12 @@
     """This class help manage JID (Node@Domaine/Resource)"""
 
     def __new__(cls, jid):
-        self = unicode.__new__(cls, cls.__normalize(jid))
-        self.__parse()
+        self = unicode.__new__(cls, cls._normalize(jid))
+        self._parse()
         return self
 
     @classmethod
-    def __normalize(cls, jid):
+    def _normalize(cls, jid):
         """Naive normalization before instantiating and parsing the JID"""
         if not jid:
             return jid
@@ -35,21 +35,25 @@
         tokens[0] = tokens[0].lower()  # force node and domain to lower-case
         return '/'.join(tokens)
 
-    def __parse(self):
+    @property
+    def bare(self):
+        if not self.node:
+            return JID(self.domain)
+        return JID(u"{}@{}".format(self.node, self.domain))
+
+    def _parse(self):
         """Find node domain and resource"""
         node_end = self.find('@')
         if node_end < 0:
             node_end = 0
         domain_end = self.find('/')
-        if domain_end < 1:
+        if domain_end == 0:
+           raise ValueError("a jid can't start with '/'")
+        if domain_end == -1:
             domain_end = len(self)
-        self.node = self[:node_end]
+        self.node = self[:node_end] or None
         self.domain = self[(node_end + 1) if node_end else 0:domain_end]
-        self.resource = self[domain_end + 1:]
-        if not node_end:
-            self.bare = self
-        else:
-            self.bare = self.node + '@' + self.domain
+        self.resource = self[domain_end + 1:] or None
 
     def is_valid(self):
         """
--- a/frontends/src/tools/xmlui.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/tools/xmlui.py	Wed Dec 10 19:00:09 2014 +0100
@@ -202,7 +202,7 @@
     This class must not be instancied directly
     """
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
         """Initialise the XMLUI instance
 
         @param host: %(doc_host)s
@@ -210,15 +210,18 @@
         @param title: force the title, or use XMLUI one if None
         @param flags: list of string which can be:
             - NO_CANCEL: the UI can't be cancelled
+        @param callback: if present, will be use with launchAction
         """
         self.host = host
         top=parsed_dom.documentElement
         self.session_id = top.getAttribute("session_id") or None
         self.submit_id = top.getAttribute("submit") or None
-        self.title = title or top.getAttribute("title") or u""
+        self.xmlui_title = title or top.getAttribute("title") or u""
         if flags is None:
             flags = []
         self.flags = flags
+        self.callback = callback
+        self.profile = profile
 
     def _isAttrSet(self, name, node):
         """Returnw widget boolean attribute status
@@ -253,7 +256,7 @@
         self._xmluiLaunchAction(self.submit_id, data)
 
     def _xmluiLaunchAction(self, action_id, data):
-        self.host.launchAction(action_id, data, profile_key = self.host.profile)
+        self.host.launchAction(action_id, data, callback=self.callback, profile=self.profile)
 
 
 class XMLUIPanel(XMLUIBase):
@@ -265,8 +268,8 @@
     """
     widget_factory = None
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
-        super(XMLUIPanel, self).__init__(host, parsed_dom, title = None, flags = None)
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
+        super(XMLUIPanel, self).__init__(host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile)
         self.ctrl_list = {}  # usefull to access ctrl
         self._main_cont = None
         self.constructUI(parsed_dom)
@@ -462,7 +465,7 @@
         raise NotImplementedError
 
     def _xmluiSetParam(self, name, value, category):
-        self.host.bridge.setParam(name, value, category, profile_key=self.host.profile)
+        self.host.bridge.setParam(name, value, category, profile=self.profile)
 
     ##EVENTS##
 
@@ -639,8 +642,8 @@
 class XMLUIDialog(XMLUIBase):
     dialog_factory = None
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
-        super(XMLUIDialog, self).__init__(host, parsed_dom, title = None, flags = None)
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
+        super(XMLUIDialog, self).__init__(host, parsed_dom, title=None, flags=None, callback=callback, profile=C.PROF_KEY_NONE)
         top=parsed_dom.documentElement
         dlg_elt =  self._getChildNode(top, "dialog")
         if dlg_elt is None:
@@ -654,23 +657,23 @@
         level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO
 
         if dlg_type == C.XMLUI_DIALOG_MESSAGE:
-            self.dlg = self.dialog_factory.createMessageDialog(self, self.title, message, level)
+            self.dlg = self.dialog_factory.createMessageDialog(self, self.xmlui_title, message, level)
         elif dlg_type == C.XMLUI_DIALOG_NOTE:
-            self.dlg = self.dialog_factory.createNoteDialog(self, self.title, message, level)
+            self.dlg = self.dialog_factory.createNoteDialog(self, self.xmlui_title, message, level)
         elif dlg_type == C.XMLUI_DIALOG_CONFIRM:
             try:
                 buttons_elt = self._getChildNode(dlg_elt, "buttons")
                 buttons_set = buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT
             except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
                 buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT
-            self.dlg = self.dialog_factory.createConfirmDialog(self, self.title, message, level, buttons_set)
+            self.dlg = self.dialog_factory.createConfirmDialog(self, self.xmlui_title, message, level, buttons_set)
         elif dlg_type == C.XMLUI_DIALOG_FILE:
             try:
                 file_elt = self._getChildNode(dlg_elt, "file")
                 filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT
             except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
                 filetype = C.XMLUI_DATA_FILETYPE_DEFAULT
-            self.dlg = self.dialog_factory.createFileDialog(self, self.title, message, level, filetype)
+            self.dlg = self.dialog_factory.createFileDialog(self, self.xmlui_title, message, level, filetype)
         else:
             raise ValueError("Unknown dialog type [%s]" % dlg_type)
 
@@ -690,7 +693,7 @@
     class_map[type_] = class_
 
 
-def create(host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None):
+def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, callback=None, profile=C.PROF_KEY_NONE):
     """
         @param dom_parse: methode equivalent to minidom.parseString (but which  must manage unicode), or None to use default one
         @param dom_free: method used to free the parsed DOM
@@ -713,6 +716,6 @@
     except KeyError:
         raise ClassNotRegistedError(_("You must register classes with registerClass before creating a XMLUI"))
 
-    xmlui = cls(host, parsed_dom, title, flags)
+    xmlui = cls(host, parsed_dom, title, flags, callback, profile)
     dom_free(parsed_dom)
     return xmlui