Mercurial > libervia-backend
diff sat_frontends/primitivus/contact_list.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/primitivus/contact_list.py@0046283a285d |
children | 81b70eeb710f |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_frontends/primitivus/contact_list.py Mon Apr 02 19:44:50 2018 +0200 @@ -0,0 +1,304 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Primitivus: a SAT frontend +# Copyright (C) 2009-2018 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.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__) +from sat_frontends.quick_frontend import quick_widgets + + +class ContactList(PrimitivusWidget, QuickContactList): + PROFILES_MULTIPLE=False + PROFILES_ALLOW_NONE=False + signals = ['click','change'] + # FIXME: Only single profile is managed so far + + def __init__(self, host, target, on_click=None, on_change=None, user_data=None, profiles=None): + QuickContactList.__init__(self, host, profiles) + self.contact_list = self.host.contact_lists[self.profile] + + #we now build the 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: + urwid.connect_signal(self, 'change', on_change, user_data) + self.host.addListener('notification', self.onNotification, [self.profile]) + self.host.addListener('notificationsClear', self.onNotification, [self.profile]) + self.postInit() + + def update(self, entities=None, type_=None, profile=None): + """Update display, keep focus""" + # FIXME: full update is done each time, must handle entities, type_ and profile + 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._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, + # 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.contact_list.show_status = not self.contact_list.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.boolConst(not self.contact_list.show_disconnected), "General", profile_key=self.profile) + elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources + self.contact_list.showResources(not self.contact_list.show_resources) + self.update() + return super(ContactList, self).keypress(size, key) + + # QuickWidget methods + + @staticmethod + def getWidgetHash(target, profiles): + profiles = sorted(profiles) + return tuple(profiles) + + # 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 + """ + 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): + # 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(False, widget, True) + return + except AttributeError: + pass + idx += 1 + + log.debug(u"Not element found for {} in setFocus".format(text)) + + # events + + def _groupClicked(self, group_wid): + group = group_wid.getValue() + data = self.contact_list.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. + @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 + self.host.modeHint(C.MODE_INSERTION) + self._emit('click', entity) + + def onNotification(self, entity, notif, profile): + notifs = list(self.host.getNotifs(C.ENTITY_ALL, profile=self.profile)) + if notifs: + self.title_dynamic = u"({})".format(len(notifs)) + else: + self.title_dynamic = None + self.host.redraw() # FIXME: should not be necessary + + # Methods to build the widget + + def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_notifs=True, with_show_attr=True, markup_prepend=None, markup_append=None, special=False): + """Build one contact markup data + + @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 selected comparisons + @param with_notifs (bool): if True, show notification count + @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 + @param special (bool): True if entity is a special one + @return (list): markup data are expected by Urwid text widgets + """ + markup = [] + if use_bare_jid: + selected = {entity.bare for entity in self.contact_list._selected} + else: + selected = self.contact_list._selected + if keys is None: + entity_txt = entity + else: + cache = self.contact_list.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 + + if with_show_attr: + show = self.contact_list.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' + + notifs = list(self.host.getNotifs(entity, exact_jid=special, profile=self.profile)) + if notifs: + header = [('cl_notifs', u'({})'.format(len(notifs))), u' '] + if list(self.host.getNotifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile)): + header = ('cl_mention', header) + else: + header = u'' + + 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 entities (iterable): iterable of JID to display + """ + if not entities: + return + widgets = [] # list of built widgets + + for entity in entities: + if entity in self.contact_list._specials or not self.contact_list.entityToShow(entity): + continue + markup_extra = [] + if self.contact_list.show_resources: + for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES): + resource_disp = ('resource_main' if resource == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n " + resource) + markup_extra.append(resource_disp) + if self.contact_list.show_status: + status = self.contact_list.getCache(jid.JID('%s/%s' % (entity, resource)), 'status') + status_disp = ('status', "\n " + status) if status else "" + markup_extra.append(status_disp) + + + else: + if self.contact_list.show_status: + status = self.contact_list.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) + + def _buildSpecials(self, content): + """Build the special entities""" + specials = sorted(self.contact_list.getSpecials()) + current = None + for entity in specials: + if current is not None and current.bare == entity.bare: + # nested entity (e.g. MUC private conversations) + widget = self._buildEntityWidget(entity, ('resource',), markup_prepend=' ', special=True) + else: + # the special widgets + if entity.resource: + widget = self._buildEntityWidget(entity, ('resource',), special=True) + else: + widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False, special=True) + content.append(widget) + + def _buildList(self): + """Build the main contact list widget""" + content = urwid.SimpleListWalker([]) + + self._buildSpecials(content) + if self.contact_list._specials: + content.append(urwid.Divider('=')) + + groups = list(self.contact_list._groups) + groups.sort(key=lambda x: x.lower() if x else x) + for group in groups: + data = self.contact_list.getGroupData(group) + folded = data.get(C.GROUP_DATA_FOLDED, False) + jids = list(data['jids']) + if group is not None and (self.contact_list.anyEntityToShow(jids) or self.contact_list.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 not folded: + self._buildEntities(content, jids) + not_in_roster = set(self.contact_list._cache).difference(self.contact_list._roster).difference(self.contact_list._specials).difference((self.contact_list.whoami.bare,)) + if not_in_roster: + content.append(urwid.Divider('-')) + self._buildEntities(content, not_in_roster) + + return urwid.ListBox(content) + +quick_widgets.register(QuickContactList, ContactList)