diff frontends/src/quick_frontend/quick_contact_list.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 3abc6563a0d2
children faa1129559b8
line wrap: on
line diff
--- 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()