Mercurial > libervia-backend
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) |