view frontends/src/primitivus/contact_list.py @ 1263:cfd636203e8f frontends_multi_profiles

core: misc improvments: - added some constants for message types and presence - removed profile authentification specific methods in favor of a more generic "validated" key
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 18:36:11 +0100
parents 93a5e2673929
children e3a9ea76de35
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Primitivus: a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from sat.core.i18n import _
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.core import log as logging
log = logging.getLogger(__name__)


class ContactList(urwid.WidgetWrap, 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)

        #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)
        if on_click:
            urwid.connect_signal(self, 'click', on_click, user_data)
        if on_change:
            urwid.connect_signal(self, 'change', on_change, user_data)

    def update(self):
        """Update display, keep focus"""
        widget, position = self.frame.body.get_focus()
        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()

    def keypress(self, size, key):
        # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
        #        and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element
        if key in sat_widgets.FOCUS_KEYS:
            if (key == a_key['FOCUS_SWITCH'] or  (key == a_key['FOCUS_UP'] and self.frame.focus_position == 'body') or
               (key == a_key['FOCUS_DOWN'] and self.frame.focus_position == 'footer')):
                return key
        if key == a_key['STATUS_HIDE']: #user wants to (un)hide contacts' statuses
            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)
        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

    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
        """
        idx = 0
        for widget in self.frame.body.body:
            try:
                if isinstance(widget, sat_widgets.ClickableText):
                    # 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
                else:
                    # Divider instance
                    continue
                # there's sometimes a leading space
                if text.strip() == value.strip():
                    self.frame.body.focus_position = idx
                    if select:
                        self.__contactClicked(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)
        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())

    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')

    def __buildContact(self, content, contacts):
        """Add contact representation in widget list
        @param content: widget list, e.g. SimpleListWalker
        @param contacts (list): list of JID userhosts"""
        if not contacts:
            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 = ''
            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
            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):
        """Build the special entities"""
        specials = self.specials.keys()
        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
            content.append(widget)
            urwid.connect_signal(widget, 'change', self.__contactClicked)

    def __buildList(self):
        """Build the main contact list widget"""
        content = urwid.SimpleListWalker([])

        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 + ' ')
                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.

        @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()