Mercurial > libervia-backend
view libervia/tui/contact_list.py @ 4326:5fd6a4dc2122
cli (output/std): use `rich` to output JSON.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 20 Nov 2024 11:38:44 +0100 |
parents | 0d7bb4df2343 |
children |
line wrap: on
line source
#!/usr/bin/env python3 # Libervia TUI # Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _ import urwid from urwid_satext import sat_widgets from libervia.frontends.quick_frontend.quick_contact_list import QuickContactList from libervia.tui.status import StatusBar from libervia.tui.constants import Const as C from libervia.tui.keys import action_key_map as a_key from libervia.tui.widget import LiberviaTUIWidget from libervia.frontends.tools import jid from libervia.backend.core import log as logging log = logging.getLogger(__name__) from libervia.frontends.quick_frontend import quick_widgets class ContactList(LiberviaTUIWidget, 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._build_list(), None, self.status_bar) LiberviaTUIWidget.__init__(self, self.frame, _("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.on_notification, [self.profile]) self.host.addListener("notificationsClear", self.on_notification, [self.profile]) self.post_init() 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._build_list() 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.param_set( C.SHOW_OFFLINE_CONTACTS, C.bool_const(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.show_resources = not self.contact_list.show_resources self.update() return super(ContactList, self).keypress(size, key) # QuickWidget methods @staticmethod def get_widget_hash(target, profiles): profiles = sorted(profiles) return tuple(profiles) # modify the contact list def set_focus(self, text, select=False): """give focus to the first element that matches the given text. You can also pass in text a libervia.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.get_value() 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._contact_clicked(False, widget, True) return except AttributeError: pass idx += 1 log.debug("Not element found for {} in set_focus".format(text)) # events def _group_clicked(self, group_wid): group = group_wid.get_value() data = self.contact_list.get_group_data(group) data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False) self.set_focus(group) self.update() def _contact_clicked(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._build_entity_widget. @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.mode_hint(C.MODE_INSERTION) self._emit("click", entity) def on_notification(self, entity, notif, profile): notifs = list(self.host.get_notifs(C.ENTITY_ALL, profile=self.profile)) if notifs: self.title_dynamic = "({})".format(len(notifs)) else: self.title_dynamic = None self.host.redraw() # FIXME: should not be necessary # Methods to build the widget def _build_entity_widget( 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, default=None) if show is None: show = C.PRESENCE_UNAVAILABLE show_icon, entity_attr = C.PRESENCE.get(show, ("", "default")) markup.insert(0, "{} ".format(show_icon)) else: entity_attr = "default" notifs = list( self.host.get_notifs(entity, exact_jid=special, profile=self.profile) ) mentions = list( self.host.get_notifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile) ) if notifs or mentions: attr = "cl_mention" if mentions else "cl_notifs" header = [(attr, "({})".format(len(notifs) + len(mentions))), " "] 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._contact_clicked, user_args=[use_bare_jid] ) return widget def _build_entities(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.entity_visible(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", default=None ) 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", default=None) status_disp = ("status", "\n " + status) if status else "" markup_extra.append(status_disp) widget = self._build_entity_widget( 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 _build_specials(self, content): """Build the special entities""" specials = sorted(self.contact_list.get_specials()) 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._build_entity_widget( entity, ("resource",), markup_prepend=" ", special=True ) else: # the special widgets if entity.resource: widget = self._build_entity_widget( entity, ("resource",), special=True ) else: widget = self._build_entity_widget( entity, ("cache_nick", "cache_name", "node"), with_show_attr=False, special=True, ) content.append(widget) def _build_list(self): """Build the main contact list widget""" content = urwid.SimpleListWalker([]) self._build_specials(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 "") for group in groups: data = self.contact_list.get_group_data(group) folded = data.get(C.GROUP_DATA_FOLDED, False) jids = list(data["jids"]) if group is not None and ( self.contact_list.any_entity_visible(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._group_clicked) if not folded: self._build_entities(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._build_entities(content, not_in_roster) return urwid.ListBox(content) quick_widgets.register(QuickContactList, ContactList)