Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_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/quick_frontend/quick_contact_list.py@0046283a285d |
children | 2e6864b1d577 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # helper class for making 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 """Contact List handling multi profiles at once, should replace quick_contact_list module in the future""" | |
21 | |
22 from sat.core.i18n import _ | |
23 from sat.core.log import getLogger | |
24 log = getLogger(__name__) | |
25 from sat.core import exceptions | |
26 from sat_frontends.quick_frontend.quick_widgets import QuickWidget | |
27 from sat_frontends.quick_frontend.constants import Const as C | |
28 from sat_frontends.tools import jid | |
29 from collections import OrderedDict | |
30 | |
31 | |
32 try: | |
33 # FIXME: to be removed when an acceptable solution is here | |
34 unicode('') # XXX: unicode doesn't exist in pyjamas | |
35 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
36 # XXX: pyjamas' max doesn't support key argument, so we implement it ourself | |
37 pyjamas_max = max | |
38 | |
39 def max(iterable, key): | |
40 iter_cpy = list(iterable) | |
41 iter_cpy.sort(key=key) | |
42 return pyjamas_max(iter_cpy) | |
43 | |
44 # next doesn't exist in pyjamas | |
45 def next(iterable, *args): | |
46 try: | |
47 return iterable.next() | |
48 except StopIteration as e: | |
49 if args: | |
50 return args[0] | |
51 raise e | |
52 | |
53 | |
54 handler = None | |
55 | |
56 | |
57 class ProfileContactList(object): | |
58 """Contact list data for a single profile""" | |
59 | |
60 def __init__(self, profile): | |
61 self.host = handler.host | |
62 self.profile = profile | |
63 # contain all jids in roster or not, | |
64 # bare jids as keys, resources are used in data | |
65 # XXX: we don't mutualise cache, as values may differ | |
66 # for different profiles (e.g. directed presence) | |
67 self._cache = {} | |
68 | |
69 # special entities (groupchat, gateways, etc) | |
70 # may be bare or full jid | |
71 self._specials = set() | |
72 | |
73 # group data contain jids in groups and misc frontend data | |
74 # None key is used for jids with not group | |
75 self._groups = {} # groups to group data map | |
76 | |
77 # contacts in roster (bare jids) | |
78 self._roster = set() | |
79 | |
80 # selected entities, full jid | |
81 self._selected = set() | |
82 | |
83 # we keep our own jid | |
84 self.whoami = self.host.profiles[profile].whoami | |
85 | |
86 # options | |
87 self.show_disconnected = False | |
88 self.show_empty_groups = True | |
89 self.show_resources = False | |
90 self.show_status = False | |
91 | |
92 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups) | |
93 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts) | |
94 | |
95 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) | |
96 self.presenceListener = self.onPresenceUpdate | |
97 self.host.addListener('presence', self.presenceListener, [self.profile]) | |
98 self.nickListener = self.onNickUpdate | |
99 self.host.addListener('nick', self.nickListener, [self.profile]) | |
100 self.notifListener = self.onNotification | |
101 self.host.addListener('notification', self.notifListener, [self.profile]) | |
102 # notifListener only update the entity, so we can re-use it | |
103 self.host.addListener('notificationsClear', self.notifListener, [self.profile]) | |
104 | |
105 def _showEmptyGroups(self, show_str): | |
106 # Called only by __init__ | |
107 # self.update is not wanted here, as it is done by | |
108 # handler when all profiles are ready | |
109 self.showEmptyGroups(C.bool(show_str)) | |
110 | |
111 def _showOfflineContacts(self, show_str): | |
112 # same comments as for _showEmptyGroups | |
113 self.showOfflineContacts(C.bool(show_str)) | |
114 | |
115 def __contains__(self, entity): | |
116 """Check if entity is in contact list | |
117 | |
118 An entity can be in contact list even if not in roster | |
119 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) | |
120 """ | |
121 if entity.resource: | |
122 try: | |
123 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) | |
124 except KeyError: | |
125 return False | |
126 return entity in self._cache | |
127 | |
128 @property | |
129 def roster(self): | |
130 """Return all the bare JIDs of the roster entities. | |
131 | |
132 @return (set[jid.JID]) | |
133 """ | |
134 return self._roster | |
135 | |
136 @property | |
137 def roster_connected(self): | |
138 """Return all the bare JIDs of the roster entities that are connected. | |
139 | |
140 @return (set[jid.JID]) | |
141 """ | |
142 return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None]) | |
143 | |
144 @property | |
145 def roster_entities_by_group(self): | |
146 """Return a dictionary binding the roster groups to their entities bare JIDs. | |
147 | |
148 This also includes the empty group (None key). | |
149 @return (dict[unicode,set(jid.JID)]) | |
150 """ | |
151 return {group: self._groups[group]['jids'] for group in self._groups} | |
152 | |
153 @property | |
154 def roster_groups_by_entities(self): | |
155 """Return a dictionary binding the entities bare JIDs to their roster groups | |
156 | |
157 @return (dict[jid.JID, set(unicode)]) | |
158 """ | |
159 result = {} | |
160 for group, data in self._groups.iteritems(): | |
161 for entity in data['jids']: | |
162 result.setdefault(entity, set()).add(group) | |
163 return result | |
164 | |
165 @property | |
166 def selected(self): | |
167 """Return contacts currently selected | |
168 | |
169 @return (set): set of selected entities | |
170 """ | |
171 return self._selected | |
172 | |
173 @property | |
174 def all_iter(self): | |
175 """return all know entities in cache as an iterator of tuples | |
176 | |
177 entities are not sorted | |
178 """ | |
179 return self._cache.iteritems() | |
180 | |
181 | |
182 @property | |
183 def items(self): | |
184 """Return item representation for all visible entities in cache | |
185 | |
186 entities are not sorted | |
187 key: bare jid, value: data | |
188 """ | |
189 return {jid_:cache for jid_, cache in self._cache.iteritems() if self.entityToShow(jid_)} | |
190 | |
191 | |
192 def getItem(self, entity): | |
193 """Return item representation of requested entity | |
194 | |
195 @param entity(jid.JID): bare jid of entity | |
196 @raise (KeyError): entity is unknown | |
197 """ | |
198 return self._cache[entity] | |
199 | |
200 def _gotContacts(self, contacts): | |
201 """Called during filling, add contacts and notice parent that contacts are filled""" | |
202 for contact in contacts: | |
203 self.host.newContactHandler(*contact, profile=self.profile) | |
204 handler._contactsFilled(self.profile) | |
205 | |
206 def _fill(self): | |
207 """Get all contacts from backend | |
208 | |
209 Contacts will be cleared before refilling them | |
210 """ | |
211 self.clearContacts(keep_cache=True) | |
212 self.host.bridge.getContacts(self.profile, callback=self._gotContacts) | |
213 | |
214 def fill(self): | |
215 handler.fill(self.profile) | |
216 | |
217 def getCache(self, entity, name=None, bare_default=True): | |
218 """Return a cache value for a contact | |
219 | |
220 @param entity(jid.JID): entity of the contact from who we want data (resource is used if given) | |
221 if a resource specific information is requested: | |
222 - if no resource is given (bare jid), the main resource is used, according to priority | |
223 - if resource is given, it is used | |
224 @param name(unicode): name the data to get, or None to get everything | |
225 @param bare_default(bool, None): if True and entity is a full jid, the value of bare jid | |
226 will be returned if not value is found for the requested resource. | |
227 If False, None is returned if no value is found for the requested resource. | |
228 If None, bare_default will be set to False if entity is in a room, True else | |
229 @return: full cache if no name is given, or value of "name", or None | |
230 """ | |
231 # FIXME: resource handling need to be reworked | |
232 # FIXME: bare_default work for requesting full jid to get bare jid, but not the other way | |
233 # e.g.: if we have set an avatar for user@server.tld/resource and we request user@server.tld | |
234 # we won't get the avatar set in the resource | |
235 try: | |
236 cache = self._cache[entity.bare] | |
237 except KeyError: | |
238 self.setContact(entity) | |
239 cache = self._cache[entity.bare] | |
240 | |
241 if name is None: | |
242 return cache | |
243 | |
244 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): | |
245 # these data are related to the resource | |
246 if not entity.resource: | |
247 main_resource = cache[C.CONTACT_MAIN_RESOURCE] | |
248 if main_resource is None: | |
249 # we ignore presence info if we don't have any resource in cache | |
250 # FIXME: to be checked | |
251 return | |
252 cache = cache[C.CONTACT_RESOURCES].setdefault(main_resource, {}) | |
253 else: | |
254 cache = cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) | |
255 | |
256 if name == 'status': # XXX: we get the first status for 'status' key | |
257 # TODO: manage main language for statuses | |
258 return cache[C.PRESENCE_STATUSES].get(C.PRESENCE_STATUSES_DEFAULT, '') | |
259 | |
260 elif entity.resource: | |
261 try: | |
262 return cache[C.CONTACT_RESOURCES][entity.resource][name] | |
263 except KeyError: | |
264 if bare_default is None: | |
265 bare_default = not self.isRoom(entity.bare) | |
266 if not bare_default: | |
267 return None | |
268 | |
269 try: | |
270 return cache[name] | |
271 except KeyError: | |
272 return None | |
273 | |
274 def setCache(self, entity, name, value): | |
275 """Set or update value for one data in cache | |
276 | |
277 @param entity(JID): entity to update | |
278 @param name(unicode): value to set or update | |
279 """ | |
280 self.setContact(entity, attributes={name: value}) | |
281 | |
282 def getFullJid(self, entity): | |
283 """Get full jid from a bare jid | |
284 | |
285 @param entity(jid.JID): must be a bare jid | |
286 @return (jid.JID): bare jid + main resource | |
287 @raise ValueError: the entity is not bare | |
288 """ | |
289 if entity.resource: | |
290 raise ValueError(u"getFullJid must be used with a bare jid") | |
291 main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE) | |
292 return jid.JID(u"{}/{}".format(entity, main_resource)) | |
293 | |
294 def setGroupData(self, group, name, value): | |
295 """Register a data for a group | |
296 | |
297 @param group: a valid (existing) group name | |
298 @param name: name of the data (can't be "jids") | |
299 @param value: value to set | |
300 """ | |
301 assert name is not 'jids' | |
302 self._groups[group][name] = value | |
303 | |
304 def getGroupData(self, group, name=None): | |
305 """Return value associated to group data | |
306 | |
307 @param group: a valid (existing) group name | |
308 @param name: name of the data or None to get the whole dict | |
309 @return: registered value | |
310 """ | |
311 if name is None: | |
312 return self._groups[group] | |
313 return self._groups[group][name] | |
314 | |
315 def isRoom(self, entity): | |
316 """Helper method to know if entity is a MUC room | |
317 | |
318 @param entity(jid.JID): jid of the entity | |
319 hint: use bare jid here, as room can't be full jid with MUC | |
320 @return (bool): True if entity is a room | |
321 """ | |
322 assert entity.resource is None # FIXME: this may change when MIX will be handled | |
323 return self.isSpecial(entity, C.CONTACT_SPECIAL_GROUP) | |
324 | |
325 def isSpecial(self, entity, special_type): | |
326 """Tell if an entity is of a specialy _type | |
327 | |
328 @param entity(jid.JID): jid of the special entity | |
329 if the jid is full, will be added to special extras | |
330 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) | |
331 @return (bool): True if entity is from this special type | |
332 """ | |
333 return self.getCache(entity, C.CONTACT_SPECIAL) == special_type | |
334 | |
335 def setSpecial(self, entity, special_type): | |
336 """Set special flag on an entity | |
337 | |
338 @param entity(jid.JID): jid of the special entity | |
339 if the jid is full, will be added to special extras | |
340 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag | |
341 """ | |
342 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) | |
343 self.setCache(entity, C.CONTACT_SPECIAL, special_type) | |
344 | |
345 def getSpecials(self, special_type=None, bare=False): | |
346 """Return all the bare JIDs of the special roster entities of with given type. | |
347 | |
348 @param special_type(unicode, None): if not None, filter by special type (e.g. C.CONTACT_SPECIAL_GROUP) | |
349 @param bare(bool): return only bare jids if True | |
350 @return (iter[jid.JID]): found special entities | |
351 """ | |
352 for entity in self._specials: | |
353 if bare and entity.resource: | |
354 continue | |
355 if special_type is not None and self.getCache(entity, C.CONTACT_SPECIAL) != special_type: | |
356 continue | |
357 yield entity | |
358 | |
359 def disconnect(self): | |
360 # for now we just clear contacts on disconnect | |
361 self.clearContacts() | |
362 | |
363 def clearContacts(self, keep_cache=False): | |
364 """Clear all the contact list | |
365 | |
366 @param keep_cache: if True, don't reset the cache | |
367 """ | |
368 self.select(None) | |
369 if not keep_cache: | |
370 self._cache.clear() | |
371 self._groups.clear() | |
372 self._specials.clear() | |
373 self._roster.clear() | |
374 self.update() | |
375 | |
376 def setContact(self, entity, groups=None, attributes=None, in_roster=False): | |
377 """Add a contact to the list if doesn't exist, else update it. | |
378 | |
379 This method can be called with groups=None for the purpose of updating | |
380 the contact's attributes (e.g. nickname). In that case, the groups | |
381 attribute must not be set to the default group but ignored. If not, | |
382 you may move your contact from its actual group(s) to the default one. | |
383 | |
384 None value for 'groups' has a different meaning than [None] which is for the default group. | |
385 | |
386 @param entity (jid.JID): entity to add or replace | |
387 if entity is a full jid, attributes will be cached in for the full jid only | |
388 @param groups (list): list of groups or None to ignore the groups membership. | |
389 @param attributes (dict): attibutes of the added jid or to update | |
390 if attribute value is None, it will be removed | |
391 @param in_roster (bool): True if contact is from roster | |
392 """ | |
393 if attributes is None: | |
394 attributes = {} | |
395 | |
396 entity_bare = entity.bare | |
397 update_type = C.UPDATE_MODIFY if entity_bare in self._cache else C.UPDATE_ADD | |
398 | |
399 if in_roster: | |
400 self._roster.add(entity_bare) | |
401 | |
402 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}, | |
403 C.CONTACT_MAIN_RESOURCE: None, | |
404 C.CONTACT_SELECTED: set()}) | |
405 | |
406 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes | |
407 | |
408 # we set groups and fill self._groups accordingly | |
409 if groups is not None: | |
410 if not groups: | |
411 groups = [None] # [None] is the default group | |
412 if C.CONTACT_GROUPS in cache: | |
413 # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS] | |
414 for group in [group for group in cache[C.CONTACT_GROUPS] if group not in groups]: | |
415 self._groups[group]['jids'].remove(entity_bare) | |
416 cache[C.CONTACT_GROUPS] = groups | |
417 for group in groups: | |
418 self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare) | |
419 | |
420 # special entities management | |
421 if C.CONTACT_SPECIAL in attributes: | |
422 if attributes[C.CONTACT_SPECIAL] is None: | |
423 del attributes[C.CONTACT_SPECIAL] | |
424 self._specials.remove(entity) | |
425 else: | |
426 self._specials.add(entity) | |
427 cache[C.CONTACT_MAIN_RESOURCE] = None | |
428 | |
429 # now the attributes we keep in cache | |
430 # XXX: if entity is a full jid, we store the value for the resource only | |
431 cache_attr = cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) if entity.resource else cache | |
432 for attribute, value in attributes.iteritems(): | |
433 if value is None: | |
434 # XXX: pyjamas hack: we need to use pop instead of del | |
435 try: | |
436 cache_attr[attribute].pop(value) | |
437 except KeyError: | |
438 pass | |
439 else: | |
440 cache_attr[attribute] = value | |
441 | |
442 # we can update the display | |
443 self.update([entity], update_type, self.profile) | |
444 | |
445 def entityToShow(self, entity, check_resource=False): | |
446 """Tell if the contact should be showed or hidden. | |
447 | |
448 @param entity (jid.JID): jid of the contact | |
449 @param check_resource (bool): True if resource must be significant | |
450 @return (bool): True if that contact should be showed in the list | |
451 """ | |
452 show = self.getCache(entity, C.PRESENCE_SHOW) | |
453 | |
454 if check_resource: | |
455 selected = self._selected | |
456 else: | |
457 selected = {selected.bare for selected in self._selected} | |
458 return ((show is not None and show != C.PRESENCE_UNAVAILABLE) | |
459 or self.show_disconnected | |
460 or entity in selected | |
461 or next(self.host.getNotifs(entity.bare, profile=self.profile), None) | |
462 ) | |
463 | |
464 def anyEntityToShow(self, entities, check_resources=False): | |
465 """Tell if in a list of entities, at least one should be shown | |
466 | |
467 @param entities (list[jid.JID]): list of jids | |
468 @param check_resources (bool): True if resources must be significant | |
469 @return (bool): True if a least one entity need to be shown | |
470 """ | |
471 # FIXME: looks inefficient, really needed? | |
472 for entity in entities: | |
473 if self.entityToShow(entity, check_resources): | |
474 return True | |
475 return False | |
476 | |
477 def isEntityInGroup(self, entity, group): | |
478 """Tell if an entity is in a roster group | |
479 | |
480 @param entity(jid.JID): jid of the entity | |
481 @param group(unicode): group to check | |
482 @return (bool): True if the entity is in the group | |
483 """ | |
484 return entity in self.getGroupData(group, "jids") | |
485 | |
486 def removeContact(self, entity): | |
487 """remove a contact from the list | |
488 | |
489 @param entity(jid.JID): jid of the entity to remove (bare jid is used) | |
490 """ | |
491 entity_bare = entity.bare | |
492 try: | |
493 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) | |
494 except KeyError: | |
495 log.error(_(u"Trying to delete an unknow entity [{}]").format(entity)) | |
496 try: | |
497 self._roster.remove(entity_bare) | |
498 except KeyError: | |
499 pass | |
500 del self._cache[entity_bare] | |
501 for group in groups: | |
502 self._groups[group]['jids'].remove(entity_bare) | |
503 if not self._groups[group]['jids']: | |
504 self._groups.pop(group) # FIXME: we use pop because of pyjamas: http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en | |
505 for iterable in (self._selected, self._specials): | |
506 to_remove = set() | |
507 for set_entity in iterable: | |
508 if set_entity.bare == entity.bare: | |
509 to_remove.add(set_entity) | |
510 iterable.difference_update(to_remove) | |
511 self.update([entity], C.UPDATE_DELETE, self.profile) | |
512 | |
513 def onPresenceUpdate(self, entity, show, priority, statuses, profile): | |
514 """Update entity's presence status | |
515 | |
516 @param entity(jid.JID): entity updated | |
517 @param show: availability | |
518 @parap priority: resource's priority | |
519 @param statuses: dict of statuses | |
520 @param profile: %(doc_profile)s | |
521 """ | |
522 cache = self.getCache(entity) | |
523 if show == C.PRESENCE_UNAVAILABLE: | |
524 if not entity.resource: | |
525 cache[C.CONTACT_RESOURCES].clear() | |
526 cache[C.CONTACT_MAIN_RESOURCE] = None | |
527 else: | |
528 try: | |
529 del cache[C.CONTACT_RESOURCES][entity.resource] | |
530 except KeyError: | |
531 log.error(u"Presence unavailable received for an unknown resource [{}]".format(entity)) | |
532 if not cache[C.CONTACT_RESOURCES]: | |
533 cache[C.CONTACT_MAIN_RESOURCE] = None | |
534 else: | |
535 if not entity.resource: | |
536 log.warning(_(u"received presence from entity without resource: {}".format(entity))) | |
537 resources_data = cache[C.CONTACT_RESOURCES] | |
538 resource_data = resources_data.setdefault(entity.resource, {}) | |
539 resource_data[C.PRESENCE_SHOW] = show | |
540 resource_data[C.PRESENCE_PRIORITY] = int(priority) | |
541 resource_data[C.PRESENCE_STATUSES] = statuses | |
542 | |
543 if entity.bare not in self._specials: | |
544 # we may have resources with no priority | |
545 # (when a cached value is added for a not connected resource) | |
546 priority_resource = max(resources_data, key=lambda res: resources_data[res].get(C.PRESENCE_PRIORITY, -2**32)) | |
547 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource | |
548 self.update([entity], C.UPDATE_MODIFY, self.profile) | |
549 | |
550 def onNickUpdate(self, entity, new_nick, profile): | |
551 """Update entity's nick | |
552 | |
553 @param entity(jid.JID): entity updated | |
554 @param new_nick(unicode): new nick of the entity | |
555 @param profile: %(doc_profile)s | |
556 """ | |
557 assert profile == self.profile | |
558 self.setCache(entity, 'nick', new_nick) | |
559 self.update([entity], C.UPDATE_MODIFY, profile) | |
560 | |
561 def onNotification(self, entity, notif, profile): | |
562 """Update entity with notification | |
563 | |
564 @param entity(jid.JID): entity updated | |
565 @param notif(dict): notification data | |
566 @param profile: %(doc_profile)s | |
567 """ | |
568 assert profile == self.profile | |
569 if entity is not None: | |
570 self.update([entity], C.UPDATE_MODIFY, profile) | |
571 | |
572 def unselect(self, entity): | |
573 """Unselect an entity | |
574 | |
575 @param entity(jid.JID): entity to unselect | |
576 """ | |
577 try: | |
578 cache = self._cache[entity.bare] | |
579 except: | |
580 log.error(u"Try to unselect an entity not in cache") | |
581 else: | |
582 try: | |
583 cache[C.CONTACT_SELECTED].remove(entity.resource) | |
584 except KeyError: | |
585 log.error(u"Try to unselect a not selected entity") | |
586 else: | |
587 self._selected.remove(entity) | |
588 self.update([entity], C.UPDATE_SELECTION) | |
589 | |
590 def select(self, entity): | |
591 """Select an entity | |
592 | |
593 @param entity(jid.JID, None): entity to select (resource is significant) | |
594 None to unselect all entities | |
595 """ | |
596 if entity is None: | |
597 self._selected.clear() | |
598 for cache in self._cache.itervalues(): | |
599 cache[C.CONTACT_SELECTED].clear() | |
600 self.update(type_=C.UPDATE_SELECTION, profile=self.profile) | |
601 else: | |
602 log.debug(u"select %s" % entity) | |
603 try: | |
604 cache = self._cache[entity.bare] | |
605 except: | |
606 log.error(u"Try to select an entity not in cache") | |
607 else: | |
608 cache[C.CONTACT_SELECTED].add(entity.resource) | |
609 self._selected.add(entity) | |
610 self.update([entity], C.UPDATE_SELECTION, profile=self.profile) | |
611 | |
612 def showOfflineContacts(self, show): | |
613 """Tell if offline contacts should shown | |
614 | |
615 @param show(bool): True if offline contacts should be shown | |
616 """ | |
617 assert isinstance(show, bool) | |
618 if self.show_disconnected == show: | |
619 return | |
620 self.show_disconnected = show | |
621 self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile) | |
622 | |
623 def showEmptyGroups(self, show): | |
624 assert isinstance(show, bool) | |
625 if self.show_empty_groups == show: | |
626 return | |
627 self.show_empty_groups = show | |
628 self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile) | |
629 | |
630 def showResources(self, show): | |
631 assert isinstance(show, bool) | |
632 if self.show_resources == show: | |
633 return | |
634 self.show_resources = show | |
635 self.update(profile=self.profile) | |
636 | |
637 def plug(self): | |
638 handler.addProfile(self.profile) | |
639 | |
640 def unplug(self): | |
641 handler.removeProfile(self.profile) | |
642 | |
643 def update(self, entities=None, type_=None, profile=None): | |
644 handler.update(entities, type_, profile) | |
645 | |
646 | |
647 class QuickContactListHandler(object): | |
648 | |
649 def __init__(self, host): | |
650 super(QuickContactListHandler, self).__init__() | |
651 self.host = host | |
652 global handler | |
653 if handler is not None: | |
654 raise exceptions.InternalError(u"QuickContactListHandler must be instanciated only once") | |
655 handler = self | |
656 self._clist = {} # key: profile, value: ProfileContactList | |
657 self._widgets = set() | |
658 self._update_locked = False # se to True to ignore updates | |
659 | |
660 def __getitem__(self, profile): | |
661 """Return ProfileContactList instance for the requested profile""" | |
662 return self._clist[profile] | |
663 | |
664 def __contains__(self, entity): | |
665 """Check if entity is in contact list | |
666 | |
667 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) | |
668 """ | |
669 for contact_list in self._clist.itervalues(): | |
670 if entity in contact_list: | |
671 return True | |
672 return False | |
673 | |
674 @property | |
675 def roster(self): | |
676 """Return all the bare JIDs of the roster entities. | |
677 | |
678 @return (set[jid.JID]) | |
679 """ | |
680 entities = set() | |
681 for contact_list in self._clist.itervalues(): | |
682 entities.update(contact_list.roster) | |
683 return entities | |
684 | |
685 @property | |
686 def roster_connected(self): | |
687 """Return all the bare JIDs of the roster entities that are connected. | |
688 | |
689 @return (set[jid.JID]) | |
690 """ | |
691 entities = set() | |
692 for contact_list in self._clist.itervalues(): | |
693 entities.update(contact_list.roster_connected) | |
694 return entities | |
695 | |
696 @property | |
697 def roster_entities_by_group(self): | |
698 """Return a dictionary binding the roster groups to their entities bare | |
699 JIDs. This also includes the empty group (None key). | |
700 | |
701 @return (dict[unicode,set(jid.JID)]) | |
702 """ | |
703 groups = {} | |
704 for contact_list in self._clist.itervalues(): | |
705 groups.update(contact_list.roster_entities_by_group) | |
706 return groups | |
707 | |
708 @property | |
709 def roster_groups_by_entities(self): | |
710 """Return a dictionary binding the entities bare JIDs to their roster | |
711 groups. | |
712 | |
713 @return (dict[jid.JID, set(unicode)]) | |
714 """ | |
715 entities = {} | |
716 for contact_list in self._clist.itervalues(): | |
717 entities.update(contact_list.roster_groups_by_entities) | |
718 return entities | |
719 | |
720 @property | |
721 def selected(self): | |
722 """Return contacts currently selected | |
723 | |
724 @return (set): set of selected entities | |
725 """ | |
726 entities = set() | |
727 for contact_list in self._clist.itervalues(): | |
728 entities.update(contact_list.selected) | |
729 return entities | |
730 | |
731 @property | |
732 def all_iter(self): | |
733 """Return item representation for all entities in cache | |
734 | |
735 items are unordered | |
736 """ | |
737 for profile, contact_list in self._clist.iteritems(): | |
738 for bare_jid, cache in contact_list.all_iter: | |
739 data = cache.copy() | |
740 data[C.CONTACT_PROFILE] = profile | |
741 yield bare_jid, data | |
742 | |
743 @property | |
744 def items(self): | |
745 """Return item representation for visible entities in cache | |
746 | |
747 items are unordered | |
748 key: bare jid, value: data | |
749 """ | |
750 items = {} | |
751 for profile, contact_list in self._clist.iteritems(): | |
752 for bare_jid, cache in contact_list.items.iteritems(): | |
753 data = cache.copy() | |
754 items[bare_jid] = data | |
755 data[C.CONTACT_PROFILE] = profile | |
756 return items | |
757 | |
758 @property | |
759 def items_sorted(self): | |
760 """Return item representation for visible entities in cache | |
761 | |
762 items are ordered using self.items_sort | |
763 key: bare jid, value: data | |
764 """ | |
765 return self.items_sort(self.items) | |
766 | |
767 def items_sort(self, items): | |
768 """sort items | |
769 | |
770 @param items(dict): items to sort (will be emptied !) | |
771 @return (OrderedDict): sorted items | |
772 """ | |
773 ordered_items = OrderedDict() | |
774 bare_jids = sorted(items.keys()) | |
775 for jid_ in bare_jids: | |
776 ordered_items[jid_] = items.pop(jid_) | |
777 return ordered_items | |
778 | |
779 def register(self, widget): | |
780 """Register a QuickContactList widget | |
781 | |
782 This method should only be used in QuickContactList | |
783 """ | |
784 self._widgets.add(widget) | |
785 | |
786 def unregister(self, widget): | |
787 """Unregister a QuickContactList widget | |
788 | |
789 This method should only be used in QuickContactList | |
790 """ | |
791 self._widgets.remove(widget) | |
792 | |
793 def addProfiles(self, profiles): | |
794 """Add a contact list for plugged profiles | |
795 | |
796 @param profile(iterable[unicode]): plugged profiles | |
797 """ | |
798 for profile in profiles: | |
799 if profile not in self._clist: | |
800 self._clist[profile] = ProfileContactList(profile) | |
801 return [self._clist[profile] for profile in profiles] | |
802 | |
803 def addProfile(self, profile): | |
804 return self.addProfiles([profile])[0] | |
805 | |
806 def removeProfiles(self, profiles): | |
807 """Remove given unplugged profiles from contact list | |
808 | |
809 @param profile(iterable[unicode]): unplugged profiles | |
810 """ | |
811 for profile in profiles: | |
812 del self._clist[profile] | |
813 | |
814 def removeProfile(self, profile): | |
815 self.removeProfiles([profile]) | |
816 | |
817 def getSpecialExtras(self, special_type=None): | |
818 """Return special extras with given type | |
819 | |
820 If special_type is None, return all special extras. | |
821 | |
822 @param special_type(unicode, None): one of special type (e.g. C.CONTACT_SPECIAL_GROUP) | |
823 None to return all special extras. | |
824 @return (set[jid.JID]) | |
825 """ | |
826 entities = set() | |
827 for contact_list in self._clist.itervalues(): | |
828 entities.update(contact_list.getSpecialExtras(special_type)) | |
829 return entities | |
830 | |
831 def _contactsFilled(self, profile): | |
832 self._to_fill.remove(profile) | |
833 if not self._to_fill: | |
834 del self._to_fill | |
835 self.update() | |
836 | |
837 def fill(self, profile=None): | |
838 """Get all contacts from backend, and fill the widget | |
839 | |
840 Contacts will be cleared before refilling them | |
841 @param profile(unicode, None): profile to fill | |
842 None to fill all profiles | |
843 """ | |
844 try: | |
845 to_fill = self._to_fill | |
846 except AttributeError: | |
847 to_fill = self._to_fill = set() | |
848 | |
849 # if check if profiles have already been filled | |
850 # to void filling them several times | |
851 filled = to_fill.copy() | |
852 | |
853 if profile is not None: | |
854 assert profile in self._clist | |
855 to_fill.add(profile) | |
856 else: | |
857 to_fill.update(self._clist.items()) | |
858 | |
859 remaining = to_fill.difference(filled) | |
860 if remaining != to_fill: | |
861 log.debug(u"Not re-filling already filled contact list(s) for {}".format(u', '.join(to_fill.intersection(filled)))) | |
862 for profile in remaining: | |
863 self._clist[profile]._fill() | |
864 | |
865 def clearContacts(self, keep_cache=False): | |
866 """Clear all the contact list | |
867 | |
868 @param keep_cache: if True, don't reset the cache | |
869 """ | |
870 for contact_list in self._clist.itervalues(): | |
871 contact_list.clearContacts(keep_cache) | |
872 self.update() | |
873 | |
874 def select(self, entity): | |
875 for contact_list in self._clist.itervalues(): | |
876 contact_list.select(entity) | |
877 | |
878 def unselect(self, entity): | |
879 for contact_list in self._clist.itervalues(): | |
880 contact_list.select(entity) | |
881 | |
882 def lockUpdate(self, locked=True, do_update=True): | |
883 """Forbid contact list updates | |
884 | |
885 Used mainly while profiles are plugged, as many updates can occurs, causing | |
886 an impact on performances | |
887 @param locked(bool): updates are forbidden if True | |
888 @param do_update(bool): if True, a full update is done after unlocking | |
889 if set to False, widget state can be inconsistent, be sure to know | |
890 what youa re doing! | |
891 """ | |
892 log.debug(u"Contact lists updates are now {}".format(u"LOCKED" if locked else u"UNLOCKED")) | |
893 self._update_locked = locked | |
894 if not locked and do_update: | |
895 self.update() | |
896 | |
897 def update(self, entities=None, type_=None, profile=None): | |
898 if not self._update_locked: | |
899 for widget in self._widgets: | |
900 widget.update(entities, type_, profile) | |
901 | |
902 | |
903 class QuickContactList(QuickWidget): | |
904 """This class manage the visual representation of contacts""" | |
905 SINGLE=False | |
906 PROFILES_MULTIPLE=True | |
907 PROFILES_ALLOW_NONE=True # Can be linked to no profile (e.g. at the early forntend start) | |
908 | |
909 def __init__(self, host, profiles): | |
910 super(QuickContactList, self).__init__(host, None, profiles) | |
911 | |
912 # options | |
913 # for next values, None means use indivual value per profile | |
914 # True or False mean override these values for all profiles | |
915 self.show_disconnected = None # TODO | |
916 self.show_empty_groups = None # TODO | |
917 self.show_resources = None # TODO | |
918 self.show_status = None # TODO | |
919 | |
920 def postInit(self): | |
921 """Method to be called by frontend after widget is initialised""" | |
922 handler.register(self) | |
923 | |
924 @property | |
925 def all_iter(self): | |
926 return handler.all_iter | |
927 | |
928 @property | |
929 def items(self): | |
930 return handler.items | |
931 | |
932 @property | |
933 def items_sorted(self): | |
934 return handler.items | |
935 | |
936 def update(self, entities=None, type_=None, profile=None): | |
937 """Update the display when something changed | |
938 | |
939 @param entities(iterable[jid.JID], None): updated entities, | |
940 None to update the whole contact list | |
941 @param type_(unicode, None): update type, may be: | |
942 - C.UPDATE_DELETE: entity deleted | |
943 - C.UPDATE_MODIFY: entity updated | |
944 - C.UPDATE_ADD: entity added | |
945 - C.UPDATE_SELECTION: selection modified | |
946 or None for undefined update | |
947 @param profile(unicode, None): profile concerned with the update | |
948 None if unknown | |
949 """ | |
950 raise NotImplementedError | |
951 | |
952 def onDelete(self): | |
953 QuickWidget.onDelete(self) | |
954 handler.unregister(self) |