Mercurial > libervia-backend
comparison sat_frontends/primitivus/contact_list.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/primitivus/contact_list.py@0046283a285d |
children | 81b70eeb710f |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Primitivus: a SAT frontend | |
5 # Copyright (C) 2009-2018 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 sat.core.i18n import _ | |
21 import urwid | |
22 from urwid_satext import sat_widgets | |
23 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList | |
24 from sat_frontends.primitivus.status import StatusBar | |
25 from sat_frontends.primitivus.constants import Const as C | |
26 from sat_frontends.primitivus.keys import action_key_map as a_key | |
27 from sat_frontends.primitivus.widget import PrimitivusWidget | |
28 from sat_frontends.tools import jid | |
29 from sat.core import log as logging | |
30 log = logging.getLogger(__name__) | |
31 from sat_frontends.quick_frontend import quick_widgets | |
32 | |
33 | |
34 class ContactList(PrimitivusWidget, QuickContactList): | |
35 PROFILES_MULTIPLE=False | |
36 PROFILES_ALLOW_NONE=False | |
37 signals = ['click','change'] | |
38 # FIXME: Only single profile is managed so far | |
39 | |
40 def __init__(self, host, target, on_click=None, on_change=None, user_data=None, profiles=None): | |
41 QuickContactList.__init__(self, host, profiles) | |
42 self.contact_list = self.host.contact_lists[self.profile] | |
43 | |
44 #we now build the widget | |
45 self.status_bar = StatusBar(host) | |
46 self.frame = sat_widgets.FocusFrame(self._buildList(), None, self.status_bar) | |
47 PrimitivusWidget.__init__(self, self.frame, _(u'Contacts')) | |
48 if on_click: | |
49 urwid.connect_signal(self, 'click', on_click, user_data) | |
50 if on_change: | |
51 urwid.connect_signal(self, 'change', on_change, user_data) | |
52 self.host.addListener('notification', self.onNotification, [self.profile]) | |
53 self.host.addListener('notificationsClear', self.onNotification, [self.profile]) | |
54 self.postInit() | |
55 | |
56 def update(self, entities=None, type_=None, profile=None): | |
57 """Update display, keep focus""" | |
58 # FIXME: full update is done each time, must handle entities, type_ and profile | |
59 widget, position = self.frame.body.get_focus() | |
60 self.frame.body = self._buildList() | |
61 if position: | |
62 try: | |
63 self.frame.body.focus_position = position | |
64 except IndexError: | |
65 pass | |
66 self._invalidate() | |
67 self.host.redraw() # FIXME: check if can be avoided | |
68 | |
69 def keypress(self, size, key): | |
70 # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent, | |
71 # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element | |
72 if key in sat_widgets.FOCUS_KEYS: | |
73 if (key == a_key['FOCUS_SWITCH'] or (key == a_key['FOCUS_UP'] and self.frame.focus_position == 'body') or | |
74 (key == a_key['FOCUS_DOWN'] and self.frame.focus_position == 'footer')): | |
75 return key | |
76 if key == a_key['STATUS_HIDE']: #user wants to (un)hide contacts' statuses | |
77 self.contact_list.show_status = not self.contact_list.show_status | |
78 self.update() | |
79 elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts | |
80 self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.boolConst(not self.contact_list.show_disconnected), "General", profile_key=self.profile) | |
81 elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources | |
82 self.contact_list.showResources(not self.contact_list.show_resources) | |
83 self.update() | |
84 return super(ContactList, self).keypress(size, key) | |
85 | |
86 # QuickWidget methods | |
87 | |
88 @staticmethod | |
89 def getWidgetHash(target, profiles): | |
90 profiles = sorted(profiles) | |
91 return tuple(profiles) | |
92 | |
93 # modify the contact list | |
94 | |
95 def setFocus(self, text, select=False): | |
96 """give focus to the first element that matches the given text. You can also | |
97 pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode). | |
98 | |
99 @param text: contact group name, contact or muc userhost, muc private dialog jid | |
100 @param select: if True, the element is also clicked | |
101 """ | |
102 idx = 0 | |
103 for widget in self.frame.body.body: | |
104 try: | |
105 if isinstance(widget, sat_widgets.ClickableText): | |
106 # contact group | |
107 value = widget.getValue() | |
108 elif isinstance(widget, sat_widgets.SelectableText): | |
109 # contact or muc | |
110 value = widget.data | |
111 else: | |
112 # Divider instance | |
113 continue | |
114 # there's sometimes a leading space | |
115 if text.strip() == value.strip(): | |
116 self.frame.body.focus_position = idx | |
117 if select: | |
118 self._contactClicked(False, widget, True) | |
119 return | |
120 except AttributeError: | |
121 pass | |
122 idx += 1 | |
123 | |
124 log.debug(u"Not element found for {} in setFocus".format(text)) | |
125 | |
126 # events | |
127 | |
128 def _groupClicked(self, group_wid): | |
129 group = group_wid.getValue() | |
130 data = self.contact_list.getGroupData(group) | |
131 data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False) | |
132 self.setFocus(group) | |
133 self.update() | |
134 | |
135 def _contactClicked(self, use_bare_jid, contact_wid, selected): | |
136 """Method called when a contact is clicked | |
137 | |
138 @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget. | |
139 @param contact_wid: widget of the contact, must have the entity set in data attribute | |
140 @param selected: boolean returned by the widget, telling if it is selected | |
141 """ | |
142 entity = contact_wid.data | |
143 self.host.modeHint(C.MODE_INSERTION) | |
144 self._emit('click', entity) | |
145 | |
146 def onNotification(self, entity, notif, profile): | |
147 notifs = list(self.host.getNotifs(C.ENTITY_ALL, profile=self.profile)) | |
148 if notifs: | |
149 self.title_dynamic = u"({})".format(len(notifs)) | |
150 else: | |
151 self.title_dynamic = None | |
152 self.host.redraw() # FIXME: should not be necessary | |
153 | |
154 # Methods to build the widget | |
155 | |
156 def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_notifs=True, with_show_attr=True, markup_prepend=None, markup_append=None, special=False): | |
157 """Build one contact markup data | |
158 | |
159 @param entity (jid.JID): entity to build | |
160 @param keys (iterable): value to markup, in preferred order. | |
161 The first available key will be used. | |
162 If key starts with "cache_", it will be checked in cache, | |
163 else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')). | |
164 If nothing full or keys is None, full entity is used. | |
165 @param use_bare_jid (bool): if True, use bare jid for selected comparisons | |
166 @param with_notifs (bool): if True, show notification count | |
167 @param with_show_attr (bool): if True, show color corresponding to presence status | |
168 @param markup_prepend (list): markup to prepend to the generated one before building the widget | |
169 @param markup_append (list): markup to append to the generated one before building the widget | |
170 @param special (bool): True if entity is a special one | |
171 @return (list): markup data are expected by Urwid text widgets | |
172 """ | |
173 markup = [] | |
174 if use_bare_jid: | |
175 selected = {entity.bare for entity in self.contact_list._selected} | |
176 else: | |
177 selected = self.contact_list._selected | |
178 if keys is None: | |
179 entity_txt = entity | |
180 else: | |
181 cache = self.contact_list.getCache(entity) | |
182 for key in keys: | |
183 if key.startswith('cache_'): | |
184 entity_txt = cache.get(key[6:]) | |
185 else: | |
186 entity_txt = getattr(entity, key) | |
187 if entity_txt: | |
188 break | |
189 if not entity_txt: | |
190 entity_txt = entity | |
191 | |
192 if with_show_attr: | |
193 show = self.contact_list.getCache(entity, C.PRESENCE_SHOW) | |
194 if show is None: | |
195 show = C.PRESENCE_UNAVAILABLE | |
196 show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default')) | |
197 markup.insert(0, u"{} ".format(show_icon)) | |
198 else: | |
199 entity_attr = 'default' | |
200 | |
201 notifs = list(self.host.getNotifs(entity, exact_jid=special, profile=self.profile)) | |
202 if notifs: | |
203 header = [('cl_notifs', u'({})'.format(len(notifs))), u' '] | |
204 if list(self.host.getNotifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile)): | |
205 header = ('cl_mention', header) | |
206 else: | |
207 header = u'' | |
208 | |
209 markup.append((entity_attr, entity_txt)) | |
210 if markup_prepend: | |
211 markup.insert(0, markup_prepend) | |
212 if markup_append: | |
213 markup.extend(markup_append) | |
214 | |
215 widget = sat_widgets.SelectableText(markup, | |
216 selected = entity in selected, | |
217 header = header) | |
218 widget.data = entity | |
219 widget.comp = entity_txt.lower() # value to use for sorting | |
220 urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid]) | |
221 return widget | |
222 | |
223 def _buildEntities(self, content, entities): | |
224 """Add entity representation in widget list | |
225 | |
226 @param content: widget list, e.g. SimpleListWalker | |
227 @param entities (iterable): iterable of JID to display | |
228 """ | |
229 if not entities: | |
230 return | |
231 widgets = [] # list of built widgets | |
232 | |
233 for entity in entities: | |
234 if entity in self.contact_list._specials or not self.contact_list.entityToShow(entity): | |
235 continue | |
236 markup_extra = [] | |
237 if self.contact_list.show_resources: | |
238 for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES): | |
239 resource_disp = ('resource_main' if resource == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n " + resource) | |
240 markup_extra.append(resource_disp) | |
241 if self.contact_list.show_status: | |
242 status = self.contact_list.getCache(jid.JID('%s/%s' % (entity, resource)), 'status') | |
243 status_disp = ('status', "\n " + status) if status else "" | |
244 markup_extra.append(status_disp) | |
245 | |
246 | |
247 else: | |
248 if self.contact_list.show_status: | |
249 status = self.contact_list.getCache(entity, 'status') | |
250 status_disp = ('status', "\n " + status) if status else "" | |
251 markup_extra.append(status_disp) | |
252 widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), use_bare_jid=True, markup_append=markup_extra) | |
253 widgets.append(widget) | |
254 | |
255 widgets.sort(key=lambda widget: widget.comp) | |
256 | |
257 for widget in widgets: | |
258 content.append(widget) | |
259 | |
260 def _buildSpecials(self, content): | |
261 """Build the special entities""" | |
262 specials = sorted(self.contact_list.getSpecials()) | |
263 current = None | |
264 for entity in specials: | |
265 if current is not None and current.bare == entity.bare: | |
266 # nested entity (e.g. MUC private conversations) | |
267 widget = self._buildEntityWidget(entity, ('resource',), markup_prepend=' ', special=True) | |
268 else: | |
269 # the special widgets | |
270 if entity.resource: | |
271 widget = self._buildEntityWidget(entity, ('resource',), special=True) | |
272 else: | |
273 widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False, special=True) | |
274 content.append(widget) | |
275 | |
276 def _buildList(self): | |
277 """Build the main contact list widget""" | |
278 content = urwid.SimpleListWalker([]) | |
279 | |
280 self._buildSpecials(content) | |
281 if self.contact_list._specials: | |
282 content.append(urwid.Divider('=')) | |
283 | |
284 groups = list(self.contact_list._groups) | |
285 groups.sort(key=lambda x: x.lower() if x else x) | |
286 for group in groups: | |
287 data = self.contact_list.getGroupData(group) | |
288 folded = data.get(C.GROUP_DATA_FOLDED, False) | |
289 jids = list(data['jids']) | |
290 if group is not None and (self.contact_list.anyEntityToShow(jids) or self.contact_list.show_empty_groups): | |
291 header = '[-]' if not folded else '[+]' | |
292 widget = sat_widgets.ClickableText(group, header=header + ' ') | |
293 content.append(widget) | |
294 urwid.connect_signal(widget, 'click', self._groupClicked) | |
295 if not folded: | |
296 self._buildEntities(content, jids) | |
297 not_in_roster = set(self.contact_list._cache).difference(self.contact_list._roster).difference(self.contact_list._specials).difference((self.contact_list.whoami.bare,)) | |
298 if not_in_roster: | |
299 content.append(urwid.Divider('-')) | |
300 self._buildEntities(content, not_in_roster) | |
301 | |
302 return urwid.ListBox(content) | |
303 | |
304 quick_widgets.register(QuickContactList, ContactList) |