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