view src/browser/sat_browser/contact_list.py @ 685:9877607c719a

2015 copyright dates update
author Goffi <goffi@goffi.org>
date Mon, 30 Mar 2015 10:28:47 +0200
parents e876f493dccc
children 3845a086f0b3
line wrap: on
line source

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

# Libervia: a Salut à Toi frontend
# Copyright (C) 2011, 2012, 2013, 2014, 2015 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/>.

import pyjd  # this is dummy in pyjs
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
from pyjamas.ui.SimplePanel import SimplePanel
from pyjamas.ui.ScrollPanel import ScrollPanel
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.ClickListener import ClickHandler
from pyjamas.ui.Label import Label
from pyjamas import Window
from pyjamas import DOM

from constants import Const as C
import libervia_widget
import contact_panel
import blog
import chat

unicode = str # XXX: pyjama doesn't manage unicode


class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler):
    def __init__(self, host, group):
        """

        @param host (SatWebFrontend)
        @param group (unicode): group name
        """
        self.group = group
        Label.__init__(self, group)  # , Element=DOM.createElement('div')
        self.setStyleName('group')
        libervia_widget.DragLabel.__init__(self, group, "GROUP", host)
        ClickHandler.__init__(self)
        self.addClickListener(self)

    def onClick(self, sender):
        self.host.displayWidget(blog.MicroblogPanel, (self.group,))


class GroupPanel(VerticalPanel):

    def __init__(self, parent):
        VerticalPanel.__init__(self)
        self.setStyleName('groupPanel')
        self._parent = parent
        self._groups = set()

    def add(self, group):
        if group in self._groups:
            log.warning("trying to add an already existing group")
            return
        _item = GroupLabel(self._parent.host, group)
        _item.addMouseListener(self._parent)
        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
        index = 0
        for group_ in [child.group for child in self.getChildren()]:
            if group_ > group:
                break
            index += 1
        VerticalPanel.insert(self, _item, index)
        self._groups.add(group)

    def remove(self, group):
        for wid in self:
            if isinstance(wid, GroupLabel) and wid.group == group:
                VerticalPanel.remove(self, wid)
                self._groups.remove(group)
                return
        log.warning("Trying to remove a non existent group")

    def getGroupBox(self, group):
        """get the widget of a group

        @param group (unicode): the group
        @return: GroupLabel instance if present, else None"""
        for wid in self:
            if isinstance(wid, GroupLabel) and wid.group == group:
                return wid
        return None

    def getGroups(self):
        return self._groups


class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler):

    def __init__(self, host, text):
        Label.__init__(self, text)  # , Element=DOM.createElement('div')
        self.setStyleName('contactTitle')
        libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host)
        ClickHandler.__init__(self)
        self.addClickListener(self)

    def onClick(self, sender):
        self.host.displayWidget(blog.MicroblogPanel, ())


