comparison 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
comparison
equal deleted inserted replaced
4075:47401850dec6 4076:b620a8e882e1
1 #!/usr/bin/env python3
2
3
4 # Libervia TUI
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from libervia.backend.core.i18n import _
21 import urwid
22 from urwid_satext import sat_widgets
23 from libervia.frontends.quick_frontend.quick_contact_list import QuickContactList
24 from libervia.tui.status import StatusBar
25 from libervia.tui.constants import Const as C
26 from libervia.tui.keys import action_key_map as a_key
27 from libervia.tui.widget import LiberviaTUIWidget
28 from libervia.frontends.tools import jid
29 from libervia.backend.core import log as logging
30
31 log = logging.getLogger(__name__)
32 from libervia.frontends.quick_frontend import quick_widgets
33
34
35 class ContactList(LiberviaTUIWidget, QuickContactList):
36 PROFILES_MULTIPLE = False
37 PROFILES_ALLOW_NONE = False
38 signals = ["click", "change"]
39 # FIXME: Only single profile is managed so far
40
41 def __init__(
42 self, host, target, on_click=None, on_change=None, user_data=None, profiles=None
43 ):
44 QuickContactList.__init__(self, host, profiles)
45 self.contact_list = self.host.contact_lists[self.profile]
46
47 # we now build the widget
48 self.status_bar = StatusBar(host)
49 self.frame = sat_widgets.FocusFrame(self._build_list(), None, self.status_bar)
50 LiberviaTUIWidget.__init__(self, self.frame, _("Contacts"))
51 if on_click:
52 urwid.connect_signal(self, "click", on_click, user_data)
53 if on_change:
54 urwid.connect_signal(self, "change", on_change, user_data)
55 self.host.addListener("notification", self.on_notification, [self.profile])
56 self.host.addListener("notificationsClear", self.on_notification, [self.profile])
57 self.post_init()
58
59 def update(self, entities=None, type_=None, profile=None):
60 """Update display, keep focus"""
61 # FIXME: full update is done each time, must handle entities, type_ and profile
62 widget, position = self.frame.body.get_focus()
63 self.frame.body = self._build_list()
64 if position:
65 try:
66 self.frame.body.focus_position = position
67 except IndexError:
68 pass
69 self._invalidate()
70 self.host.redraw() # FIXME: check if can be avoided
71
72 def keypress(self, size, key):
73 # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
74 # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element
75 if key in sat_widgets.FOCUS_KEYS:
76 if (
77 key == a_key["FOCUS_SWITCH"]
78 or (key == a_key["FOCUS_UP"] and self.frame.focus_position == "body")
79 or (key == a_key["FOCUS_DOWN"] and self.frame.focus_position == "footer")
80 ):
81 return key
82 if key == a_key["STATUS_HIDE"]: # user wants to (un)hide contacts' statuses
83 self.contact_list.show_status = not self.contact_list.show_status
84 self.update()
85 elif (
86 key == a_key["DISCONNECTED_HIDE"]
87 ): # user wants to (un)hide disconnected contacts
88 self.host.bridge.param_set(
89 C.SHOW_OFFLINE_CONTACTS,
90 C.bool_const(not self.contact_list.show_disconnected),
91 "General",
92 profile_key=self.profile,
93 )
94 elif key == a_key["RESOURCES_HIDE"]: # user wants to (un)hide contacts resources
95 self.contact_list.show_resources(not self.contact_list.show_resources)
96 self.update()
97 return super(ContactList, self).keypress(size, key)
98
99 # QuickWidget methods
100
101 @staticmethod
102 def get_widget_hash(target, profiles):
103 profiles = sorted(profiles)
104 return tuple(profiles)
105
106 # modify the contact list
107
108 def set_focus(self, text, select=False):
109 """give focus to the first element that matches the given text. You can also
110 pass in text a libervia.frontends.tools.jid.JID (it's a subclass of unicode).
111
112 @param text: contact group name, contact or muc userhost, muc private dialog jid
113 @param select: if True, the element is also clicked
114 """
115 idx = 0
116 for widget in self.frame.body.body:
117 try:
118 if isinstance(widget, sat_widgets.ClickableText):
119 # contact group
120 value = widget.get_value()
121 elif isinstance(widget, sat_widgets.SelectableText):
122 # contact or muc
123 value = widget.data
124 else:
125 # Divider instance
126 continue
127 # there's sometimes a leading space
128 if text.strip() == value.strip():
129 self.frame.body.focus_position = idx
130 if select:
131 self._contact_clicked(False, widget, True)
132 return
133 except AttributeError:
134 pass
135 idx += 1
136
137 log.debug("Not element found for {} in set_focus".format(text))
138
139 # events
140
141 def _group_clicked(self, group_wid):
142 group = group_wid.get_value()
143 data = self.contact_list.get_group_data(group)
144 data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False)
145 self.set_focus(group)
146 self.update()
147
148 def _contact_clicked(self, use_bare_jid, contact_wid, selected):
149 """Method called when a contact is clicked
150
151 @param use_bare_jid: True if use_bare_jid is set in self._build_entity_widget.
152 @param contact_wid: widget of the contact, must have the entity set in data attribute
153 @param selected: boolean returned by the widget, telling if it is selected
154 """
155 entity = contact_wid.data
156 self.host.mode_hint(C.MODE_INSERTION)
157 self._emit("click", entity)
158
159 def on_notification(self, entity, notif, profile):
160 notifs = list(self.host.get_notifs(C.ENTITY_ALL, profile=self.profile))
161 if notifs:
162 self.title_dynamic = "({})".format(len(notifs))
163 else:
164 self.title_dynamic = None
165 self.host.redraw() # FIXME: should not be necessary
166
167 # Methods to build the widget
168
169 def _build_entity_widget(
170 self,
171 entity,
172 keys=None,
173 use_bare_jid=False,
174 with_notifs=True,
175 with_show_attr=True,
176 markup_prepend=None,
177 markup_append=None,
178 special=False,
179 ):
180 """Build one contact markup data
181
182 @param entity (jid.JID): entity to build
183 @param keys (iterable): value to markup, in preferred order.
184 The first available key will be used.
185 If key starts with "cache_", it will be checked in cache,
186 else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
187 If nothing full or keys is None, full entity is used.
188 @param use_bare_jid (bool): if True, use bare jid for selected comparisons
189 @param with_notifs (bool): if True, show notification count
190 @param with_show_attr (bool): if True, show color corresponding to presence status
191 @param markup_prepend (list): markup to prepend to the generated one before building the widget
192 @param markup_append (list): markup to append to the generated one before building the widget
193 @param special (bool): True if entity is a special one
194 @return (list): markup data are expected by Urwid text widgets
195 """
196 markup = []
197 if use_bare_jid:
198 selected = {entity.bare for entity in self.contact_list._selected}
199 else:
200 selected = self.contact_list._selected
201 if keys is None:
202 entity_txt = entity
203 else:
204 cache = self.contact_list.getCache(entity)
205 for key in keys:
206 if key.startswith("cache_"):
207 entity_txt = cache.get(key[6:])
208 else:
209 entity_txt = getattr(entity, key)
210 if entity_txt:
211 break
212 if not entity_txt:
213 entity_txt = entity
214
215 if with_show_attr:
216 show = self.contact_list.getCache(entity, C.PRESENCE_SHOW, default=None)
217 if show is None:
218 show = C.PRESENCE_UNAVAILABLE
219 show_icon, entity_attr = C.PRESENCE.get(show, ("", "default"))
220 markup.insert(0, "{} ".format(show_icon))
221 else:
222 entity_attr = "default"
223
224 notifs = list(
225 self.host.get_notifs(entity, exact_jid=special, profile=self.profile)
226 )
227 mentions = list(
228 self.host.get_notifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile)
229 )
230 if notifs or mentions:
231 attr = 'cl_mention' if mentions else 'cl_notifs'
232 header = [(attr, "({})".format(len(notifs) + len(mentions))), " "]
233 else:
234 header = ""
235
236 markup.append((entity_attr, entity_txt))
237 if markup_prepend:
238 markup.insert(0, markup_prepend)
239 if markup_append:
240 markup.extend(markup_append)
241
242 widget = sat_widgets.SelectableText(
243 markup, selected=entity in selected, header=header
244 )
245 widget.data = entity
246 widget.comp = entity_txt.lower() # value to use for sorting
247 urwid.connect_signal(
248 widget, "change", self._contact_clicked, user_args=[use_bare_jid]
249 )
250 return widget
251
252 def _build_entities(self, content, entities):
253 """Add entity representation in widget list
254
255 @param content: widget list, e.g. SimpleListWalker
256 @param entities (iterable): iterable of JID to display
257 """
258 if not entities:
259 return
260 widgets = [] # list of built widgets
261
262 for entity in entities:
263 if (
264 entity in self.contact_list._specials
265 or not self.contact_list.entity_visible(entity)
266 ):
267 continue
268 markup_extra = []
269 if self.contact_list.show_resources:
270 for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES):
271 resource_disp = (
272 "resource_main"
273 if resource
274 == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE)
275 else "resource",
276 "\n " + resource,
277 )
278 markup_extra.append(resource_disp)
279 if self.contact_list.show_status:
280 status = self.contact_list.getCache(
281 jid.JID("%s/%s" % (entity, resource)), "status", default=None
282 )
283 status_disp = ("status", "\n " + status) if status else ""
284 markup_extra.append(status_disp)
285
286 else:
287 if self.contact_list.show_status:
288 status = self.contact_list.getCache(entity, "status", default=None)
289 status_disp = ("status", "\n " + status) if status else ""
290 markup_extra.append(status_disp)
291 widget = self._build_entity_widget(
292 entity,
293 ("cache_nick", "cache_name", "node"),
294 use_bare_jid=True,
295 markup_append=markup_extra,
296 )
297 widgets.append(widget)
298
299 widgets.sort(key=lambda widget: widget.comp)
300
301 for widget in widgets:
302 content.append(widget)
303
304 def _build_specials(self, content):
305 """Build the special entities"""
306 specials = sorted(self.contact_list.get_specials())
307 current = None
308 for entity in specials:
309 if current is not None and current.bare == entity.bare:
310 # nested entity (e.g. MUC private conversations)
311 widget = self._build_entity_widget(
312 entity, ("resource",), markup_prepend=" ", special=True
313 )
314 else:
315 # the special widgets
316 if entity.resource:
317 widget = self._build_entity_widget(entity, ("resource",), special=True)
318 else:
319 widget = self._build_entity_widget(
320 entity,
321 ("cache_nick", "cache_name", "node"),
322 with_show_attr=False,
323 special=True,
324 )
325 content.append(widget)
326
327 def _build_list(self):
328 """Build the main contact list widget"""
329 content = urwid.SimpleListWalker([])
330
331 self._build_specials(content)
332 if self.contact_list._specials:
333 content.append(urwid.Divider("="))
334
335 groups = list(self.contact_list._groups)
336 groups.sort(key=lambda x: x.lower() if x else '')
337 for group in groups:
338 data = self.contact_list.get_group_data(group)
339 folded = data.get(C.GROUP_DATA_FOLDED, False)
340 jids = list(data["jids"])
341 if group is not None and (
342 self.contact_list.any_entity_visible(jids)
343 or self.contact_list.show_empty_groups
344 ):
345 header = "[-]" if not folded else "[+]"
346 widget = sat_widgets.ClickableText(group, header=header + " ")
347 content.append(widget)
348 urwid.connect_signal(widget, "click", self._group_clicked)
349 if not folded:
350 self._build_entities(content, jids)
351 not_in_roster = (
352 set(self.contact_list._cache)
353 .difference(self.contact_list._roster)
354 .difference(self.contact_list._specials)
355 .difference((self.contact_list.whoami.bare,))
356 )
357 if not_in_roster:
358 content.append(urwid.Divider("-"))
359 self._build_entities(content, not_in_roster)
360
361 return urwid.ListBox(content)
362
363
364 quick_widgets.register(QuickContactList, ContactList)