diff frontends/src/primitivus/contact_list.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents 1679ac59f701
children 017270e6eea4
line wrap: on
line diff
--- a/frontends/src/primitivus/contact_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/primitivus/contact_list.py	Wed Mar 18 10:52:28 2015 +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,214 @@
                 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 _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._emit('click', entity)
+
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
+        super(ContactList, self).onPresenceUpdate(entity, show, priority, statuses, profile)
+        self.update()
+
+    def onNickUpdate(self, entity, new_nick, profile):
         self.update()
 
-    def __groupClicked(self, group_wid):
-        group = self.groups[group_wid.getValue()]
-        group[0] = not group[0]
-        self.update()
-        self.setFocus(group_wid.getValue())
+    # 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).difference((self.whoami.bare,))
+        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)