class ContactList(SimplePanel, QuickContactList):
    """Manage the contacts and groups"""

    def __init__(self, host):
        QuickContactList.__init__(self, host, C.PROF_KEY_NONE)
        SimplePanel.__init__(self)
        self.host = host
        self.scroll_panel = ScrollPanel()
        self.vPanel = VerticalPanel()
        _title = ContactTitleLabel(host, 'Contacts')
        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")

        def on_click(contact_jid):
            self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE)

        self._contacts_panel = contact_panel.ContactsPanel(host, contacts_click=on_click, contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT))
        self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists !
        self._group_panel = GroupPanel(self)

        self.vPanel.add(_title)
        self.vPanel.add(self._group_panel)
        self.vPanel.add(self._contacts_panel)
        self.scroll_panel.add(self.vPanel)
        self.add(self.scroll_panel)
        self.setStyleName('contactList')
        Window.addWindowResizeListener(self)

        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
        self.avatarListener = self.onAvatarUpdate
        host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])

    @property
    def profile(self):
        return C.PROF_KEY_NONE

    def onDelete(self):
        QuickContactList.onDelete(self)
        self.host.removeListener('avatar', self.avatarListener)

    def update(self):
        ### GROUPS ###
        _keys = self._groups.keys()
        try:
            # XXX: Pyjamas doesn't do the set casting if None is present
            _keys.remove(None)
        except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options
            pass
        current_groups = set(_keys)
        shown_groups = self._group_panel.getGroups()
        new_groups = current_groups.difference(shown_groups)
        removed_groups = shown_groups.difference(current_groups)
        for group in new_groups:
            self._group_panel.add(group)
        for group in removed_groups:
            self._group_panel.remove(group)

        ### JIDS ###
        to_show = [jid_ for jid_ in self.roster_entities if self.entityToShow(jid_) and jid_ != self.whoami.bare]
        to_show.sort()

        self._contacts_panel.setList(to_show)

        for jid_ in self._alerts:
            self._contacts_panel.getContactBox(jid_).setAlert(True)

    def remove(self, entity):
        # FIXME: SimplePanel and QuickContactList both have a 'remove' method
        QuickContactList.remove(self, entity)

    def onWindowResized(self, width, height):
        ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5
        tab_panel = self.host.panel.tab_panel
        if tab_panel.getWidgetCount() > 1:
            ideal_height -= tab_panel.getTabBar().getOffsetHeight()
        self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))

    # def updateContact(self, jid_s, attributes, groups):
    #     """Add a contact to the panel if it doesn't exist, update it else

    #     @param jid_s: jid userhost as unicode
    #     @param attributes: cf SàT Bridge API's newContact
    #     @param groups: list of groups"""
    #     _current_groups = self.getContactGroups(jid_s)
    #     _new_groups = set(groups)
    #     _key = "@%s: "

    #     for group in _current_groups.difference(_new_groups):
    #         # We remove the contact from the groups where he isn't anymore
    #         self.groups[group].remove(jid_s)
    #         if not self.groups[group]:
    #             # The group is now empty, we must remove it
    #             del self.groups[group]
    #             self._group_panel.remove(group)
    #             if self.host.uni_box:
    #                 self.host.uni_box.removeKey(_key % group)

    #     for group in _new_groups.difference(_current_groups):
    #         # We add the contact to the groups he joined
    #         if group not in self.groups.keys():
    #             self.groups[group] = set()
    #             self._group_panel.add(group)
    #             if self.host.uni_box:
    #                 self.host.uni_box.addKey(_key % group)
    #         self.groups[group].add(jid_s)

    #     # We add the contact to contact list, it will check if contact already exists
    #     self._contacts_panel.add(jid_s)
    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))

    # def removeContact(self, jid):
    #     """Remove contacts from groups where he is and contact list"""
    #     self.updateContact(jid, {}, [])  # we remove contact from every group
    #     self._contacts_panel.remove(jid)

    # def setConnected(self, jid_s, resource, availability, priority, statuses):
    #     """Set connection status
    #     @param jid_s (unicode): JID userhost as unicode
    #     """
    #     if availability == 'unavailable':
    #         if jid_s in self.connected:
    #             if resource in self.connected[jid_s]:
    #                 del self.connected[jid_s][resource]
    #             if not self.connected[jid_s]:
    #                 del self.connected[jid_s]
    #     else:
    #         if jid_s not in self.connected:
    #             self.connected[jid_s] = {}
    #         self.connected[jid_s][resource] = (availability, priority, statuses)

    #     # check if the contact is connected with another resource, use the one with highest priority
    #     if jid_s in self.connected:
    #         max_resource = max_priority = None
    #         for tmp_resource in self.connected[jid_s]:
    #             if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
    #                 max_resource = tmp_resource
    #                 max_priority = self.connected[jid_s][tmp_resource][1]
    #         if availability == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
    #             priority = max_priority
    #             availability = self.connected[jid_s][max_resource][0]
    #     if jid_s not in self.connected or priority >= max_priority:
    #         # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
    #         # case 2: update (or confirm) with the values of the resource which takes precedence
    #         self._contacts_panel.setState(jid_s, "availability", availability)

    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))

    def setContactMessageWaiting(self, jid, waiting):
        """Show a visual indicator that contact has send a message

        @param jid: jid of the contact
        @param waiting: True if message are waiting"""
        raise Exception("Should not be there")
        # self._contacts_panel.setState(jid, "messageWaiting", waiting)

    # def getConnected(self, filter_muc=False):
    #     """return a list of all jid (bare jid) connected
    #     @param filter_muc: if True, remove the groups from the list
    #     """
    #     contacts = self.connected.keys()
    #     contacts.sort()
    #     return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts())))

    # def getContactGroups(self, contact_jid_s):
    #     """Get groups where contact is
    #    @param group: string of single group, or list of string
    #    @param contact_jid_s: jid to test, as unicode
    #     """
    #     result = set()
    #     for group in self.groups:
    #         if self.isContactInGroup(group, contact_jid_s):
    #             result.add(group)
    #     return result

    # def isContactInGroup(self, group, contact_jid):
    #     """Test if the contact_jid is in the group
    #     @param group: string of single group, or list of string
    #     @param contact_jid: jid to test
    #     @return: True if contact_jid is in on of the groups"""
    #     if group in self.groups and contact_jid in self.groups[group]:
    #         return True
    #     return False

    def isContactInRoster(self, contact_jid):
        """Test if the contact is in our roster list"""
        for contact_box in self._contacts_panel:
            if contact_jid == contact_box.jid:
                return True
        return False

    # def getContacts(self):
    #     return self._contacts_panel.getContacts()

    def getGroups(self):
        return self.groups.keys()

    def onMouseMove(self, sender, x, y):
        pass

    def onMouseDown(self, sender, x, y):
        pass

    def onMouseUp(self, sender, x, y):
        pass

    def onMouseEnter(self, sender):
        if isinstance(sender, GroupLabel):
            jids = self.getGroupData(sender.group, "jids")
            for contact in self._contacts_panel:
                if contact.jid in jids:
                    contact.label.addStyleName("selected")

    def onMouseLeave(self, sender):
        if isinstance(sender, GroupLabel):
            jids = self.getGroupData(sender.group, "jids")
            for contact in self._contacts_panel:
                if contact.jid in jids:
                    contact.label.removeStyleName("selected")

    def onAvatarUpdate(self, jid_, hash_, profile):
        """Called on avatar update events

        @param jid_: jid of the entity with updated avatar
        @param hash_: hash of the avatar
        @param profile: %(doc_profile)s
        """
        self._contacts_panel.updateAvatar(jid_, self.host.getAvatarURL(jid_))

    def onNickUpdate(self, jid_, new_nick, profile):
        self._contacts_panel.updateNick(jid_, new_nick)

    def hasVisibleMembers(self, group):
        """Tell if the given group actually has visible members

        @param group (unicode): the group to check
        @return: boolean
        """
        raise Exception  # FIXME: remove this method
        for jid_ in self.groups[group]:
            if self._contacts_panel.getContactBox(jid_).isVisible():
                return True
        return False

    def offlineContactsToShow(self):
        """Tell if offline contacts should be visible according to the user settings

        @return: boolean
        """
        return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS))

    def emtyGroupsToShow(self):
        """Tell if empty groups should be visible according to the user settings

        @return: boolean
        """
        return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS))

    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
        QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile)
        self._contacts_panel.setPresence(entity, show)

    # def updateVisibility(self, jids, groups):
    #     """Set the widgets visibility for the given contacts and groups

    #     @param jids (list[unicode]): list of JID
    #     @param groups (list[unicode]): list of groups
    #     """
    #     for jid_s in jids:
    #         try:
    #             self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
    #         except TypeError:
    #             log.warning('No box for contact %s: this code line should not be reached' % jid_s)
    #     for group in groups:
    #         try:
    #             self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
    #         except TypeError:
    #             log.warning('No box for group %s: this code line should not be reached' % group)

    # def refresh(self):
    #     """Show or hide disconnected contacts and empty groups"""
    #     self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())


class JIDList(list):
    """JID-friendly list implementation for Pyjamas"""

    def __contains__(self, item):
        """Tells if the list contains the given item.

        @param item (object): element to check
        @return: bool
        """
        # Since our JID doesn't inherit from str/unicode, without this method
        # the test would return True only when the objects references are the
        # same. Tests have shown that the other iterable "set" and "dict" don't
        # need this hack to reproduce the Twisted's behavior.
        for other in self:
            if other == item:
                return True
        return False