364 lines
14 KiB
Python
364 lines
14 KiB
Python
#!/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)
|