comparison sat_frontends/primitivus/primitivus @ 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/primitivus@be96beb7ca14
children 93dfbeb41da8
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-2016 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
21 from sat.core.i18n import _, D_
22 from sat_frontends.primitivus.constants import Const as C
23 from sat.core import log_config
24 log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
25 from sat.core import log as logging
26 log = logging.getLogger(__name__)
27 from sat.tools import config as sat_config
28 import urwid
29 from urwid.util import is_wide_char
30 from urwid_satext import sat_widgets
31 from sat_frontends.quick_frontend.quick_app import QuickApp
32 from sat_frontends.quick_frontend import quick_utils
33 from sat_frontends.quick_frontend import quick_chat
34 from sat_frontends.primitivus.profile_manager import ProfileManager
35 from sat_frontends.primitivus.contact_list import ContactList
36 from sat_frontends.primitivus.chat import Chat
37 from sat_frontends.primitivus import xmlui
38 from sat_frontends.primitivus.progress import Progress
39 from sat_frontends.primitivus.notify import Notify
40 from sat_frontends.primitivus.keys import action_key_map as a_key
41 from sat_frontends.primitivus import config
42 from sat_frontends.tools.misc import InputHistory
43 from sat.tools.common import dynamic_import
44 from sat_frontends.tools import jid
45 import signal
46 import sys
47 ## bridge handling
48 # we get bridge name from conf and initialise the right class accordingly
49 main_config = sat_config.parseMainConf()
50 bridge_name = sat_config.getConfig(main_config, '', 'bridge', 'dbus')
51 if 'dbus' not in bridge_name:
52 print(u"only D-Bus bridge is currently supported")
53 sys.exit(3)
54
55
56 class EditBar(sat_widgets.ModalEdit):
57 """
58 The modal edit bar where you would enter messages and commands.
59 """
60
61 def __init__(self, host):
62 modes = {None: (C.MODE_NORMAL, u''),
63 a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '),
64 a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode
65 super(EditBar, self).__init__(modes)
66 self.host = host
67 self.setCompletionMethod(self._text_completion)
68 urwid.connect_signal(self, 'click', self.onTextEntered)
69
70 def _text_completion(self, text, completion_data, mode):
71 if mode == C.MODE_INSERTION:
72 if self.host.selected_widget is not None:
73 try:
74 completion = self.host.selected_widget.completion
75 except AttributeError:
76 return text
77 else:
78 return completion(text, completion_data)
79 else:
80 return text
81
82 def onTextEntered(self, editBar):
83 """Called when text is entered in the main edit bar"""
84 if self.mode == C.MODE_INSERTION:
85 if isinstance(self.host.selected_widget, quick_chat.QuickChat):
86 chat_widget = self.host.selected_widget
87 self.host.messageSend(
88 chat_widget.target,
89 {'': editBar.get_edit_text()}, # TODO: handle language
90 mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
91 errback=lambda failure: self.host.showDialog(_("Error while sending message ({})").format(failure), type="error"),
92 profile_key=chat_widget.profile
93 )
94 editBar.set_edit_text('')
95 elif self.mode == C.MODE_COMMAND:
96 self.commandHandler()
97
98 def commandHandler(self):
99 #TODO: separate class with auto documentation (with introspection)
100 # and completion method
101 tokens = self.get_edit_text().split(' ')
102 command, args = tokens[0], tokens[1:]
103 if command == 'quit':
104 self.host.onExit()
105 raise urwid.ExitMainLoop()
106 elif command == 'messages':
107 wid = sat_widgets.GenericList(logging.memoryGet())
108 self.host.selectWidget(wid)
109 # FIXME: reactivate the command
110 # elif command == 'presence':
111 # values = [value for value in commonConst.PRESENCE.keys()]
112 # values = [value if value else 'online' for value in values] # the empty value actually means 'online'
113 # if args and args[0] in values:
114 # presence = '' if args[0] == 'online' else args[0]
115 # self.host.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
116 # else:
117 # self.host.status_bar.onPresenceClick()
118 # elif command == 'status':
119 # if args:
120 # self.host.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
121 # else:
122 # self.host.status_bar.onStatusClick()
123 elif command == 'history':
124 widget = self.host.selected_widget
125 if isinstance(widget, quick_chat.QuickChat):
126 try:
127 limit = int(args[0])
128 except (IndexError, ValueError):
129 limit = 50
130 widget.updateHistory(size=limit, profile=widget.profile)
131 elif command == 'search':
132 widget = self.host.selected_widget
133 if isinstance(widget, quick_chat.QuickChat):
134 pattern = " ".join(args)
135 if not pattern:
136 self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
137 else:
138 widget.updateHistory(size=C.HISTORY_LIMIT_NONE, filters={'search': pattern}, profile=widget.profile)
139 elif command == 'filter':
140 # FIXME: filter is now only for current widget,
141 # need to be able to set it globally or per widget
142 widget = self.host.selected_widget
143 # FIXME: Q&D way, need to be more generic
144 if isinstance(widget, quick_chat.QuickChat):
145 widget.setFilter(args)
146 elif command in ('topic', 'suject', 'title'):
147 try:
148 new_title = args[0].strip()
149 except IndexError:
150 new_title = None
151 widget = self.host.selected_widget
152 if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
153 widget.onSubjectDialog(new_title)
154 else:
155 return
156 self.set_edit_text('')
157
158 def _historyCb(self, text):
159 self.set_edit_text(text)
160 self.set_edit_pos(len(text))
161
162 def keypress(self, size, key):
163 """Callback when a key is pressed. Send "composing" states
164 and move the index of the temporary history stack."""
165 if key == a_key['MODAL_ESCAPE']:
166 # first save the text to the current mode, then change to NORMAL
167 self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
168 self.host._updateInputHistory(mode=C.MODE_NORMAL)
169 if self._mode == C.MODE_NORMAL and key in self._modes:
170 self.host._updateInputHistory(mode=self._modes[key][0])
171 if key == a_key['HISTORY_PREV']:
172 self.host._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
173 return
174 elif key == a_key['HISTORY_NEXT']:
175 self.host._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
176 return
177 elif key == a_key['EDIT_ENTER']:
178 self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
179 else:
180 if (self._mode == C.MODE_INSERTION
181 and isinstance(self.host.selected_widget, quick_chat.QuickChat)
182 and key not in sat_widgets.FOCUS_KEYS
183 and key not in (a_key['HISTORY_PREV'], a_key['HISTORY_NEXT'])):
184 self.host.bridge.chatStateComposing(self.host.selected_widget.target, self.host.selected_widget.profile)
185
186 return super(EditBar, self).keypress(size, key)
187
188
189 class PrimitivusTopWidget(sat_widgets.FocusPile):
190 """Top most widget used in Primitivus"""
191 _focus_inversed = True
192 positions = ('menu', 'body', 'notif_bar', 'edit_bar')
193 can_hide = ('menu', 'notif_bar')
194
195 def __init__(self, body, menu, notif_bar, edit_bar):
196 self._body = body
197 self._menu = menu
198 self._notif_bar = notif_bar
199 self._edit_bar = edit_bar
200 self._hidden = {'notif_bar'}
201 self._focus_extra = False
202 super(PrimitivusTopWidget, self).__init__([('pack', self._menu), self._body, ('pack', self._edit_bar)])
203 for position in self.positions:
204 setattr(self,
205 position,
206 property(lambda: self, self.widgetGet(position=position),
207 lambda pos, new_wid: self.widgetSet(new_wid, position=pos))
208 )
209 self.focus_position = len(self.contents)-1
210
211 def getVisiblePositions(self, keep=None):
212 """Return positions that are not hidden in the right order
213
214 @param keep: if not None, this position will be keep in the right order, even if it's hidden
215 (can be useful to find its index)
216 @return (list): list of visible positions
217 """
218 return [pos for pos in self.positions if (keep and pos == keep) or pos not in self._hidden]
219
220 def keypress(self, size, key):
221 """Manage FOCUS keys that focus directly a main part (one of self.positions)
222
223 To avoid key conflicts, a combinaison must be made with FOCUS_EXTRA then an other key
224 """
225 if key == a_key['FOCUS_EXTRA']:
226 self._focus_extra = True
227 return
228 if self._focus_extra:
229 self._focus_extra = False
230 if key in ('m', '1'):
231 focus = 'menu'
232 elif key in ('b', '2'):
233 focus = 'body'
234 elif key in ('n', '3'):
235 focus = 'notif_bar'
236 elif key in ('e', '4'):
237 focus = 'edit_bar'
238 else:
239 return super(PrimitivusTopWidget, self).keypress(size, key)
240
241 if focus in self._hidden:
242 return
243
244 self.focus_position = self.getVisiblePositions().index(focus)
245 return
246
247 return super(PrimitivusTopWidget, self).keypress(size, key)
248
249 def widgetGet(self, position):
250 if not position in self.positions:
251 raise ValueError("Unknown position {}".format(position))
252 return getattr(self, "_{}".format(position))
253
254 def widgetSet(self, widget, position):
255 if not position in self.positions:
256 raise ValueError("Unknown position {}".format(position))
257 return setattr(self, "_{}".format(position), widget)
258
259 def hideSwitch(self, position):
260 if not position in self.can_hide:
261 raise ValueError("Can't switch position {}".format(position))
262 hide = not position in self._hidden
263 widget = self.widgetGet(position)
264 idx = self.getVisiblePositions(position).index(position)
265 if hide:
266 del self.contents[idx]
267 self._hidden.add(position)
268 else:
269 self.contents.insert(idx, (widget, ('pack', None)))
270 self._hidden.remove(position)
271
272 def show(self, position):
273 if position in self._hidden:
274 self.hideSwitch(position)
275
276 def hide(self, position):
277 if not position in self._hidden:
278 self.hideSwitch(position)
279
280
281 class PrimitivusApp(QuickApp, InputHistory):
282 MB_HANDLER = False
283 AVATARS_HANDLER = False
284
285 def __init__(self):
286 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
287 if bridge_module is None:
288 log.error(u"Can't import {} bridge".format(bridge_name))
289 sys.exit(3)
290 else:
291 log.debug(u"Loading {} bridge".format(bridge_name))
292 QuickApp.__init__(self, bridge_factory=bridge_module.Bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False)
293 ## main loop setup ##
294 event_loop = urwid.GLibEventLoop if 'dbus' in bridge_name else urwid.TwistedEventLoop
295 self.loop = urwid.MainLoop(urwid.SolidFill(), C.PALETTE, event_loop=event_loop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
296
297 def onBridgeConnected(self):
298
299 ##misc setup##
300 self._visible_widgets = set()
301 self.notif_bar = sat_widgets.NotificationBar()
302 urwid.connect_signal(self.notif_bar, 'change', self.onNotification)
303
304 self.progress_wid = self.widgets.getOrCreateWidget(Progress, None, on_new_widget=None)
305 urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.selectWidget(self.progress_wid))
306 self.__saved_overlay = None
307
308 self.x_notify = Notify()
309
310 # we already manage exit with a_key['APP_QUIT'], so we don't want C-c
311 signal.signal(signal.SIGINT, signal.SIG_IGN)
312 sat_conf = sat_config.parseMainConf()
313 self._bracketed_paste = C.bool(sat_config.getConfig(sat_conf, C.SECTION_NAME, 'bracketed_paste', 'false'))
314 if self._bracketed_paste:
315 log.debug("setting bracketed paste mode as requested")
316 sys.stdout.write("\033[?2004h")
317 self._bracketed_mode_set = True
318
319 self.loop.widget = self.main_widget = ProfileManager(self)
320 self.postInit()
321
322 @property
323 def visible_widgets(self):
324 return self._visible_widgets
325
326 @property
327 def mode(self):
328 return self.editBar.mode
329
330 @mode.setter
331 def mode(self, value):
332 self.editBar.mode = value
333
334 def modeHint(self, value):
335 """Change mode if make sens (i.e.: if there is nothing in the editBar)"""
336 if not self.editBar.get_edit_text():
337 self.mode = value
338
339 def debug(self):
340 """convenient method to reset screen and launch (i)p(u)db"""
341 log.info('Entered debug mode')
342 try:
343 import pudb
344 pudb.set_trace()
345 except ImportError:
346 import os
347 os.system('reset')
348 try:
349 import ipdb
350 ipdb.set_trace()
351 except ImportError:
352 import pdb
353 pdb.set_trace()
354
355 def redraw(self):
356 """redraw the screen"""
357 try:
358 self.loop.draw_screen()
359 except AttributeError:
360 pass
361
362 def start(self):
363 self.connectBridge()
364 self.loop.run()
365
366 def postInit(self):
367 try:
368 config.applyConfig(self)
369 except Exception as e:
370 log.error(u"configuration error: {}".format(e))
371 popup = self.alert(_(u"Configuration Error"), _(u"Something went wrong while reading the configuration, please check :messages"))
372 if self.options.profile:
373 self._early_popup = popup
374 else:
375 self.showPopUp(popup)
376 super(PrimitivusApp, self).postInit(self.main_widget)
377
378 def keysToText(self, keys):
379 """Generator return normal text from urwid keys"""
380 for k in keys:
381 if k == 'tab':
382 yield u'\t'
383 elif k == 'enter':
384 yield u'\n'
385 elif is_wide_char(k,0) or (len(k)==1 and ord(k) >= 32):
386 yield k
387
388 def inputFilter(self, input_, raw):
389 if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
390 return
391
392 ## paste detection/handling
393 if (len(input_) > 1 and # XXX: it may be needed to increase this value if buffer
394 not isinstance(input_[0], tuple) and # or other things result in several chars at once
395 not 'window resize' in input_): # (e.g. using Primitivus through ssh). Need some testing
396 # and experience to adjust value.
397 if input_[0] == 'begin paste' and not self._bracketed_paste:
398 log.info(u"Bracketed paste mode detected")
399 self._bracketed_paste = True
400
401 if self._bracketed_paste:
402 # after this block, extra will contain non pasted keys
403 # and input_ will contain pasted keys
404 try:
405 begin_idx = input_.index('begin paste')
406 except ValueError:
407 # this is not a paste, maybe we have something buffering
408 # or bracketed mode is set in conf but not enabled in term
409 extra = input_
410 input_ = []
411 else:
412 try:
413 end_idx = input_.index('end paste')
414 except ValueError:
415 log.warning(u"missing end paste sequence, discarding paste")
416 extra = input_[:begin_idx]
417 del input_[begin_idx:]
418 else:
419 extra = input_[:begin_idx] + input_[end_idx+1:]
420 input_ = input_[begin_idx+1:end_idx]
421 else:
422 extra = None
423
424 log.debug(u"Paste detected (len {})".format(len(input_)))
425 try:
426 edit_bar = self.editBar
427 except AttributeError:
428 log.warning(u"Paste treated as normal text: there is no edit bar yet")
429 if extra is None:
430 extra = []
431 extra.extend(input_)
432 else:
433 if self.main_widget.focus == edit_bar:
434 # XXX: if a paste is detected, we append it directly to the edit bar text
435 # so the user can check it and press [enter] if it's OK
436 buf_paste = u''.join(self.keysToText(input_))
437 pos = edit_bar.edit_pos
438 edit_bar.set_edit_text(u'{}{}{}'.format(edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:]))
439 edit_bar.edit_pos+=len(buf_paste)
440 else:
441 # we are not on the edit_bar,
442 # so we treat pasted text as normal text
443 if extra is None:
444 extra = []
445 extra.extend(input_)
446 if not extra:
447 return
448 input_ = extra
449 ## end of paste detection/handling
450
451 for i in input_:
452 if isinstance(i,tuple):
453 if i[0] == 'mouse press':
454 if i[1] == 4: #Mouse wheel up
455 input_[input_.index(i)] = a_key['HISTORY_PREV']
456 if i[1] == 5: #Mouse wheel down
457 input_[input_.index(i)] = a_key['HISTORY_NEXT']
458 return input_
459
460 def keyHandler(self, input_):
461 if input_ == a_key['MENU_HIDE']:
462 """User want to (un)hide the menu roller"""
463 try:
464 self.main_widget.hideSwitch('menu')
465 except AttributeError:
466 pass
467 elif input_ == a_key['NOTIFICATION_NEXT']:
468 """User wants to see next notification"""
469 self.notif_bar.showNext()
470 elif input_ == a_key['OVERLAY_HIDE']:
471 """User wants to (un)hide overlay window"""
472 if isinstance(self.loop.widget,urwid.Overlay):
473 self.__saved_overlay = self.loop.widget
474 self.loop.widget = self.main_widget
475 else:
476 if self.__saved_overlay:
477 self.loop.widget = self.__saved_overlay
478 self.__saved_overlay = None
479
480 elif input_ == a_key['DEBUG'] and 'D' in self.bridge.getVersion(): #Debug only for dev versions
481 self.debug()
482 elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
483 try:
484 for wid, options in self.center_part.contents:
485 if self.contact_lists_pile is wid:
486 self.center_part.contents.remove((wid, options))
487 break
488 else:
489 self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
490 except AttributeError:
491 #The main widget is not built (probably in Profile Manager)
492 pass
493 elif input_ == 'window resize':
494 width,height = self.loop.screen_size
495 if height<=5 and width<=35:
496 if not 'save_main_widget' in dir(self):
497 self.save_main_widget = self.loop.widget
498 self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !")))
499 else:
500 if 'save_main_widget' in dir(self):
501 self.loop.widget = self.save_main_widget
502 del self.save_main_widget
503 try:
504 return self.menu_roller.checkShortcuts(input_)
505 except AttributeError:
506 return input_
507
508 def addMenus(self, menu, type_filter, menu_data=None):
509 """Add cached menus to instance
510 @param menu: sat_widgets.Menu instance
511 @param type_filter: menu type like is sat.core.sat_main.importMenu
512 @param menu_data: data to send with these menus
513
514 """
515 def add_menu_cb(callback_id):
516 self.launchAction(callback_id, menu_data, profile=self.current_profile)
517 for id_, type_, path, path_i18n, extra in self.bridge.menusGet("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
518 if type_ != type_filter:
519 continue
520 if len(path) != 2:
521 raise NotImplementedError("Menu with a path != 2 are not implemented yet")
522 menu.addMenu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
523
524
525 def _buildMenuRoller(self):
526 menu = sat_widgets.Menu(self.loop)
527 general = _("General")
528 menu.addMenu(general, _("Connect"), self.onConnectRequest)
529 menu.addMenu(general, _("Disconnect"), self.onDisconnectRequest)
530 menu.addMenu(general, _("Parameters"), self.onParam)
531 menu.addMenu(general, _("About"), self.onAboutRequest)
532 menu.addMenu(general, _("Exit"), self.onExitRequest, a_key['APP_QUIT'])
533 menu.addMenu(_("Contacts")) # add empty menu to save the place in the menu order
534 groups = _("Groups")
535 menu.addMenu(groups)
536 menu.addMenu(groups, _("Join room"), self.onJoinRoomRequest, a_key['ROOM_JOIN'])
537 #additionals menus
538 #FIXME: do this in a more generic way (in quickapp)
539 self.addMenus(menu, C.MENU_GLOBAL)
540
541 menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
542 return menu_roller
543
544 def _buildMainWidget(self):
545 self.contact_lists_pile = urwid.Pile([])
546 #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
547 self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
548
549 self.editBar = EditBar(self)
550 self.menu_roller = self._buildMenuRoller()
551 self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
552 return self.main_widget
553
554 def plugging_profiles(self):
555 self.loop.widget = self._buildMainWidget()
556 self.redraw()
557 try:
558 # if a popup arrived before main widget is build, we need to show it now
559 self.showPopUp(self._early_popup)
560 except AttributeError:
561 pass
562 else:
563 del self._early_popup
564
565 def profilePlugged(self, profile):
566 QuickApp.profilePlugged(self, profile)
567 contact_list = self.widgets.getOrCreateWidget(ContactList, None, on_new_widget=None, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile)
568 self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
569 return contact_list
570
571 def isHidden(self):
572 """Tells if the frontend window is hidden.
573
574 @return bool
575 """
576 return False # FIXME: implement when necessary
577
578 def alert(self, title, message):
579 """Shortcut method to create an alert message
580
581 Alert will have an "OK" button, which remove it if pressed
582 @param title(unicode): title of the dialog
583 @param message(unicode): body of the dialog
584 @return (urwid_satext.Alert): the created Alert instance
585 """
586 popup = sat_widgets.Alert(title, message)
587 popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
588 self.showPopUp(popup)
589 return popup
590
591 def removePopUp(self, widget=None):
592 """Remove current pop-up, and if there is other in queue, show it
593
594 @param widget(None, urwid.Widget): if not None remove this popup from front or queue
595 """
596 # TODO: refactor popup management in a cleaner way
597 if widget is not None:
598 if isinstance(self.loop.widget, urwid.Overlay):
599 current_popup = self.loop.widget.top_w
600 if not current_popup == widget:
601 try:
602 self.notif_bar.removePopUp(widget)
603 except ValueError:
604 log.warning(u"Trying to remove an unknown widget {}".format(widget))
605 return
606 self.loop.widget = self.main_widget
607 next_popup = self.notif_bar.getNextPopup()
608 if next_popup:
609 #we still have popup to show, we display it
610 self.showPopUp(next_popup)
611 else:
612 self.redraw()
613
614 def showPopUp(self, pop_up_widget, perc_width=40, perc_height=40, align='center', valign='middle'):
615 "Show a pop-up window if possible, else put it in queue"
616 if not isinstance(self.loop.widget, urwid.Overlay):
617 display_widget = urwid.Overlay(pop_up_widget, self.main_widget, align, ('relative', perc_width), valign, ('relative', perc_height))
618 self.loop.widget = display_widget
619 self.redraw()
620 else:
621 self.notif_bar.addPopUp(pop_up_widget)
622
623 def barNotify(self, message):
624 """"Notify message to user via notification bar"""
625 self.notif_bar.addMessage(message)
626 self.redraw()
627
628 def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
629 if widget is None or widget is not None and widget != self.selected_widget:
630 # we ignore notification if the widget is selected but we can
631 # still do a desktop notification is the X window has not the focus
632 super(PrimitivusApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile)
633 # we don't want notifications without message on desktop
634 if message is not None and not self.x_notify.hasFocus():
635 if message is None:
636 message = _("{app}: a new event has just happened{entity}").format(
637 app=C.APP_NAME,
638 entity=u' ({})'.format(entity) if entity else '')
639 self.x_notify.sendNotification(message)
640
641
642 def newWidget(self, widget):
643 # FIXME: when several widgets are possible (e.g. with :split)
644 # do not replace current widget when self.selected_widget != None
645 self.selectWidget(widget)
646
647 def selectWidget(self, widget):
648 """Display a widget if possible,
649
650 else add it in the notification bar queue
651 @param widget: BoxWidget
652 """
653 assert len(self.center_part.widget_list)<=2
654 wid_idx = len(self.center_part.widget_list)-1
655 self.center_part.widget_list[wid_idx] = widget
656 try:
657 self.menu_roller.removeMenu(C.MENU_ID_WIDGET)
658 except KeyError:
659 log.debug("No menu to delete")
660 self.selected_widget = widget
661 try:
662 onSelected = self.selected_widget.onSelected
663 except AttributeError:
664 pass
665 else:
666 onSelected()
667 self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
668 self.contact_lists.select(None)
669
670 for wid in self.visible_widgets: # FIXME: check if widgets.getWidgets is not more appropriate
671 if isinstance(wid, Chat):
672 contact_list = self.contact_lists[wid.profile]
673 contact_list.select(wid.target)
674
675 self.redraw()
676
677 def removeWindow(self):
678 """Remove window showed on the right column"""
679 #TODO: better Window management than this hack
680 assert len(self.center_part.widget_list) <= 2
681 wid_idx = len(self.center_part.widget_list)-1
682 self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(''))
683 self.center_part.focus_position = 0
684 self.redraw()
685
686 def addProgress(self, pid, message, profile):
687 """Follow a SàT progression
688
689 @param pid: progression id
690 @param message: message to show to identify the progression
691 """
692 self.progress_wid.add(pid, message, profile)
693
694 def setProgress(self, percentage):
695 """Set the progression shown in notification bar"""
696 self.notif_bar.setProgress(percentage)
697
698 def contactSelected(self, contact_list, entity):
699 self.clearNotifs(entity, profile=contact_list.profile)
700 if entity.resource:
701 # we have clicked on a private MUC conversation
702 chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
703 else:
704 chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
705 self.selectWidget(chat_widget)
706 self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
707
708 def _dialogOkCb(self, widget, data):
709 popup, answer_cb, answer_data = data
710 self.removePopUp(popup)
711 if answer_cb is not None:
712 answer_cb(True, answer_data)
713
714 def _dialogCancelCb(self, widget, data):
715 popup, answer_cb, answer_data = data
716 self.removePopUp(popup)
717 if answer_cb is not None:
718 answer_cb(False, answer_data)
719
720 def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None):
721 if type == 'info':
722 popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
723 if answer_cb is None:
724 popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
725 elif type == 'error':
726 popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
727 if answer_cb is None:
728 popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
729 elif type == 'yes/no':
730 popup = sat_widgets.ConfirmDialog(message)
731 popup.setCallback('yes', self._dialogOkCb, (popup, answer_cb, answer_data))
732 popup.setCallback('no', self._dialogCancelCb, (popup, answer_cb, answer_data))
733 else:
734 popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
735 if answer_cb is None:
736 popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
737 log.error(u'unmanaged dialog type: {}'.format(type))
738 self.showPopUp(popup)
739
740 def dialogFailure(self, failure):
741 """Show a failure that has been returned by an asynchronous bridge method.
742
743 @param failure (defer.Failure): Failure instance
744 """
745 self.alert(failure.classname, failure.message)
746
747 def onNotification(self, notif_bar):
748 """Called when a new notification has been received"""
749 if not isinstance(self.main_widget, PrimitivusTopWidget):
750 #if we are not in the main configuration, we ignore the notifications bar
751 return
752 if self.notif_bar.canHide():
753 #No notification left, we can hide the bar
754 self.main_widget.hide('notif_bar')
755 else:
756 self.main_widget.show('notif_bar')
757 self.redraw() # FIXME: invalidate cache in a more efficient way
758
759 def _actionManagerUnknownError(self):
760 self.alert(_("Error"), _(u"Unmanaged action"))
761
762 def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, subject, profile):
763 super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, subject, profile)
764 # if self.selected_widget is None:
765 # for contact_list in self.widgets.getWidgets(ContactList):
766 # if profile in contact_list.profiles:
767 # contact_list.setFocus(jid.JID(room_jid_s), True)
768
769 def progressStartedHandler(self, pid, metadata, profile):
770 super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile)
771 self.addProgress(pid, metadata.get('name', _(u'unkown')), profile)
772
773 def progressFinishedHandler(self, pid, metadata, profile):
774 log.info(u"Progress {} finished".format(pid))
775 super(PrimitivusApp, self).progressFinishedHandler(pid, metadata, profile)
776
777 def progressErrorHandler(self, pid, err_msg, profile):
778 log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
779 super(PrimitivusApp, self).progressErrorHandler(pid, err_msg, profile)
780
781
782 ##DIALOGS CALLBACKS##
783 def onJoinRoom(self, button, edit):
784 self.removePopUp()
785 room_jid = jid.JID(edit.get_edit_text())
786 self.bridge.mucJoin(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
787
788 #MENU EVENTS#
789 def onConnectRequest(self, menu):
790 QuickApp.connect(self, self.current_profile)
791
792 def onDisconnectRequest(self, menu):
793 self.disconnect(self.current_profile)
794
795 def onParam(self, menu):
796 def success(params):
797 ui = xmlui.create(self, xml_data=params, profile=self.current_profile)
798 ui.show()
799
800 def failure(error):
801 self.alert(_("Error"), _("Can't get parameters (%s)") % error)
802 self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
803
804 def onExitRequest(self, menu):
805 QuickApp.onExit(self)
806 try:
807 if self._bracketed_mode_set: # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf)
808 log.debug("unsetting bracketed paste mode")
809 sys.stdout.write("\033[?2004l")
810 except AttributeError:
811 pass
812 raise urwid.ExitMainLoop()
813
814 def onJoinRoomRequest(self, menu):
815 """User wants to join a MUC room"""
816 pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt=self.bridge.mucGetDefaultService(), ok_cb=self.onJoinRoom)
817 pop_up_widget.setCallback('cancel', lambda dummy: self.removePopUp(pop_up_widget))
818 self.showPopUp(pop_up_widget)
819
820 def onAboutRequest(self, menu):
821 self.alert(_("About"), C.APP_NAME + " v" + self.bridge.getVersion())
822
823 #MISC CALLBACKS#
824
825 def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE):
826 contact_list_wid = self.widgets.getWidget(ContactList, profiles=profile)
827 if contact_list_wid is not None:
828 contact_list_wid.status_bar.setPresenceStatus(show, status)
829 else:
830 log.warning(u"No ContactList widget found for profile {}".format(profile))
831
832 primitivus = PrimitivusApp()
833 primitivus.start()