diff frontends/src/primitivus/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 93a5e2673929
children faa1129559b8
line wrap: on
line diff
--- 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)