comparison frontends/src/quick_frontend/quick_contact_list.py @ 1938:011eff37e21d

quick frontend, primitivus: quickContactList refactored to handle several profiles at once
author Goffi <goffi@goffi.org>
date Mon, 18 Apr 2016 18:31:13 +0200
parents 2daf7b4c6756
children e68483c5a999
comparison
equal deleted inserted replaced
1937:14a33c2b1b2a 1938:011eff37e21d
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 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/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 """Contact List handling multi profiles at once, should replace quick_contact_list module in the future"""
21
20 from sat.core.i18n import _ 22 from sat.core.i18n import _
21 from sat.core.log import getLogger 23 from sat.core.log import getLogger
22 log = getLogger(__name__) 24 log = getLogger(__name__)
25 from sat.core import exceptions
23 from sat_frontends.quick_frontend.quick_widgets import QuickWidget 26 from sat_frontends.quick_frontend.quick_widgets import QuickWidget
24 from sat_frontends.quick_frontend.constants import Const as C 27 from sat_frontends.quick_frontend.constants import Const as C
25 from sat_frontends.tools import jid 28 from sat_frontends.tools import jid
29 from collections import OrderedDict
26 30
27 31
28 try: 32 try:
29 # FIXME: to be removed when an acceptable solution is here 33 # FIXME: to be removed when an acceptable solution is here
30 unicode('') # XXX: unicode doesn't exist in pyjamas 34 unicode('') # XXX: unicode doesn't exist in pyjamas
35 def max(iterable, key): 39 def max(iterable, key):
36 iter_cpy = list(iterable) 40 iter_cpy = list(iterable)
37 iter_cpy.sort(key=key) 41 iter_cpy.sort(key=key)
38 return pyjamas_max(iter_cpy) 42 return pyjamas_max(iter_cpy)
39 43
40 44 handler = None
41 class QuickContactList(QuickWidget): 45
42 """This class manage the visual representation of contacts""" 46
43 47 class ProfileContactList(object):
44 def __init__(self, host, profile): 48 """Contact list data for a single profile"""
45 log.debug(_("Contact List init")) 49
46 super(QuickContactList, self).__init__(host, profile, profile) 50 def __init__(self, profile):
51 self.host = handler.host
52 self.profile = profile
53 # contain all jids in roster or not,
47 # bare jids as keys, resources are used in data 54 # bare jids as keys, resources are used in data
55 # XXX: we don't mutualise cache, as values may differ
56 # for different profiles (e.g. directed presence)
48 self._cache = {} 57 self._cache = {}
49 58
50 # special entities (groupchat, gateways, etc), bare jids 59 # special entities (groupchat, gateways, etc), bare jids
51 self._specials = set() 60 self._specials = set()
52 # extras are specials with full jids (e.g.: private MUC conversation) 61 # extras are specials with full jids (e.g.: private MUC conversation)
53 self._special_extras = set() 62 self._special_extras = set()
54 63
55 # group data contain jids in groups and misc frontend data 64 # group data contain jids in groups and misc frontend data
65 # None key is used for jids with not group
56 self._groups = {} # groups to group data map 66 self._groups = {} # groups to group data map
57 67
58 # contacts in roster (bare jids) 68 # contacts in roster (bare jids)
59 self._roster = set() 69 self._roster = set()
60 70
61 # entities with alert(s) and their counts (usually a waiting message), dict{full jid: int) 71 # alerts per entity (key: full jid, value: list of alerts)
62 self._alerts = dict() 72 self._alerts = {}
63 73
64 # selected entities, full jid 74 # selected entities, full jid
65 self._selected = set() 75 self._selected = set()
66 76
67 # we keep our own jid 77 # we keep our own jid
68 self.whoami = host.profiles[profile].whoami 78 self.whoami = self.host.profiles[profile].whoami
69 79
70 # options 80 # options
71 self.show_disconnected = False 81 self.show_disconnected = False
72 self.show_empty_groups = True 82 self.show_empty_groups = True
73 self.show_resources = False 83 self.show_resources = False
74 self.show_status = False 84 self.show_status = False
75 # TODO: this may lead to two successive UI refresh and needs an optimization 85
76 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups) 86 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups)
77 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts) 87 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts)
78 88
79 # 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) 89 # 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)
80 self.presenceListener = self.onPresenceUpdate 90 self.presenceListener = self.onPresenceUpdate
81 self.host.addListener('presence', self.presenceListener, [profile]) 91 self.host.addListener('presence', self.presenceListener, [self.profile])
82 self.nickListener = self.onNickUpdate 92 self.nickListener = self.onNickUpdate
83 self.host.addListener('nick', self.nickListener, [profile]) 93 self.host.addListener('nick', self.nickListener, [self.profile])
94
95 def _showEmptyGroups(self, show_str):
96 # Called only by __init__
97 # self.update is not wanted here, as it is done by
98 # handler when all profiles are ready
99 self.showEmptyGroups(C.bool(show_str))
100
101 def _showOfflineContacts(self, show_str):
102 # same comments as for _showEmptyGroups
103 self.showOfflineContacts(C.bool(show_str))
84 104
85 def __contains__(self, entity): 105 def __contains__(self, entity):
86 """Check if entity is in contact list 106 """Check if entity is in contact list
87 107
108 An entity can be in contact list even if not in roster
88 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) 109 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
89 """ 110 """
90 if entity.resource: 111 if entity.resource:
91 try: 112 try:
92 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) 113 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
93 except KeyError: 114 except KeyError:
94 return False 115 return False
95 return entity in self._cache 116 return entity in self._cache
96 117
97 @property 118 @property
98 def roster_entities(self): 119 def roster(self):
99 """Return all the bare JIDs of the roster entities. 120 """Return all the bare JIDs of the roster entities.
100 121
101 @return: set(jid.JID) 122 @return (set[jid.JID])
102 """ 123 """
103 return self._roster 124 return self._roster
104 125
105 @property 126 @property
106 def roster_entities_connected(self): 127 def roster_connected(self):
107 """Return all the bare JIDs of the roster entities that are connected. 128 """Return all the bare JIDs of the roster entities that are connected.
108 129
109 @return: set(jid.JID) 130 @return (set[jid.JID])
110 """ 131 """
111 return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None]) 132 return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None])
112 133
113 @property 134 @property
114 def roster_entities_by_group(self): 135 def roster_entities_by_group(self):
115 """Return a dictionary binding the roster groups to their entities bare 136 """Return a dictionary binding the roster groups to their entities bare JIDs.
116 JIDs. This also includes the empty group (None key). 137
117 138 This also includes the empty group (None key).
118 @return: dict{unicode: set(jid.JID)} 139 @return (dict[unicode,set(jid.JID)])
119 """ 140 """
120 return {group: self._groups[group]['jids'] for group in self._groups} 141 return {group: self._groups[group]['jids'] for group in self._groups}
121 142
122 @property 143 @property
123 def roster_groups_by_entity(self): 144 def roster_groups_by_entities(self):
124 """Return a dictionary binding the entities bare JIDs to their roster 145 """Return a dictionary binding the entities bare JIDs to their roster groups
125 groups. The empty group is filtered out. 146
126 147 @return (dict[jid.JID, set(unicode)])
127 @return: dict{jid.JID: set(unicode)}
128 """ 148 """
129 result = {} 149 result = {}
130 for group, data in self._groups.iteritems(): 150 for group, data in self._groups.iteritems():
131 if group is None:
132 continue
133 for entity in data['jids']: 151 for entity in data['jids']:
134 result.setdefault(entity, set()).add(group) 152 result.setdefault(entity, set()).add(group)
135 return result 153 return result
136 154
155 @property
156 def selected(self):
157 """Return contacts currently selected
158
159 @return (set): set of selected entities
160 """
161 return self._selected
162
163 @property
164 def items(self):
165 """Return item representation for all visible entities in cache
166
167 entities are not sorted
168 key: bare jid, value: data
169 """
170 return {jid_:cache for jid_, cache in self._cache.iteritems() if self.entityToShow(jid_)}
171
172 def getItem(self, entity):
173 """Return item representation of requested entity
174
175 @param entity(jid.JID): bare jid of entity
176 @raise (KeyError): entity is unknown
177 """
178 return self._cache[entity]
179
180 def getSpecialExtras(self, special_type=None):
181 """Return special extras with given type
182
183 If special_type is None, return all special extras.
184
185 @param special_type(unicode, None): one of special type (e.g. C.CONTACT_SPECIAL_GROUP)
186 None to return all special extras.
187 @return (set[jid.JID])
188 """
189 if special_type is None:
190 return self._special_extras
191 specials = self.getSpecials(special_type)
192 return {extra for extra in self._special_extras if extra.bare in specials}
193
137 def _gotContacts(self, contacts): 194 def _gotContacts(self, contacts):
195 """Called during filling, add contacts and notice parent that contacts are filled"""
138 for contact in contacts: 196 for contact in contacts:
139 self.host.newContactHandler(*contact, profile=self.profile) 197 self.host.newContactHandler(*contact, profile=self.profile)
198 handler._contactsFilled(self.profile)
199
200 def _fill(self):
201 """Get all contacts from backend
202
203 Contacts will be cleared before refilling them
204 """
205 self.clearContacts(keep_cache=True)
206 self.host.bridge.getContacts(self.profile, callback=self._gotContacts)
140 207
141 def fill(self): 208 def fill(self):
142 """Get all contacts from backend, and fill the widget 209 handler.fill(self.profile)
143
144 Contacts will be cleared before refilling them
145 """
146 self.clearContacts(keep_cache=True)
147
148 self.host.bridge.getContacts(self.profile, callback=self._gotContacts)
149
150 def update(self):
151 """Update the display when something changed"""
152 raise NotImplementedError
153 210
154 def getCache(self, entity, name=None): 211 def getCache(self, entity, name=None):
155 """Return a cache value for a contact 212 """Return a cache value for a contact
156 213
157 @param entity(entity.entity): entity of the contact from who we want data (resource is used if given) 214 @param entity(entity.entity): entity of the contact from who we want data (resource is used if given)
190 """Set or update value for one data in cache 247 """Set or update value for one data in cache
191 248
192 @param entity(JID): entity to update 249 @param entity(JID): entity to update
193 @param name(unicode): value to set or update 250 @param name(unicode): value to set or update
194 """ 251 """
195 self.setContact(entity, None, {name: value}) 252 self.setContact(entity, attributes={name: value})
196 253
197 def getFullJid(self, entity): 254 def getFullJid(self, entity):
198 """Get full jid from a bare jid 255 """Get full jid from a bare jid
199 256
200 @param entity(jid.JID): must be a bare jid 257 @param entity(jid.JID): must be a bare jid
201 @return (jid.JID): bare jid + main resource 258 @return (jid.JID): bare jid + main resource
202 @raise ValueError: the entity is not bare 259 @raise ValueError: the entity is not bare
203 """ 260 """
204 if entity.resource: 261 if entity.resource:
205 raise ValueError("getFullJid must be used with a bare jid") 262 raise ValueError(u"getFullJid must be used with a bare jid")
206 main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE) 263 main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE)
207 return jid.JID(u"{}/{}".format(entity, main_resource)) 264 return jid.JID(u"{}/{}".format(entity, main_resource))
208 265
209 def setGroupData(self, group, name, value): 266 def setGroupData(self, group, name, value):
210 """Register a data for a group 267 """Register a data for a group
211 268
212 @param group: a valid (existing) group name 269 @param group: a valid (existing) group name
213 @param name: name of the data (can't be "jids") 270 @param name: name of the data (can't be "jids")
214 @param value: value to set 271 @param value: value to set
215 """ 272 """
216 # FIXME: this is never used, should it be removed?
217 assert name is not 'jids' 273 assert name is not 'jids'
218 self._groups[group][name] = value 274 self._groups[group][name] = value
219 275
220 def getGroupData(self, group, name=None): 276 def getGroupData(self, group, name=None):
221 """Return value associated to group data 277 """Return value associated to group data
236 """ 292 """
237 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) 293 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
238 self.setCache(entity, C.CONTACT_SPECIAL, special_type) 294 self.setCache(entity, C.CONTACT_SPECIAL, special_type)
239 295
240 def getSpecials(self, special_type=None): 296 def getSpecials(self, special_type=None):
241 """Return all the bare JIDs of the special roster entities of the type 297 """Return all the bare JIDs of the special roster entities of with given type.
242 specified by special_type. If special_type is None, return all specials. 298
243 299 If special_type is None, return all specials.
244 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all specials. 300 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all specials.
245 @return: set(jid.JID) 301 @return: set(jid.JID)
246 """ 302 """
247 if special_type is None: 303 if special_type is None:
248 return self._specials 304 return self._specials
249 return set([entity for entity in self._specials if self.getCache(entity, C.CONTACT_SPECIAL) == special_type]) 305 return set([entity for entity in self._specials if self.getCache(entity, C.CONTACT_SPECIAL) == special_type])
250 306
251 def getSpecialExtras(self, special_type=None): 307
252 """Return all the JIDs of the special extras entities that are related 308 def disconnect(self):
253 to a special entity of the type specified by special_type. 309 # for now we just clear contacts on disconnect
254 If special_type is None, return all special extras. 310 self.clearContacts()
255
256 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all special extras.
257 @return: set(jid.JID)
258 """
259 if special_type is None:
260 return self._special_extras
261 return set([extra for extra in self._special_extras if extra.bare in self.getSpecials(special_type)])
262 311
263 def clearContacts(self, keep_cache=False): 312 def clearContacts(self, keep_cache=False):
264 """Clear all the contact list 313 """Clear all the contact list
265 314
266 @param keep_cache: if True, don't reset the cache 315 @param keep_cache: if True, don't reset the cache
267 """ 316 """
268 self.unselectAll() 317 self.select(None)
269 if not keep_cache: 318 if not keep_cache:
270 self._cache.clear() 319 self._cache.clear()
271 self._groups.clear() 320 self._groups.clear()
272 self._specials.clear() 321 self._specials.clear()
273 self._special_extras.clear() 322 self._special_extras.clear()
293 """ 342 """
294 if attributes is None: 343 if attributes is None:
295 attributes = {} 344 attributes = {}
296 345
297 entity_bare = entity.bare 346 entity_bare = entity.bare
347 update_type = C.UPDATE_MODIFY if entity_bare in self._cache else C.UPDATE_ADD
298 348
299 if in_roster: 349 if in_roster:
300 self._roster.add(entity_bare) 350 self._roster.add(entity_bare)
301 351
302 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}}) 352 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {},
353 C.CONTACT_SELECTED: set()})
303 354
304 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes 355 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes
305 356
306 # we set groups and fill self._groups accordingly 357 # we set groups and fill self._groups accordingly
307 if groups is not None: 358 if groups is not None:
322 self._specials.remove(entity_bare) 373 self._specials.remove(entity_bare)
323 else: 374 else:
324 self._specials.add(entity_bare) 375 self._specials.add(entity_bare)
325 cache[C.CONTACT_MAIN_RESOURCE] = None 376 cache[C.CONTACT_MAIN_RESOURCE] = None
326 377
327 # now the attribute we keep in cache 378 # now the attributes we keep in cache
328 for attribute, value in attributes.iteritems(): 379 for attribute, value in attributes.iteritems():
329 cache[attribute] = value 380 cache[attribute] = value
330 381
331 # we can update the display 382 # we can update the display
332 self.update() 383 self.update([entity], update_type, self.profile)
333
334 def getContacts(self):
335 """Return contacts currently selected
336
337 @return (set): set of selected entities"""
338 return self._selected
339 384
340 def entityToShow(self, entity, check_resource=False): 385 def entityToShow(self, entity, check_resource=False):
341 """Tell if the contact should be showed or hidden. 386 """Tell if the contact should be showed or hidden.
342 387
343 @param entity (jid.JID): jid of the contact 388 @param entity (jid.JID): jid of the contact
360 def anyEntityToShow(self, entities, check_resources=False): 405 def anyEntityToShow(self, entities, check_resources=False):
361 """Tell if in a list of entities, at least one should be shown 406 """Tell if in a list of entities, at least one should be shown
362 407
363 @param entities (list[jid.JID]): list of jids 408 @param entities (list[jid.JID]): list of jids
364 @param check_resources (bool): True if resources must be significant 409 @param check_resources (bool): True if resources must be significant
365 @return: bool 410 @return (bool): True if a least one entity need to be shown
366 """ 411 """
412 # FIXME: looks inefficient, really needed?
367 for entity in entities: 413 for entity in entities:
368 if self.entityToShow(entity, check_resources): 414 if self.entityToShow(entity, check_resources):
369 return True 415 return True
370 return False 416 return False
371 417
376 @param group(unicode): group to check 422 @param group(unicode): group to check
377 @return (bool): True if the entity is in the group 423 @return (bool): True if the entity is in the group
378 """ 424 """
379 return entity in self.getGroupData(group, "jids") 425 return entity in self.getGroupData(group, "jids")
380 426
381 def removeContact(self, entity, in_roster=False): 427 def removeContact(self, entity):
382 """remove a contact from the list 428 """remove a contact from the list
383 429
384 @param entity(jid.JID): jid of the entity to remove (bare jid is used) 430 @param entity(jid.JID): jid of the entity to remove (bare jid is used)
385 @param in_roster (bool): True if contact is from roster
386 """ 431 """
387 entity_bare = entity.bare 432 entity_bare = entity.bare
388 try: 433 try:
389 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) 434 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
390 except KeyError: 435 except KeyError:
391 log.warning(_(u"Trying to delete an unknow entity [{}]").format(entity)) 436 log.error(_(u"Trying to delete an unknow entity [{}]").format(entity))
392 if in_roster: 437 try:
393 self._roster.remove(entity_bare) 438 self._roster.remove(entity_bare)
439 except KeyError:
440 pass
394 del self._cache[entity_bare] 441 del self._cache[entity_bare]
395 for group in groups: 442 for group in groups:
396 self._groups[group]['jids'].remove(entity_bare) 443 self._groups[group]['jids'].remove(entity_bare)
397 if not self._groups[group]['jids']: 444 if not self._groups[group]['jids']:
398 self._groups.pop(group) 445 self._groups.pop(group) # FIXME: we use pop because of pyjamas: http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en
399 for iterable in (self._selected, self._alerts, self._specials, self._special_extras): 446 for iterable in (self._selected, self._alerts, self._specials, self._special_extras):
400 to_remove = set() 447 to_remove = set()
401 for set_entity in iterable: 448 for set_entity in iterable:
402 if set_entity.bare == entity.bare: 449 if set_entity.bare == entity.bare:
403 to_remove.add(set_entity) 450 to_remove.add(set_entity)
404 if isinstance(iterable, set): 451 if isinstance(iterable, set):
405 iterable.difference_update(to_remove) 452 iterable.difference_update(to_remove)
406 else: # XXX: self._alerts is a dict 453 else: # XXX: self._alerts is a dict
407 for item in to_remove: 454 for item in to_remove:
408 del iterable[item] 455 del iterable[item]
409 self.update() 456 self.update([entity], C.UPDATE_DELETE, self.profile)
410 457
411 def onPresenceUpdate(self, entity, show, priority, statuses, profile): 458 def onPresenceUpdate(self, entity, show, priority, statuses, profile):
412 """Update entity's presence status 459 """Update entity's presence status
413 460
414 @param entity(jid.JID): entity updated 461 @param entity(jid.JID): entity updated
438 resource_data[C.PRESENCE_STATUSES] = statuses 485 resource_data[C.PRESENCE_STATUSES] = statuses
439 486
440 if entity.bare not in self._specials: 487 if entity.bare not in self._specials:
441 priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY]) 488 priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
442 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource 489 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
443 self.update() 490 self.update([entity], C.UPDATE_MODIFY, self.profile)
444 491
445 def onNickUpdate(self, entity, new_nick, profile): 492 def onNickUpdate(self, entity, new_nick, profile):
446 """Update entity's nick 493 """Update entity's nick
447 494
448 @param entity(jid.JID): entity updated 495 @param entity(jid.JID): entity updated
449 @param new_nick(unicode): new nick of the entity 496 @param new_nick(unicode): new nick of the entity
450 @param profile: %(doc_profile)s 497 @param profile: %(doc_profile)s
451 """ 498 """
452 raise NotImplementedError # Must be implemented by frontends 499 assert profile == self.profile
453 500 self.setCache(entity, 'nick', new_nick)
454 def unselectAll(self): 501 self.update([entity], C.UPDATE_MODIFY, profile)
455 """Unselect all contacts""" 502
456 self._selected.clear() 503 def unselect(self, entity):
457 self.update() 504 """Unselect an entity
505
506 @param entity(jid.JID): entity to unselect
507 """
508 try:
509 cache = self._cache[entity.bare]
510 except:
511 log.error(u"Try to unselect an entity not in cache")
512 else:
513 try:
514 cache[C.CONTACT_SELECTED].remove(entity.resource)
515 except KeyError:
516 log.error(u"Try to unselect a not selected entity")
517 else:
518 self._selected.remove(entity)
519 self.update([entity], C.UPDATE_SELECTION)
458 520
459 def select(self, entity): 521 def select(self, entity):
460 """Select an entity 522 """Select an entity
461 523
462 @param entity(jid.JID): entity to select (resource is significant) 524 @param entity(jid.JID, None): entity to select (resource is significant)
463 """ 525 None to unselect all entities
464 log.debug(u"select %s" % entity) 526 """
465 self._selected.add(entity) 527 if entity is None:
466 self.update() 528 self._selected.clear()
467 529 for cache in self._cache.itervalues():
468 def getAlerts(self, entity, use_bare_jid=False): 530 cache[C.CONTACT_SELECTED].clear()
469 """Return the number of alerts set to this entity. 531 self.update(type_=C.UPDATE_SELECTION, profile=self.profile)
470 532 else:
533 log.debug(u"select %s" % entity)
534 try:
535 cache = self._cache[entity.bare]
536 except:
537 log.error(u"Try to select an entity not in cache")
538 else:
539 cache[C.CONTACT_SELECTED].add(entity.resource)
540 self._selected.add(entity)
541 self.update([entity], C.UPDATE_SELECTION, profile=self.profile)
542
543 def getAlerts(self, entity, use_bare_jid=False, filter_=None):
544 """Return alerts set to this entity.
545
471 @param entity (jid.JID): entity 546 @param entity (jid.JID): entity
472 @param use_bare_jid (bool): if True, cumulate the alerts of all the resources sharing the same bare JID 547 @param use_bare_jid (bool): if True, cumulate the alerts of all the resources sharing the same bare JID
473 @return int 548 @param filter_(iterable, None): alert to take into account,
549 None to count all of them
550 @return (list[unicode,None]): list of C.ALERT_* or None for undefined ones
474 """ 551 """
475 if not use_bare_jid: 552 if not use_bare_jid:
476 return self._alerts.get(entity, 0) 553 alerts = self._alerts.get(entity, [])
477 554 else:
478 alerts = {} 555 alerts = []
479 for contact in self._alerts: 556 for contact, contact_alerts in self._alerts:
480 alerts.setdefault(contact.bare, 0) 557 if contact.bare == entity:
481 alerts[contact.bare] += self._alerts[contact] 558 alerts.extend(contact_alerts)
482 return alerts.get(entity.bare, 0) 559 if filter_ is None:
483 560 return alerts
484 def addAlert(self, entity): 561 else:
485 """Increase the alerts counter for this entity (usually for a waiting message) 562 return [alert for alert in alerts if alert in filter_]
486 563
487 @param entity(jid.JID): entity which must displayed in alert mode (resource is significant) 564 def addAlert(self, entity, type_=None):
488 """ 565 """Add an alert for this enity
489 self._alerts.setdefault(entity, 0) 566
490 self._alerts[entity] += 1 567 @param entity(jid.JID): entity who received an alert (resource is significant)
491 self.update() 568 @param type_(unicode, None): type of alert (C.ALERT_*)
492 self.host.updateAlertsCounter() 569 None for generic alert
570 """
571 self._alerts.setdefault(entity, [])
572 self._alerts[entity].append(type_)
573 self.update([entity], C.UPDATE_MODIFY, self.profile)
574 self.host.updateAlertsCounter() # FIXME: ?
493 575
494 def removeAlerts(self, entity, use_bare_jid=True): 576 def removeAlerts(self, entity, use_bare_jid=True):
495 """Eventually remove an alert on the entity (usually for a waiting message). 577 """Eventually remove an alert on the entity (usually for a waiting message).
496 578
497 @param entity(jid.JID): entity (resource is significant) 579 @param entity(jid.JID): entity (resource is significant)
504 to_remove.add(alert_entity) 586 to_remove.add(alert_entity)
505 if not to_remove: 587 if not to_remove:
506 return # nothing changed 588 return # nothing changed
507 for entity in to_remove: 589 for entity in to_remove:
508 del self._alerts[entity] 590 del self._alerts[entity]
591 self.update([to_remove], C.UPDATE_MODIFY, self.profile)
592 self.host.updateAlertsCounter() # FIXME: ?
509 else: 593 else:
510 try: 594 try:
511 del self._alerts[entity] 595 del self._alerts[entity]
512 except KeyError: 596 except KeyError:
513 return # nothing changed 597 return # nothing changed
514 self.update() 598 else:
515 self.host.updateAlertsCounter() 599 self.update([entity], C.UPDATE_MODIFY, self.profile)
516 600 self.host.updateAlertsCounter() # FIXME: ?
517 def _showOfflineContacts(self, show_str):
518 self.showOfflineContacts(C.bool(show_str))
519 601
520 def showOfflineContacts(self, show): 602 def showOfflineContacts(self, show):
521 """Tell if offline contacts should shown 603 """Tell if offline contacts should shown
522 604
523 @param show(bool): True if offline contacts should be shown 605 @param show(bool): True if offline contacts should be shown
524 """ 606 """
525 assert isinstance(show, bool) 607 assert isinstance(show, bool)
526 if self.show_disconnected == show: 608 if self.show_disconnected == show:
527 return 609 return
528 self.show_disconnected = show 610 self.show_disconnected = show
529 self.update() 611 self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile)
530
531 def _showEmptyGroups(self, show_str):
532 self.showEmptyGroups(C.bool(show_str))
533 612
534 def showEmptyGroups(self, show): 613 def showEmptyGroups(self, show):
535 assert isinstance(show, bool) 614 assert isinstance(show, bool)
536 if self.show_empty_groups == show: 615 if self.show_empty_groups == show:
537 return 616 return
538 self.show_empty_groups = show 617 self.show_empty_groups = show
539 self.update() 618 self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile)
540 619
541 def showResources(self, show): 620 def showResources(self, show):
542 assert isinstance(show, bool) 621 assert isinstance(show, bool)
543 if self.show_resources == show: 622 if self.show_resources == show:
544 return 623 return
545 self.show_resources = show 624 self.show_resources = show
625 self.update(profile=self.profile)
626
627 def plug(self):
628 handler.addProfile(self.profile)
629
630 def unplug(self):
631 handler.removeProfile(self.profile)
632
633 def update(self, entities=None, type_=None, profile=None):
634 handler.update(entities, type_, profile)
635
636
637 class QuickContactListHandler(object):
638
639 def __init__(self, host):
640 super(QuickContactListHandler, self).__init__()
641 self.host = host
642 global handler
643 if handler is not None:
644 raise exceptions.InternalError(u"QuickContactListHandler must be instanciated only once")
645 handler = self
646 self._clist = {} # key: profile, value: ProfileContactList
647 self._widgets = set()
648
649 def __getitem__(self, profile):
650 """Return ProfileContactList instance for the requested profile"""
651 return self._clist[profile]
652
653 def __contains__(self, entity):
654 """Check if entity is in contact list
655
656 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
657 """
658 for contact_list in self._clist.itervalues():
659 if entity in contact_list:
660 return True
661 return False
662
663 @property
664 def roster_entities(self):
665 """Return all the bare JIDs of the roster entities.
666
667 @return (set[jid.JID])
668 """
669 entities = set()
670 for contact_list in self._clist.itervalues():
671 entities.update(contact_list.roster_entities)
672 return entities
673
674 @property
675 def roster_entities_connected(self):
676 """Return all the bare JIDs of the roster entities that are connected.
677
678 @return (set[jid.JID])
679 """
680 entities = set()
681 for contact_list in self._clist.itervalues():
682 entities.update(contact_list.roster_entities_connected)
683 return entities
684
685 @property
686 def roster_entities_by_group(self):
687 """Return a dictionary binding the roster groups to their entities bare
688 JIDs. This also includes the empty group (None key).
689
690 @return (dict[unicode,set(jid.JID)])
691 """
692 groups = {}
693 for contact_list in self._clist.itervalues():
694 groups.update(contact_list.roster_entities_by_group)
695 return groups
696
697 @property
698 def roster_groups_by_entities(self):
699 """Return a dictionary binding the entities bare JIDs to their roster
700 groups.
701
702 @return (dict[jid.JID, set(unicode)])
703 """
704 entities = {}
705 for contact_list in self._clist.itervalues():
706 entities.update(contact_list.roster_groups_by_entities)
707 return entities
708
709 @property
710 def selected(self):
711 """Return contacts currently selected
712
713 @return (set): set of selected entities
714 """
715 entities = set()
716 for contact_list in self._clist.itervalues():
717 entities.update(contact_list.selected)
718 return entities
719
720 @property
721 def items(self):
722 """Return item representation for all visible entities in cache
723
724 items are unordered
725 key: bare jid, value: data
726 """
727 items = {}
728 for profile, contact_list in self._clist.iteritems():
729 for bare_jid, cache in contact_list.items.iteritems():
730 data = cache.copy()
731 items[bare_jid] = data
732 data[C.CONTACT_PROFILE] = profile
733 items.update(contact_list.items)
734 return items
735
736 @property
737 def items_sorted(self):
738 """Return item representation for all visible entities in cache
739
740 items are ordered using self.items_sort
741 key: bare jid, value: data
742 """
743 return self.items_sort(self.items)
744
745 def items_sort(self, items):
746 """sort items
747
748 @param items(dict): items to sort (we be emptied !)
749 @return (OrderedDict): sorted items
750 """
751 ordered_items = OrderedDict()
752 bare_jids = sorted(items.keys())
753 for jid_ in bare_jids:
754 ordered_items[jid_] = items.pop(jid_)
755 return ordered_items
756
757 def register(self, widget):
758 """Register a QuickContactList widget
759
760 This method should only be used in QuickContactList
761 """
762 self._widgets.add(widget)
763
764 def unregister(self, widget):
765 """Unregister a QuickContactList widget
766
767 This method should only be used in QuickContactList
768 """
769 self._widgets.remove(widget)
770
771 def addProfiles(self, profiles):
772 """Add a contact list for plugged profiles
773
774 @param profile(iterable[unicode]): plugged profiles
775 """
776 for profile in profiles:
777 if profile not in self._clist:
778 self._clist[profile] = ProfileContactList(profile)
779 return [self._clist[profile] for profile in profiles]
780
781 def addProfile(self, profile):
782 return self.addProfiles([profile])[0]
783
784 def removeProfiles(self, profiles):
785 """Remove given unplugged profiles from contact list
786
787 @param profile(iterable[unicode]): unplugged profiles
788 """
789 for profile in profiles:
790 del self._clist[profile]
791
792 def removeProfile(self, profile):
793 self.removeProfiles([profile])
794
795 def getSpecialExtras(self, special_type=None):
796 """Return special extras with given type
797
798 If special_type is None, return all special extras.
799
800 @param special_type(unicode, None): one of special type (e.g. C.CONTACT_SPECIAL_GROUP)
801 None to return all special extras.
802 @return (set[jid.JID])
803 """
804 entities = set()
805 for contact_list in self._clist.itervalues():
806 entities.update(contact_list.getSpecialExtras(special_type))
807 return entities
808
809 def _contactsFilled(self, profile):
810 self._to_fill.remove(profile)
811 if not self._to_fill:
812 del self._to_fill
813 self.update()
814
815 def fill(self, profile=None):
816 """Get all contacts from backend, and fill the widget
817
818 Contacts will be cleared before refilling them
819 @param profile(unicode, None): profile to fill
820 None to fill all profiles
821 """
822 try:
823 to_fill = self._to_fill
824 except AttributeError:
825 to_fill = self._to_fill = set()
826
827 # if check if profiles have already been filled
828 # to void filling them several times
829 filled = to_fill.copy()
830
831 if profile is not None:
832 assert profile in self._clist
833 to_fill.add(profile)
834 else:
835 to_fill.update(self._clist.items())
836
837 remaining = to_fill.difference(filled)
838 if remaining != to_fill:
839 log.debug(u"Not re-filling already filled contact list(s) for {}".format(u', '.join(to_fill.intersection(filled))))
840 for profile in remaining:
841 self._clist[profile]._fill()
842
843 def clearContacts(self, keep_cache=False):
844 """Clear all the contact list
845
846 @param keep_cache: if True, don't reset the cache
847 """
848 for contact_list in self._clist.itervalues():
849 contact_list.clearContacts(keep_cache)
546 self.update() 850 self.update()
851
852 def select(self, entity):
853 for contact_list in self._clist.itervalues():
854 contact_list.select(entity)
855
856 def unselect(self, entity):
857 for contact_list in self._clist.itervalues():
858 contact_list.select(entity)
859
860 def update(self, entities=None, type_=None, profile=None):
861 for widget in self._widgets:
862 widget.update(entities, type_, profile)
863
864
865 class QuickContactList(QuickWidget):
866 """This class manage the visual representation of contacts"""
867 SINGLE=False
868 PROFILES_MULTIPLE=True
869 PROFILES_ALLOW_NONE=True # Can be linked to no profile (e.g. at the early forntend start)
870
871 def __init__(self, host, profiles):
872 super(QuickContactList, self).__init__(host, None, profiles)
873 handler.register(self)
874
875 # options
876 # for next values, None means use indivual value per profile
877 # True or False mean override these values for all profiles
878 self.show_disconnected = None # TODO
879 self.show_empty_groups = None # TODO
880 self.show_resources = None # TODO
881 self.show_status = None # TODO
882
883 @property
884 def items(self):
885 return handler.items
886
887 @property
888 def items_sorted(self):
889 return handler.items
890
891 def update(self, entities=None, type_=None, profile=None):
892 """Update the display when something changed
893
894 @param entities(iterable[jid.JID], None): updated entities,
895 None to update the whole contact list
896 @param type_(unicode, None): update type, may be:
897 - C.UPDATE_DELETE: entity deleted
898 - C.UPDATE_MODIFY: entity updated
899 - C.UPDATE_ADD: entity added
900 - C.UPDATE_SELECTION: selection modified
901 or None for undefined update
902 @param profile(unicode, None): profile concerned with the update
903 None if unknown
904 """
905 raise NotImplementedError
547 906
548 def onDelete(self): 907 def onDelete(self):
549 QuickWidget.onDelete(self) 908 QuickWidget.onDelete(self)
550 self.host.removeListener('presence', self.presenceListener) 909 handler.unregister(self)