diff frontends/src/quick_frontend/quick_app.py @ 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 e56dfe0378a1
children faa1129559b8
line wrap: on
line diff
--- 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)