diff libervia/tui/contact_list.py @ 4076:b620a8e882e1

refactoring: rename `libervia.frontends.primitivus` to `libervia.tui`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:25:25 +0200
parents libervia/frontends/primitivus/contact_list.py@26b7ed2817da
children b47f21f2b8fa
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/contact_list.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,364 @@
+#!/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)