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)