comparison frontends/src/primitivus/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 93a5e2673929
children faa1129559b8
comparison
equal deleted inserted replaced
1264:60dfa2f5d61f 1265:e3a9ea76de35
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 # Methods to build the widget
137 self.selected = contact_wid.data 152
138 for widget in self.frame.body.body: 153 def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None):
139 if widget.__class__ == sat_widgets.SelectableText: 154 """Build one contact markup data
140 widget.setState(widget.data == self.selected, invisible=True) 155
141 if self.selected in self.alert_jid: 156 @param entity (jid.JID): entity to build
142 self.alert_jid.remove(self.selected) 157 @param keys (iterable): value to markup, in preferred order.
143 self.host.modeHint('INSERTION') 158 The first available key will be used.
144 self.update() 159 If key starts with "cache_", it will be checked in cache,
145 self._emit('click') 160 else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
146 161 If nothing full or keys is None, full entity is used.
147 def __buildContact(self, content, contacts): 162 @param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons
148 """Add contact representation in widget list 163 @param with_alert (bool): if True, show alert if entity is in self._alerts
164 @param with_show_attr (bool): if True, show color corresponding to presence status
165 @param markup_prepend (list): markup to prepend to the generated one before building the widget
166 @param markup_append (list): markup to append to the generated one before building the widget
167 @return (list): markup data are expected by Urwid text widgets
168 """
169 markup = []
170 if use_bare_jid:
171 alerts = {entity.bare for entity in self._alerts}
172 selected = {entity.bare for entity in self._selected}
173 else:
174 alerts = self._alerts
175 selected = self._selected
176 if keys is None:
177 entity_txt = entity
178 else:
179 cache = self.getCache(entity)
180 for key in keys:
181 if key.startswith('cache_'):
182 entity_txt = cache.get(key[6:])
183 else:
184 entity_txt = getattr(entity, key)
185 if entity_txt:
186 break
187 if not entity_txt:
188 entity_txt = entity
189
190 if with_show_attr:
191 show = self.getCache(entity, C.PRESENCE_SHOW)
192 if show is None:
193 show = C.PRESENCE_UNAVAILABLE
194 show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default'))
195 markup.insert(0, u"{} ".format(show_icon))
196 else:
197 entity_attr = 'default'
198
199 if with_alert and entity in alerts:
200 entity_attr = 'alert'
201 header = C.ALERT_HEADER
202 else:
203 header = ''
204
205 markup.append((entity_attr, entity_txt))
206 if markup_prepend:
207 markup.insert(0, markup_prepend)
208 if markup_append:
209 markup.extend(markup_append)
210
211 widget = sat_widgets.SelectableText(markup,
212 selected = entity in selected,
213 header = header)
214 widget.data = entity
215 widget.comp = entity_txt.lower() # value to use for sorting
216 urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid])
217 return widget
218
219 def _buildEntities(self, content, entities):
220 """Add entity representation in widget list
221
149 @param content: widget list, e.g. SimpleListWalker 222 @param content: widget list, e.g. SimpleListWalker
150 @param contacts (list): list of JID userhosts""" 223 @param entities (iterable): iterable of JID to display
151 if not contacts: 224 """
225 if not entities:
152 return 226 return
153 widgets = [] # list of built widgets 227 widgets = [] # list of built widgets
154 228
155 for contact in contacts: 229 for entity in entities:
156 if contact.startswith(C.PRIVATE_PREFIX): 230 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)) 231 continue
158 show_icon = '' 232 markup_extra = []
159 status = '' 233 if self.show_resources:
234 for resource in self.getCache(entity, C.CONTACT_RESOURCES):
235 resource_disp = ('resource_main' if resource == self.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n " + resource)
236 markup_extra.append(resource_disp)
237 if self.show_status:
238 status = self.getCache(jid.JID('%s/%s' % (entity, resource)), 'status')
239 status_disp = ('status', "\n " + status) if status else ""
240 markup_extra.append(status_disp)
241
242
160 else: 243 else:
161 jid = JID(contact) 244 if self.show_status:
162 name = self.getCache(jid, 'name') 245 status = self.getCache(entity, 'status')
163 nick = self.getCache(jid, 'nick') 246 status_disp = ('status', "\n " + status) if status else ""
164 status = self.getCache(jid, 'status') 247 markup_extra.append(status_disp)
165 show = self.getCache(jid, 'show') 248 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) 249 widgets.append(widget)
183 250
184 widgets.sort(key=lambda widget: widget.comp) 251 widgets.sort(key=lambda widget: widget.comp)
185 252
186 for widget in widgets: 253 for widget in widgets:
187 content.append(widget) 254 content.append(widget)
188 urwid.connect_signal(widget, 'change', self.__contactClicked) 255
189 256 def _buildSpecials(self, content):
190 def __buildSpecials(self, content):
191 """Build the special entities""" 257 """Build the special entities"""
192 specials = self.specials.keys() 258 specials = list(self._specials)
193 specials.sort() 259 specials.sort()
194 for special in specials: 260 extra_shown = set()
195 jid=JID(special) 261 for entity in specials:
196 name = self.getCache(jid, 'name') 262 # the special widgets
197 nick = self.getCache(jid, 'nick') 263 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) 264 content.append(widget)
206 urwid.connect_signal(widget, 'change', self.__contactClicked) 265
207 266 # resources which must be displayed (e.g. MUC private conversations)
208 def __buildList(self): 267 extras = [extra for extra in self._special_extras if extra.bare == entity.bare]
268 extras.sort()
269 for extra in extras:
270 widget = self._buildEntityWidget(extra, ('resource',), markup_prepend = ' ')
271 content.append(widget)
272 extra_shown.add(extra)
273
274 # entities which must be visible but not resource of current special entities
275 for extra in self._special_extras.difference(extra_shown):
276 widget = self._buildEntityWidget(extra, ('resource',))
277 content.append(widget)
278
279 def _buildList(self):
209 """Build the main contact list widget""" 280 """Build the main contact list widget"""
210 content = urwid.SimpleListWalker([]) 281 content = urwid.SimpleListWalker([])
211 282
212 self.__buildSpecials(content) 283 self._buildSpecials(content)
213 if self.specials: 284 if self._specials:
214 content.append(urwid.Divider('=')) 285 content.append(urwid.Divider('='))
215 286
216 group_keys = self.groups.keys() 287 groups = list(self._groups)
217 group_keys.sort(key=lambda x: x.lower() if x else x) 288 groups.sort(key=lambda x: x.lower() if x else x)
218 for key in group_keys: 289 for group in groups:
219 unfolded = self.groups[key][0] 290 data = self.getGroupData(group)
220 contacts = list(self.groups[key][1]) 291 folded = data.get(C.GROUP_DATA_FOLDED, False)
221 if key is not None and (self.nonEmptyGroup(contacts) or self.show_empty_groups): 292 jids = list(data['jids'])
222 header = '[-]' if unfolded else '[+]' 293 if group is not None and (self.anyEntityToShow(jids) or self.show_empty_groups):
223 widget = sat_widgets.ClickableText(key, header=header + ' ') 294 header = '[-]' if not folded else '[+]'
295 widget = sat_widgets.ClickableText(group, header=header + ' ')
224 content.append(widget) 296 content.append(widget)
225 urwid.connect_signal(widget, 'click', self.__groupClicked) 297 urwid.connect_signal(widget, 'click', self._groupClicked)
226 if unfolded: 298 if not folded:
227 self.__buildContact(content, contacts) 299 self._buildEntities(content, jids)
300 not_in_roster = set(self._cache).difference(self._roster).difference(self._specials)
301 if not_in_roster:
302 content.append(urwid.Divider('-'))
303 self._buildEntities(content, not_in_roster)
304
228 return urwid.ListBox(content) 305 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()