comparison sat_frontends/primitivus/contact_list.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/primitivus/contact_list.py@0046283a285d
children 81b70eeb710f
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # Primitivus: a SAT frontend
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
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/>.
19
20 from sat.core.i18n import _
21 import urwid
22 from urwid_satext import sat_widgets
23 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
24 from sat_frontends.primitivus.status import StatusBar
25 from sat_frontends.primitivus.constants import Const as C
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
30 log = logging.getLogger(__name__)
31 from sat_frontends.quick_frontend import quick_widgets
32
33
34 class ContactList(PrimitivusWidget, QuickContactList):
35 PROFILES_MULTIPLE=False
36 PROFILES_ALLOW_NONE=False
37 signals = ['click','change']
38 # FIXME: Only single profile is managed so far
39
40 def __init__(self, host, target, on_click=None, on_change=None, user_data=None, profiles=None):
41 QuickContactList.__init__(self, host, profiles)
42 self.contact_list = self.host.contact_lists[self.profile]
43
44 #we now build the widget
45 self.status_bar = StatusBar(host)
46 self.frame = sat_widgets.FocusFrame(self._buildList(), None, self.status_bar)
47 PrimitivusWidget.__init__(self, self.frame, _(u'Contacts'))
48 if on_click:
49 urwid.connect_signal(self, 'click', on_click, user_data)
50 if on_change:
51 urwid.connect_signal(self, 'change', on_change, user_data)
52 self.host.addListener('notification', self.onNotification, [self.profile])
53 self.host.addListener('notificationsClear', self.onNotification, [self.profile])
54 self.postInit()
55
56 def update(self, entities=None, type_=None, profile=None):
57 """Update display, keep focus"""
58 # FIXME: full update is done each time, must handle entities, type_ and profile
59 widget, position = self.frame.body.get_focus()
60 self.frame.body = self._buildList()
61 if position:
62 try:
63 self.frame.body.focus_position = position
64 except IndexError:
65 pass
66 self._invalidate()
67 self.host.redraw() # FIXME: check if can be avoided
68
69 def keypress(self, size, key):
70 # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
71 # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element
72 if key in sat_widgets.FOCUS_KEYS:
73 if (key == a_key['FOCUS_SWITCH'] or (key == a_key['FOCUS_UP'] and self.frame.focus_position == 'body') or
74 (key == a_key['FOCUS_DOWN'] and self.frame.focus_position == 'footer')):
75 return key
76 if key == a_key['STATUS_HIDE']: #user wants to (un)hide contacts' statuses
77 self.contact_list.show_status = not self.contact_list.show_status
78 self.update()
79 elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts
80 self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.boolConst(not self.contact_list.show_disconnected), "General", profile_key=self.profile)
81 elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources
82 self.contact_list.showResources(not self.contact_list.show_resources)
83 self.update()
84 return super(ContactList, self).keypress(size, key)
85
86 # QuickWidget methods
87
88 @staticmethod
89 def getWidgetHash(target, profiles):
90 profiles = sorted(profiles)
91 return tuple(profiles)
92
93 # modify the contact list
94
95 def setFocus(self, text, select=False):
96 """give focus to the first element that matches the given text. You can also
97 pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode).
98
99 @param text: contact group name, contact or muc userhost, muc private dialog jid
100 @param select: if True, the element is also clicked
101 """
102 idx = 0
103 for widget in self.frame.body.body:
104 try:
105 if isinstance(widget, sat_widgets.ClickableText):
106 # contact group
107 value = widget.getValue()
108 elif isinstance(widget, sat_widgets.SelectableText):
109 # contact or muc
110 value = widget.data
111 else:
112 # Divider instance
113 continue
114 # there's sometimes a leading space
115 if text.strip() == value.strip():
116 self.frame.body.focus_position = idx
117 if select:
118 self._contactClicked(False, widget, True)
119 return
120 except AttributeError:
121 pass
122 idx += 1
123
124 log.debug(u"Not element found for {} in setFocus".format(text))
125
126 # events
127
128 def _groupClicked(self, group_wid):
129 group = group_wid.getValue()
130 data = self.contact_list.getGroupData(group)
131 data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False)
132 self.setFocus(group)
133 self.update()
134
135 def _contactClicked(self, use_bare_jid, contact_wid, selected):
136 """Method called when a contact is clicked
137
138 @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget.
139 @param contact_wid: widget of the contact, must have the entity set in data attribute
140 @param selected: boolean returned by the widget, telling if it is selected
141 """
142 entity = contact_wid.data
143 self.host.modeHint(C.MODE_INSERTION)
144 self._emit('click', entity)
145
146 def onNotification(self, entity, notif, profile):
147 notifs = list(self.host.getNotifs(C.ENTITY_ALL, profile=self.profile))
148 if notifs:
149 self.title_dynamic = u"({})".format(len(notifs))
150 else:
151 self.title_dynamic = None
152 self.host.redraw() # FIXME: should not be necessary
153
154 # Methods to build the widget
155
156 def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_notifs=True, with_show_attr=True, markup_prepend=None, markup_append=None, special=False):
157 """Build one contact markup data
158
159 @param entity (jid.JID): entity to build
160 @param keys (iterable): value to markup, in preferred order.
161 The first available key will be used.
162 If key starts with "cache_", it will be checked in cache,
163 else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
164 If nothing full or keys is None, full entity is used.
165 @param use_bare_jid (bool): if True, use bare jid for selected comparisons
166 @param with_notifs (bool): if True, show notification count
167 @param with_show_attr (bool): if True, show color corresponding to presence status
168 @param markup_prepend (list): markup to prepend to the generated one before building the widget
169 @param markup_append (list): markup to append to the generated one before building the widget
170 @param special (bool): True if entity is a special one
171 @return (list): markup data are expected by Urwid text widgets
172 """
173 markup = []
174 if use_bare_jid:
175 selected = {entity.bare for entity in self.contact_list._selected}
176 else:
177 selected = self.contact_list._selected
178 if keys is None:
179 entity_txt = entity
180 else:
181 cache = self.contact_list.getCache(entity)
182 for key in keys:
183 if key.startswith('cache_'):
184 entity_txt = cache.get(key[6:])
185 else:
186 entity_txt = getattr(entity, key)
187 if entity_txt:
188 break
189 if not entity_txt:
190 entity_txt = entity
191
192 if with_show_attr:
193 show = self.contact_list.getCache(entity, C.PRESENCE_SHOW)
194 if show is None:
195 show = C.PRESENCE_UNAVAILABLE
196 show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default'))
197 markup.insert(0, u"{} ".format(show_icon))
198 else:
199 entity_attr = 'default'
200
201 notifs = list(self.host.getNotifs(entity, exact_jid=special, profile=self.profile))
202 if notifs:
203 header = [('cl_notifs', u'({})'.format(len(notifs))), u' ']
204 if list(self.host.getNotifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile)):
205 header = ('cl_mention', header)
206 else:
207 header = u''
208
209 markup.append((entity_attr, entity_txt))
210 if markup_prepend:
211 markup.insert(0, markup_prepend)
212 if markup_append:
213 markup.extend(markup_append)
214
215 widget = sat_widgets.SelectableText(markup,
216 selected = entity in selected,
217 header = header)
218 widget.data = entity
219 widget.comp = entity_txt.lower() # value to use for sorting
220 urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid])
221 return widget
222
223 def _buildEntities(self, content, entities):
224 """Add entity representation in widget list
225
226 @param content: widget list, e.g. SimpleListWalker
227 @param entities (iterable): iterable of JID to display
228 """
229 if not entities:
230 return
231 widgets = [] # list of built widgets
232
233 for entity in entities:
234 if entity in self.contact_list._specials or not self.contact_list.entityToShow(entity):
235 continue
236 markup_extra = []
237 if self.contact_list.show_resources:
238 for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES):
239 resource_disp = ('resource_main' if resource == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n " + resource)
240 markup_extra.append(resource_disp)
241 if self.contact_list.show_status:
242 status = self.contact_list.getCache(jid.JID('%s/%s' % (entity, resource)), 'status')
243 status_disp = ('status', "\n " + status) if status else ""
244 markup_extra.append(status_disp)
245
246
247 else:
248 if self.contact_list.show_status:
249 status = self.contact_list.getCache(entity, 'status')
250 status_disp = ('status', "\n " + status) if status else ""
251 markup_extra.append(status_disp)
252 widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), use_bare_jid=True, markup_append=markup_extra)
253 widgets.append(widget)
254
255 widgets.sort(key=lambda widget: widget.comp)
256
257 for widget in widgets:
258 content.append(widget)
259
260 def _buildSpecials(self, content):
261 """Build the special entities"""
262 specials = sorted(self.contact_list.getSpecials())
263 current = None
264 for entity in specials:
265 if current is not None and current.bare == entity.bare:
266 # nested entity (e.g. MUC private conversations)
267 widget = self._buildEntityWidget(entity, ('resource',), markup_prepend=' ', special=True)
268 else:
269 # the special widgets
270 if entity.resource:
271 widget = self._buildEntityWidget(entity, ('resource',), special=True)
272 else:
273 widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False, special=True)
274 content.append(widget)
275
276 def _buildList(self):
277 """Build the main contact list widget"""
278 content = urwid.SimpleListWalker([])
279
280 self._buildSpecials(content)
281 if self.contact_list._specials:
282 content.append(urwid.Divider('='))
283
284 groups = list(self.contact_list._groups)
285 groups.sort(key=lambda x: x.lower() if x else x)
286 for group in groups:
287 data = self.contact_list.getGroupData(group)
288 folded = data.get(C.GROUP_DATA_FOLDED, False)
289 jids = list(data['jids'])
290 if group is not None and (self.contact_list.anyEntityToShow(jids) or self.contact_list.show_empty_groups):
291 header = '[-]' if not folded else '[+]'
292 widget = sat_widgets.ClickableText(group, header=header + ' ')
293 content.append(widget)
294 urwid.connect_signal(widget, 'click', self._groupClicked)
295 if not folded:
296 self._buildEntities(content, jids)
297 not_in_roster = set(self.contact_list._cache).difference(self.contact_list._roster).difference(self.contact_list._specials).difference((self.contact_list.whoami.bare,))
298 if not_in_roster:
299 content.append(urwid.Divider('-'))
300 self._buildEntities(content, not_in_roster)
301
302 return urwid.ListBox(content)
303
304 quick_widgets.register(QuickContactList, ContactList)