comparison sat_frontends/primitivus/base.py @ 3481:7892585b7e17

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