comparison frontends/src/primitivus/contact_list.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents 1679ac59f701
children 017270e6eea4
comparison
equal deleted inserted replaced
1295:1e3b1f9ad6e2 1367:f71a0fc26886
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 import urwid 21 import urwid
22 from urwid_satext import sat_widgets 22 from urwid_satext import sat_widgets
23 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList 23 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
24 from sat_frontends.quick_frontend.quick_utils import unescapePrivate
25 from sat_frontends.tools.jid import JID
26 from sat_frontends.primitivus.status import StatusBar 24 from sat_frontends.primitivus.status import StatusBar
27 from sat_frontends.primitivus.constants import Const as C 25 from sat_frontends.primitivus.constants import Const as C
28 from sat_frontends.primitivus.keys import action_key_map as a_key 26 from sat_frontends.primitivus.keys import action_key_map as a_key
27 from sat_frontends.primitivus.widget import PrimitivusWidget
28 from sat_frontends.tools import jid
29 from sat.core import log as logging 29 from sat.core import log as logging
30 log = logging.getLogger(__name__) 30 log = logging.getLogger(__name__)
31 31
32 32
33 class ContactList(urwid.WidgetWrap, QuickContactList): 33 class ContactList(PrimitivusWidget, QuickContactList):
34 signals = ['click','change'] 34 signals = ['click','change']
35 35
36 def __init__(self, host, on_click=None, on_change=None, user_data=None): 36 def __init__(self, host, on_click=None, on_change=None, user_data=None, profile=None):
37 QuickContactList.__init__(self) 37 QuickContactList.__init__(self, host, profile)
38 self.host = host
39 self.selected = None
40 self.groups={}
41 self.alert_jid=set()
42 self.show_status = False
43 self.show_disconnected = False
44 self.show_empty_groups = True
45 # TODO: this may lead to two successive UI refresh and needs an optimization
46 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=host.profile, callback=self.showEmptyGroups)
47 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=host.profile, callback=self.showOfflineContacts)
48 38
49 #we now build the widget 39 #we now build the widget
50 self.host.status_bar = StatusBar(host) 40 self.status_bar = StatusBar(host)
51 self.frame = sat_widgets.FocusFrame(self.__buildList(), None, self.host.status_bar) 41 self.frame = sat_widgets.FocusFrame(self._buildList(), None, self.status_bar)
52 self.main_widget = sat_widgets.LabelLine(self.frame, sat_widgets.SurroundedText(_("Contacts"))) 42 PrimitivusWidget.__init__(self, self.frame, _(u'Contacts'))
53 urwid.WidgetWrap.__init__(self, self.main_widget)
54 if on_click: 43 if on_click:
55 urwid.connect_signal(self, 'click', on_click, user_data) 44 urwid.connect_signal(self, 'click', on_click, user_data)
56 if on_change: 45 if on_change:
57 urwid.connect_signal(self, 'change', on_change, user_data) 46 urwid.connect_signal(self, 'change', on_change, user_data)
58 47
59 def update(self): 48 def update(self):
60 """Update display, keep focus""" 49 """Update display, keep focus"""
61 widget, position = self.frame.body.get_focus() 50 widget, position = self.frame.body.get_focus()
62 self.frame.body = self.__buildList() 51 self.frame.body = self._buildList()
63 if position: 52 if position:
64 try: 53 try:
65 self.frame.body.focus_position = position 54 self.frame.body.focus_position = position
66 except IndexError: 55 except IndexError:
67 pass 56 pass
68 self.host.redraw() 57 self._invalidate()
69 58 self.host.redraw() # FIXME: check if can be avoided
70 def update_jid(self, jid):
71 self.update()
72 59
73 def keypress(self, size, key): 60 def keypress(self, size, key):
74 # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent, 61 # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
75 # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element 62 # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element
76 if key in sat_widgets.FOCUS_KEYS: 63 if key in sat_widgets.FOCUS_KEYS:
79 return key 66 return key
80 if key == a_key['STATUS_HIDE']: #user wants to (un)hide contacts' statuses 67 if key == a_key['STATUS_HIDE']: #user wants to (un)hide contacts' statuses
81 self.show_status = not self.show_status 68 self.show_status = not self.show_status
82 self.update() 69 self.update()
83 elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts 70 elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts
84 self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.host.profile) 71 self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.profile)
72 elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources
73 self.showResources(not self.show_resources)
74 self.update()
85 return super(ContactList, self).keypress(size, key) 75 return super(ContactList, self).keypress(size, key)
86 76
87 def __contains__(self, jid): 77 # modify the contact list
88 for group in self.groups:
89 if jid.bare in self.groups[group][1]:
90 return True
91 return False
92 78
93 def setFocus(self, text, select=False): 79 def setFocus(self, text, select=False):
94 """give focus to the first element that matches the given text. You can also 80 """give focus to the first element that matches the given text. You can also
95 pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode). 81 pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode).
82
96 @param text: contact group name, contact or muc userhost, muc private dialog jid 83 @param text: contact group name, contact or muc userhost, muc private dialog jid
97 @param select: if True, the element is also clicked 84 @param select: if True, the element is also clicked
98 """ 85 """
99 idx = 0 86 idx = 0
100 for widget in self.frame.body.body: 87 for widget in self.frame.body.body:
101 try: 88 try:
102 if isinstance(widget, sat_widgets.ClickableText): 89 if isinstance(widget, sat_widgets.ClickableText):
103 # contact group 90 # contact group
104 value = widget.getValue() 91 value = widget.getValue()
105 elif isinstance(widget, sat_widgets.SelectableText): 92 elif isinstance(widget, sat_widgets.SelectableText):
106 if widget.data.startswith(C.PRIVATE_PREFIX): 93 # contact or muc
107 # muc private dialog 94 value = widget.data
108 value = widget.getValue()
109 else:
110 # contact or muc
111 value = widget.data
112 else: 95 else:
113 # Divider instance 96 # Divider instance
114 continue 97 continue
115 # there's sometimes a leading space 98 # there's sometimes a leading space
116 if text.strip() == value.strip(): 99 if text.strip() == value.strip():
117 self.frame.body.focus_position = idx 100 self.frame.body.focus_position = idx
118 if select: 101 if select:
119 self.__contactClicked(widget, True) 102 self._contactClicked(False, widget, True)
120 return 103 return
121 except AttributeError: 104 except AttributeError:
122 pass 105 pass
123 idx += 1 106 idx += 1
124 107
125 def putAlert(self, jid): 108 log.debug(u"Not element found for {} in setFocus".format(text))
126 """Put an alert on the jid to get attention from user (e.g. for new message)""" 109
127 self.alert_jid.add(jid.bare) 110 def specialResourceVisible(self, entity):
111 """Assure a resource of a special entity is visible and clickable
112
113 Mainly used to display private conversation in MUC rooms
114 @param entity: full jid of the resource to show
115 """
116 assert isinstance(entity, jid.JID)
117 if entity not in self._special_extras:
118 self._special_extras.add(entity)
119 self.update()
120
121 # events
122
123 def _groupClicked(self, group_wid):
124 group = group_wid.getValue()
125 data = self.getGroupData(group)
126 data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False)
127 self.setFocus(group)
128 self.update() 128 self.update()
129 129
130 def __groupClicked(self, group_wid): 130 def _contactClicked(self, use_bare_jid, contact_wid, selected):
131 group = self.groups[group_wid.getValue()] 131 """Method called when a contact is clicked
132 group[0] = not group[0] 132
133 @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget.
134 If True, all jids in self._alerts with the same bare jid has contact_wid.data will be removed
135 @param contact_wid: widget of the contact, must have the entity set in data attribute
136 @param selected: boolean returned by the widget, telling if it is selected
137 """
138 entity = contact_wid.data
139 if use_bare_jid:
140 to_remove = set()
141 for alert_entity in self._alerts:
142 if alert_entity.bare == entity.bare:
143 to_remove.add(alert_entity)
144 self._alerts.difference_update(to_remove)
145 else:
146 self._alerts.discard(entity)
147 self.host.modeHint(C.MODE_INSERTION)
133 self.update() 148 self.update()
134 self.setFocus(group_wid.getValue()) 149 self._emit('click', entity)
135 150
136 def __contactClicked(self, contact_wid, selected): 151 def onPresenceUpdate(self, entity, show, priority, statuses, profile):
137 self.selected = contact_wid.data 152 super(ContactList, self).onPresenceUpdate(entity, show, priority, statuses, profile)
138 for widget in self.frame.body.body:
139 if widget.__class__ == sat_widgets.SelectableText:
140 widget.setState(widget.data == self.selected, invisible=True)
141 if self.selected in self.alert_jid:
142 self.alert_jid.remove(self.selected)
143 self.host.modeHint('INSERTION')
144 self.update() 153 self.update()
145 self._emit('click') 154
146 155 def onNickUpdate(self, entity, new_nick, profile):
147 def __buildContact(self, content, contacts): 156 self.update()
148 """Add contact representation in widget list 157
158 # Methods to build the widget
159
160 def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None):
161 """Build one contact markup data
162
163 @param entity (jid.JID): entity to build
164 @param keys (iterable): value to markup, in preferred order.
165 The first available key will be used.
166 If key starts with "cache_", it will be checked in cache,
167 else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
168 If nothing full or keys is None, full entity is used.
169 @param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons
170 @param with_alert (bool): if True, show alert if entity is in self._alerts
171 @param with_show_attr (bool): if True, show color corresponding to presence status
172 @param markup_prepend (list): markup to prepend to the generated one before building the widget
173 @param markup_append (list): markup to append to the generated one before building the widget
174 @return (list): markup data are expected by Urwid text widgets
175 """
176 markup = []
177 if use_bare_jid:
178 alerts = {entity.bare for entity in self._alerts}
179 selected = {entity.bare for entity in self._selected}
180 else:
181 alerts = self._alerts
182 selected = self._selected
183 if keys is None:
184 entity_txt = entity
185 else:
186 cache = self.getCache(entity)
187 for key in keys:
188 if key.startswith('cache_'):
189 entity_txt = cache.get(key[6:])
190 else:
191 entity_txt = getattr(entity, key)
192 if entity_txt:
193 break
194 if not entity_txt:
195 entity_txt = entity
196
197 if with_show_attr:
198 show = self.getCache(entity, C.PRESENCE_SHOW)
199 if show is None:
200 show = C.PRESENCE_UNAVAILABLE
201 show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default'))
202 markup.insert(0, u"{} ".format(show_icon))
203 else:
204 entity_attr = 'default'
205
206 if with_alert and entity in alerts:
207 entity_attr = 'alert'
208 header = C.ALERT_HEADER
209 else:
210 header = ''
211
212 markup.append((entity_attr, entity_txt))
213 if markup_prepend:
214 markup.insert(0, markup_prepend)
215 if markup_append:
216 markup.extend(markup_append)
217
218 widget = sat_widgets.SelectableText(markup,
219 selected = entity in selected,
220 header = header)
221 widget.data = entity
222 widget.comp = entity_txt.lower() # value to use for sorting
223 urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid])
224 return widget
225
226 def _buildEntities(self, content, entities):
227 """Add entity representation in widget list
228
149 @param content: widget list, e.g. SimpleListWalker 229 @param content: widget list, e.g. SimpleListWalker
150 @param contacts (list): list of JID userhosts""" 230 @param entities (iterable): iterable of JID to display
151 if not contacts: 231 """
232 if not entities:
152 return 233 return
153 widgets = [] # list of built widgets 234 widgets = [] # list of built widgets
154 235
155 for contact in contacts: 236 for entity in entities:
156 if contact.startswith(C.PRIVATE_PREFIX): 237 if entity in self._specials or not self.entityToShow(entity):
157 contact_disp = ('alert' if contact in self.alert_jid else "show_normal", unescapePrivate(contact)) 238 continue
158 show_icon = '' 239 markup_extra = []
159 status = '' 240 if self.show_resources:
241 for resource in self.getCache(entity, C.CONTACT_RESOURCES):
242 resource_disp = ('resource_main' if resource == self.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n " + resource)
243 markup_extra.append(resource_disp)
244 if self.show_status:
245 status = self.getCache(jid.JID('%s/%s' % (entity, resource)), 'status')
246 status_disp = ('status', "\n " + status) if status else ""
247 markup_extra.append(status_disp)
248
249
160 else: 250 else:
161 jid = JID(contact) 251 if self.show_status:
162 name = self.getCache(jid, 'name') 252 status = self.getCache(entity, 'status')
163 nick = self.getCache(jid, 'nick') 253 status_disp = ('status', "\n " + status) if status else ""
164 status = self.getCache(jid, 'status') 254 markup_extra.append(status_disp)
165 show = self.getCache(jid, 'show') 255 widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), use_bare_jid=True, markup_append=markup_extra)
166 if show is None:
167 show = "unavailable"
168 if not self.contactToShow(contact):
169 continue
170 show_icon, show_attr = C.PRESENCE.get(show, ('', 'default'))
171 contact_disp = ('alert' if contact in self.alert_jid else show_attr, nick or name or jid.node or jid.bare)
172 display = [show_icon + " ", contact_disp]
173 if self.show_status:
174 status_disp = ('status', "\n " + status) if status else ""
175 display.append(status_disp)
176 header = '(*) ' if contact in self.alert_jid else ''
177 widget = sat_widgets.SelectableText(display,
178 selected=contact == self.selected,
179 header=header)
180 widget.data = contact
181 widget.comp = contact_disp[1].lower() # value to use for sorting
182 widgets.append(widget) 256 widgets.append(widget)
183 257
184 widgets.sort(key=lambda widget: widget.comp) 258 widgets.sort(key=lambda widget: widget.comp)
185 259
186 for widget in widgets: 260 for widget in widgets:
187 content.append(widget) 261 content.append(widget)
188 urwid.connect_signal(widget, 'change', self.__contactClicked) 262
189 263 def _buildSpecials(self, content):
190 def __buildSpecials(self, content):
191 """Build the special entities""" 264 """Build the special entities"""
192 specials = self.specials.keys() 265 specials = list(self._specials)
193 specials.sort() 266 specials.sort()
194 for special in specials: 267 extra_shown = set()
195 jid=JID(special) 268 for entity in specials:
196 name = self.getCache(jid, 'name') 269 # the special widgets
197 nick = self.getCache(jid, 'nick') 270 widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False)
198 special_disp = ('alert' if special in self.alert_jid else 'default', nick or name or jid.node or jid.bare)
199 display = [ " " , special_disp]
200 header = '(*) ' if special in self.alert_jid else ''
201 widget = sat_widgets.SelectableText(display,
202 selected = special==self.selected,
203 header=header)
204 widget.data = special
205 content.append(widget) 271 content.append(widget)
206 urwid.connect_signal(widget, 'change', self.__contactClicked) 272
207 273 # resources which must be displayed (e.g. MUC private conversations)
208 def __buildList(self): 274 extras = [extra for extra in self._special_extras if extra.bare == entity.bare]
275 extras.sort()
276 for extra in extras:
277 widget = self._buildEntityWidget(extra, ('resource',), markup_prepend = ' ')
278 content.append(widget)
279 extra_shown.add(extra)
280
281 # entities which must be visible but not resource of current special entities
282 for extra in self._special_extras.difference(extra_shown):
283 widget = self._buildEntityWidget(extra, ('resource',))
284 content.append(widget)
285
286 def _buildList(self):
209 """Build the main contact list widget""" 287 """Build the main contact list widget"""
210 content = urwid.SimpleListWalker([]) 288 content = urwid.SimpleListWalker([])
211 289
212 self.__buildSpecials(content) 290 self._buildSpecials(content)
213 if self.specials: 291 if self._specials:
214 content.append(urwid.Divider('=')) 292 content.append(urwid.Divider('='))
215 293
216 group_keys = self.groups.keys() 294 groups = list(self._groups)
217 group_keys.sort(key=lambda x: x.lower() if x else x) 295 groups.sort(key=lambda x: x.lower() if x else x)
218 for key in group_keys: 296 for group in groups:
219 unfolded = self.groups[key][0] 297 data = self.getGroupData(group)
220 contacts = list(self.groups[key][1]) 298 folded = data.get(C.GROUP_DATA_FOLDED, False)
221 if key is not None and (self.nonEmptyGroup(contacts) or self.show_empty_groups): 299 jids = list(data['jids'])
222 header = '[-]' if unfolded else '[+]' 300 if group is not None and (self.anyEntityToShow(jids) or self.show_empty_groups):
223 widget = sat_widgets.ClickableText(key, header=header + ' ') 301 header = '[-]' if not folded else '[+]'
302 widget = sat_widgets.ClickableText(group, header=header + ' ')
224 content.append(widget) 303 content.append(widget)
225 urwid.connect_signal(widget, 'click', self.__groupClicked) 304 urwid.connect_signal(widget, 'click', self._groupClicked)
226 if unfolded: 305 if not folded:
227 self.__buildContact(content, contacts) 306 self._buildEntities(content, jids)
307 not_in_roster = set(self._cache).difference(self._roster).difference(self._specials).difference((self.whoami.bare,))
308 if not_in_roster:
309 content.append(urwid.Divider('-'))
310 self._buildEntities(content, not_in_roster)
311
228 return urwid.ListBox(content) 312 return urwid.ListBox(content)
229
230 def contactToShow(self, contact):
231 """Tell if the contact should be showed or hidden.
232
233 @param contact (str): JID userhost of the contact
234 @return: True if that contact should be showed in the list"""
235 show = self.getCache(JID(contact), 'show')
236 return (show is not None and show != "unavailable") or \
237 self.show_disconnected or contact in self.alert_jid or contact == self.selected
238
239 def nonEmptyGroup(self, contacts):
240 """Tell if a contact group contains some contacts to show.
241
242 @param contacts (list[str]): list of JID userhosts
243 @return: bool
244 """
245 for contact in contacts:
246 if self.contactToShow(contact):
247 return True
248 return False
249
250 def unselectAll(self):
251 """Unselect all contacts"""
252 self.selected = None
253 for widget in self.frame.body.body:
254 if widget.__class__ == sat_widgets.SelectableText:
255 widget.setState(False, invisible=True)
256
257 def getContact(self):
258 """Return contact currently selected"""
259 return self.selected
260
261 def clearContacts(self):
262 """clear all the contact list"""
263 QuickContactList.clearContacts(self)
264 self.groups={}
265 self.selected = None
266 self.unselectAll()
267 self.update()
268
269 def replace(self, jid, groups=None, attributes=None):
270 """Add a contact to the list if doesn't exist, else update it.
271
272 This method can be called with groups=None for the purpose of updating
273 the contact's attributes (e.g. nickname). In that case, the groups
274 attribute must not be set to the default group but ignored. If not,
275 you may move your contact from its actual group(s) to the default one.
276
277 None value for 'groups' has a different meaning than [None] which is for the default group.
278
279 @param jid (JID)
280 @param groups (list): list of groups or None to ignore the groups membership.
281 @param attributes (dict)
282 """
283 QuickContactList.replace(self, jid, groups, attributes) # eventually change the nickname
284 if jid.bare in self.specials:
285 return
286 if groups is None:
287 self.update()
288 return
289 assert isinstance(jid, JID)
290 assert isinstance(groups, list)
291 if groups == []:
292 groups = [None] # [None] is the default group
293 for group in [group for group in self.groups if group not in groups]:
294 try: # remove the contact from a previous group
295 self.groups[group][1].remove(jid.bare)
296 except KeyError:
297 pass
298 for group in groups:
299 if group not in self.groups:
300 self.groups[group] = [True, set()] # [unfold, list_of_contacts]
301 self.groups[group][1].add(jid.bare)
302 self.update()
303
304 def remove(self, jid):
305 """remove a contact from the list"""
306 QuickContactList.remove(self, jid)
307 groups_to_remove = []
308 for group in self.groups:
309 contacts = self.groups[group][1]
310 if jid.bare in contacts:
311 contacts.remove(jid.bare)
312 if not len(contacts):
313 groups_to_remove.append(group)
314 for group in groups_to_remove:
315 del self.groups[group]
316 self.update()
317
318 def add(self, jid, param_groups=None):
319 """add a contact to the list"""
320 self.replace(jid, param_groups if param_groups else [None])
321
322 def setSpecial(self, special_jid, special_type, show=False):
323 """Set entity as a special
324 @param special_jid: jid of the entity
325 @param special_type: special type (e.g.: "MUC")
326 @param show: True to display the dialog to chat with this entity
327 """
328 QuickContactList.setSpecial(self, special_jid, special_type, show)
329 if None in self.groups:
330 folded, group_jids = self.groups[None]
331 for group_jid in group_jids:
332 if JID(group_jid).bare == special_jid.bare:
333 group_jids.remove(group_jid)
334 break
335 self.update()
336 if show:
337 # also display the dialog for this room
338 self.setFocus(special_jid, True)
339 self.host.redraw()
340
341 def updatePresence(self, jid, show, priority, statuses):
342 #XXX: for the moment, we ignore presence updates for special entities
343 if jid.bare not in self.specials:
344 QuickContactList.updatePresence(self, jid, show, priority, statuses)
345
346 def showOfflineContacts(self, show):
347 show = C.bool(show)
348 if self.show_disconnected == show:
349 return
350 self.show_disconnected = show
351 self.update()
352
353 def showEmptyGroups(self, show):
354 show = C.bool(show)
355 if self.show_empty_groups == show:
356 return
357 self.show_empty_groups = show
358 self.update()