comparison frontends/src/quick_frontend/quick_contact_list.py @ 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents 3abc6563a0d2
children faa1129559b8
comparison
equal deleted inserted replaced
1264:60dfa2f5d61f 1265:e3a9ea76de35
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 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 from sat.core.log import getLogger 21 from sat.core.log import getLogger
22 log = getLogger(__name__) 22 log = getLogger(__name__)
23 23 from sat_frontends.quick_frontend.quick_widgets import QuickWidget
24 24 from sat_frontends.quick_frontend.constants import Const as C
25 class QuickContactList(object): 25 from sat_frontends.tools import jid
26
27
28 class QuickContactList(QuickWidget):
26 """This class manage the visual representation of contacts""" 29 """This class manage the visual representation of contacts"""
27 30
28 def __init__(self): 31 def __init__(self, host, profile):
29 log.debug(_("Contact List init")) 32 log.debug(_("Contact List init"))
33 super(QuickContactList, self).__init__(host, profile, profile)
34 # bare jids as keys, resources are used in data
30 self._cache = {} 35 self._cache = {}
31 self.specials={} 36
32 37 # special entities (groupchat, gateways, etc), bare jids
33 def update_jid(self, jid): 38 self._specials = set()
34 """Update the jid in the list when something changed""" 39 # extras are specials with full jids (e.g.: private MUC conversation)
40 self._special_extras = set()
41
42 # group data contain jids in groups and misc frontend data
43 self._groups = {} # groups to group data map
44
45 # contacts in roster (bare jids)
46 self._roster = set()
47
48 # entities with an alert (usually a waiting message), full jid
49 self._alerts = set()
50
51 # selected entities, full jid
52 self._selected = set()
53
54 # options
55 self.show_disconnected = False
56 self.show_empty_groups = True
57 self.show_resources = False
58 self.show_status = False
59 # TODO: this may lead to two successive UI refresh and needs an optimization
60 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self.showEmptyGroups)
61 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self.showOfflineContacts)
62
63 def __contains__(self, entity):
64 """Check if entity is in contact list
65
66 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
67 """
68 if entity.resource:
69 try:
70 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
71 except KeyError:
72 return False
73 return entity in self._cache
74
75 def fill(self):
76 """Get all contacts from backend, and fill the widget"""
77 def gotContacts(contacts):
78 for contact in contacts:
79 self.host.newContactHandler(*contact, profile=self.profile)
80
81 presences = self.host.bridge.getPresenceStatuses(self.profile)
82 for contact in presences:
83 for res in presences[contact]:
84 jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
85 show = presences[contact][res][0]
86 priority = presences[contact][res][1]
87 statuses = presences[contact][res][2]
88 self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
89 data = self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile)
90 for key in ('avatar', 'nick'):
91 if key in data:
92 self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
93 self.host.bridge.getContacts(self.profile, callback=gotContacts)
94
95 def update(self):
96 """Update the display when something changed"""
35 raise NotImplementedError 97 raise NotImplementedError
36 98
37 def getCache(self, jid, name): 99 def getCache(self, entity, name=None):
100 """Return a cache value for a contact
101
102 @param entity(entity.entity): entity of the contact from who we want data (resource is used if given)
103 if a resource specific information is requested:
104 - if no resource is given (bare jid), the main resource is used, according to priority
105 - if resource is given, it is used
106 @param name(unicode): name the data to get, or None to get everything
107 """
108 cache = self._cache[entity.bare]
109 if name is None:
110 return cache
38 try: 111 try:
39 jid_cache = self._cache[jid.bare] 112 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
40 if name == 'status': #XXX: we get the first status for 'status' key 113 # these data are related to the resource
41 return jid_cache['statuses'].get('default','') 114 if not entity.resource:
42 return jid_cache[name] 115 main_resource = cache[C.CONTACT_MAIN_RESOURCE]
43 except (KeyError, IndexError): 116 cache = cache[C.CONTACT_RESOURCES][main_resource]
117 else:
118 cache = cache[C.CONTACT_RESOURCES][entity.resource]
119
120 if name == 'status': #XXX: we get the first status for 'status' key
121 # TODO: manage main language for statuses
122 return cache[C.PRESENCE_STATUSES].get('default','')
123
124 return cache[name]
125 except KeyError:
44 return None 126 return None
45 127
46 def setCache(self, jid, name, value): 128 def setCache(self, entity, name, value):
47 jid_cache = self._cache.setdefault(jid.bare, {}) 129 """Set or update value for one data in cache
48 jid_cache[name] = value 130
49 131 @param entity(JID): entity to update
50 def __contains__(self, jid): 132 @param name(unicode): value to set or update
51 raise NotImplementedError 133 """
134 self.setContact(entity, None, {name: value})
135
136 def setGroupData(self, group, name, value):
137 """Register a data for a group
138
139 @param group: a valid (existing) group name
140 @param name: name of the data (can't be "jids")
141 @param value: value to set
142 """
143 assert name is not 'jids'
144 self._groups[group][name] = value
145
146 def getGroupData(self, group, name=None):
147 """Return value associated to group data
148
149 @param group: a valid (existing) group name
150 @param name: name of the data or None to get the whole dict
151 @return: registered value
152 """
153 if name is None:
154 return self._groups[group]
155 return self._groups[group][name]
156
157 def setSpecial(self, entity, special_type):
158 """Set special flag on an entity
159
160 @param entity(jid.JID): jid of the special entity
161 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag
162 """
163 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
164 self.setCache(entity, C.CONTACT_SPECIAL, special_type)
52 165
53 def clearContacts(self): 166 def clearContacts(self):
54 """Clear all the contact list""" 167 """Clear all the contact list"""
55 self.specials.clear() 168 self.unselectAll()
56 169 self._cache.clear()
57 def replace(self, jid, groups=None, attributes=None): 170 self._groups.clear()
171 self._specials.clear()
172 self.update()
173
174 def setContact(self, entity, groups=None, attributes=None, in_roster=False):
58 """Add a contact to the list if doesn't exist, else update it. 175 """Add a contact to the list if doesn't exist, else update it.
59 176
60 This method can be called with groups=None for the purpose of updating 177 This method can be called with groups=None for the purpose of updating
61 the contact's attributes (e.g. nickname). In that case, the groups 178 the contact's attributes (e.g. nickname). In that case, the groups
62 attribute must not be set to the default group but ignored. If not, 179 attribute must not be set to the default group but ignored. If not,
63 you may move your contact from its actual group(s) to the default one. 180 you may move your contact from its actual group(s) to the default one.
64 181
65 None value for 'groups' has a different meaning than [None] which is for the default group. 182 None value for 'groups' has a different meaning than [None] which is for the default group.
66 183
67 @param jid (JID) 184 @param entity (jid.JID): entity to add or replace
68 @param groups (list): list of groups or None to ignore the groups membership. 185 @param groups (list): list of groups or None to ignore the groups membership.
69 @param attributes (dict) 186 @param attributes (dict): attibutes of the added jid or to update
70 """ 187 @param in_roster (bool): True if contact is from roster
71 if attributes and 'name' in attributes: 188 """
72 self.setCache(jid, 'name', attributes['name']) 189 if attributes is None:
73 190 attributes = {}
74 def remove(self, jid): 191
75 """remove a contact from the list""" 192 entity_bare = entity.bare
193
194 if in_roster:
195 self._roster.add(entity_bare)
196
197 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}})
198
199 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes
200
201 # we set groups and fill self._groups accordingly
202 if groups is not None:
203 if not groups:
204 groups = [None] # [None] is the default group
205 cache[C.CONTACT_GROUPS] = groups
206 for group in groups:
207 self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare)
208
209 # special entities management
210 if C.CONTACT_SPECIAL in attributes:
211 if attributes[C.CONTACT_SPECIAL] is None:
212 del attributes[C.CONTACT_SPECIAL]
213 self._specials.remove(entity_bare)
214 else:
215 self._specials.add(entity_bare)
216
217 # now the attribute we keep in cache
218 for attribute, value in attributes.iteritems():
219 cache[attribute] = value
220
221 # we can update the display
222 self.update()
223
224 def getContacts(self):
225 """Return contacts currently selected
226
227 @return (set): set of selected entities"""
228 return self._selected
229
230 def entityToShow(self, entity, check_resource=False):
231 """Tell if the contact should be showed or hidden.
232
233 @param contact (jid.JID): jid of the contact
234 @param check_resource (bool): True if resource must be significant
235 @return: True if that contact should be showed in the list
236 """
237 show = self.getCache(entity, C.PRESENCE_SHOW)
238
239 if check_resource:
240 alerts = self._alerts
241 selected = self._selected
242 else:
243 alerts = {alert.bare for alert in self._alerts}
244 selected = {selected.bare for selected in self._selected}
245 return ((show is not None and show != "unavailable")
246 or self.show_disconnected
247 or entity in alerts
248 or entity in selected)
249
250 def anyEntityToShow(self, entities, check_resources=False):
251 """Tell if in a list of entities, at least one should be shown
252
253 @param entities (list[jid.JID]): list of jids
254 @param check_resources (bool): True if resources must be significant
255 @return: bool
256 """
257 for entity in entities:
258 if self.entityToShow(entity, check_resources):
259 return True
260 return False
261
262 def remove(self, entity):
263 """remove a contact from the list
264
265 @param entity(jid.JID): jid of the entity to remove (bare jid is used)
266 """
267 entity_bare = entity.bare
76 try: 268 try:
77 del self.specials[jid.bare] 269 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
78 except KeyError: 270 except KeyError:
79 pass 271 log.warning(_("Trying to delete an unknow entity [{}]").format(entity))
272 del self._cache[entity_bare]
273 for group in groups:
274 self._groups[group]['jids'].remove(entity_bare)
275 for set_ in (self._selected, self._alerts, self._specials, self._special_extras):
276 to_remove = set()
277 for set_entity in set_:
278 if set_entity.bare == entity.bare:
279 to_remove.add(set_entity)
280 set_.difference_update(to_remove)
281 self.update()
80 282
81 def add(self, jid, param_groups=None): 283 def add(self, jid, param_groups=None):
82 """add a contact to the list""" 284 """add a contact to the list"""
83 raise NotImplementedError 285 raise NotImplementedError
84 286
85 def getSpecial(self, jid): 287 def updatePresence(self, entity, show, priority, statuses):
86 """Return special type of jid, or None if it's not special"""
87 return self.specials.get(jid.bare)
88
89 def setSpecial(self, jid, _type, show=False):
90 """Set entity as a special
91 @param jid: jid of the entity
92 @param _type: special type (e.g.: "MUC")
93 @param show: True to display the dialog to chat with this entity
94 """
95 self.specials[jid.bare] = _type
96
97 def updatePresence(self, jid, show, priority, statuses):
98 """Update entity's presence status 288 """Update entity's presence status
99 @param jid: entity to update's jid 289
290 @param entity(jid.JID): entity to update's entity
100 @param show: availability 291 @param show: availability
101 @parap priority: resource's priority 292 @parap priority: resource's priority
102 @param statuses: dict of statuses""" 293 @param statuses: dict of statuses
103 self.setCache(jid, 'show', show) 294 """
104 self.setCache(jid, 'prority', priority) 295 cache = self.getCache(entity)
105 self.setCache(jid, 'statuses', statuses) 296 if show == C.PRESENCE_UNAVAILABLE:
106 self.update_jid(jid) 297 if not entity.resource:
298 cache[C.CONTACT_RESOURCES].clear()
299 cache[C.CONTACT_MAIN_RESOURCE]= None
300 else:
301 del cache[C.CONTACT_RESOURCES][entity.resource]
302 if not cache[C.CONTACT_RESOURCES]:
303 cache[C.CONTACT_MAIN_RESOURCE] = None
304 else:
305 assert entity.resource
306 resources_data = cache[C.CONTACT_RESOURCES]
307 resource_data = resources_data.setdefault(entity.resource, {})
308 resource_data[C.PRESENCE_SHOW] = show
309 resource_data[C.PRESENCE_PRIORITY] = int(priority)
310 resource_data[C.PRESENCE_STATUSES] = statuses
311
312 priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
313 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
314 self.update()
315
316 def unselectAll(self):
317 """Unselect all contacts"""
318 self._selected.clear()
319 self.update()
320
321 def select(self, entity):
322 """Select an entity
323
324 @param entity(jid.JID): entity to select (resource is significant)
325 """
326 log.debug("select %s" % entity)
327 self._selected.add(entity)
328 self.update()
329
330 def setAlert(self, entity):
331 """Set an alert on the entity (usually for a waiting message)
332
333 @param entity(jid.JID): entity which must displayed in alert mode (resource is significant)
334 """
335 self._alerts.add(entity)
336 self.update()
107 337
108 def showOfflineContacts(self, show): 338 def showOfflineContacts(self, show):
109 pass 339 show = C.bool(show)
340 if self.show_disconnected == show:
341 return
342 self.show_disconnected = show
343 self.update()
110 344
111 def showEmptyGroups(self, show): 345 def showEmptyGroups(self, show):
112 pass 346 show = C.bool(show)
347 if self.show_empty_groups == show:
348 return
349 self.show_empty_groups = show
350 self.update()
351
352 def showResources(self, show):
353 show = C.bool(show)
354 if self.show_resources == show:
355 return
356 self.show_resources = show
357 self.update()