comparison libervia/tui/base.py @ 4076:b620a8e882e1

refactoring: rename `libervia.frontends.primitivus` to `libervia.tui`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:25:25 +0200
parents libervia/frontends/primitivus/base.py@26b7ed2817da
children 15055a00162c
comparison
equal deleted inserted replaced
4075:47401850dec6 4076:b620a8e882e1
1 #!/usr/bin/env python3
2
3 # Libervia TUI
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 libervia.backend.core.i18n import _, D_
21 from libervia.tui.constants import Const as C
22 from libervia.backend.core import log_config
23 log_config.sat_configure(C.LOG_BACKEND_STANDARD, C)
24 from libervia.backend.core import log as logging
25 log = logging.getLogger(__name__)
26 from libervia.backend.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 libervia.frontends.quick_frontend.quick_app import QuickApp
31 from libervia.frontends.quick_frontend import quick_utils
32 from libervia.frontends.quick_frontend import quick_chat
33 from libervia.tui.profile_manager import ProfileManager
34 from libervia.tui.contact_list import ContactList
35 from libervia.tui.chat import Chat
36 from libervia.tui import xmlui
37 from libervia.tui.progress import Progress
38 from libervia.tui.notify import Notify
39 from libervia.tui.keys import action_key_map as a_key
40 from libervia.tui import config
41 from libervia.frontends.tools.misc import InputHistory
42 from libervia.backend.tools.common import dynamic_import
43 from libervia.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.parse_main_conf()
49 bridge_name = sat_config.config_get(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.set_completion_method(self._text_completion)
67 urwid.connect_signal(self, 'click', self.on_text_entered)
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 on_text_entered(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.message_send(
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.show_dialog(_("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.command_handler()
96
97 def command_handler(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.on_exit()
104 raise urwid.ExitMainLoop()
105 elif command == 'messages':
106 wid = sat_widgets.GenericList(logging.memory_get())
107 self.host.select_widget(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.on_change(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
115 # else:
116 # self.host.status_bar.on_presence_click()
117 # elif command == 'status':
118 # if args:
119 # self.host.status_bar.on_change(user_data=sat_widgets.AdvancedEdit(args[0]))
120 # else:
121 # self.host.status_bar.on_status_click()
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.update_history(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.add_message(D_("Please specify the globbing pattern to search for"))
136 else:
137 widget.update_history(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.set_filter(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.on_subject_dialog(new_title)
153 else:
154 return
155 self.set_edit_text('')
156
157 def _history_cb(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._update_input_history(self.get_edit_text(), mode=self.mode)
167 self.host._update_input_history(mode=C.MODE_NORMAL)
168 if self._mode == C.MODE_NORMAL and key in self._modes:
169 self.host._update_input_history(mode=self._modes[key][0])
170 if key == a_key['HISTORY_PREV']:
171 self.host._update_input_history(self.get_edit_text(), -1, self._history_cb, self.mode)
172 return
173 elif key == a_key['HISTORY_NEXT']:
174 self.host._update_input_history(self.get_edit_text(), +1, self._history_cb, self.mode)
175 return
176 elif key == a_key['EDIT_ENTER']:
177 self.host._update_input_history(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.chat_state_composing(self.host.selected_widget.target, self.host.selected_widget.profile)
185
186 return super(EditBar, self).keypress(size, key)
187
188
189 class LiberviaTUITopWidget(sat_widgets.FocusPile):
190 """Top most widget used in LiberviaTUI"""
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(LiberviaTUITopWidget, 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.widget_get(position=position),
207 lambda pos, new_wid: self.widget_set(new_wid, position=pos))
208 )
209 self.focus_position = len(self.contents)-1
210
211 def get_visible_positions(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(LiberviaTUITopWidget, self).keypress(size, key)
240
241 if focus in self._hidden:
242 return
243
244 self.focus_position = self.get_visible_positions().index(focus)
245 return
246
247 return super(LiberviaTUITopWidget, self).keypress(size, key)
248
249 def widget_get(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 widget_set(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 hide_switch(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.widget_get(position)
264 idx = self.get_visible_positions(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.hide_switch(position)
275
276 def hide(self, position):
277 if not position in self._hidden:
278 self.hide_switch(position)
279
280
281 class LiberviaTUIApp(QuickApp, InputHistory):
282 MB_HANDLER = False
283 AVATARS_HANDLER = False
284
285 def __init__(self):
286 bridge_module = dynamic_import.bridge(bridge_name, 'libervia.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.input_filter, unhandled_input=self.key_handler)
296
297 @classmethod
298 def run(cls):
299 cls().start()
300
301 def on_bridge_connected(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.on_notification)
307
308 self.progress_wid = self.widgets.get_or_create_widget(Progress, None, on_new_widget=None)
309 urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.select_widget(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.parse_main_conf()
317 self._bracketed_paste = C.bool(
318 sat_config.config_get(sat_conf, C.CONFIG_SECTION, 'bracketed_paste', 'false')
319 )
320 if self._bracketed_paste:
321 log.debug("setting bracketed paste mode as requested")
322 sys.stdout.write("\033[?2004h")
323 self._bracketed_mode_set = True
324
325 self.loop.widget = self.main_widget = ProfileManager(self)
326 self.post_init()
327
328 @property
329 def visible_widgets(self):
330 return self._visible_widgets
331
332 @property
333 def mode(self):
334 return self.editBar.mode
335
336 @mode.setter
337 def mode(self, value):
338 self.editBar.mode = value
339
340 def mode_hint(self, value):
341 """Change mode if make sens (i.e.: if there is nothing in the editBar)"""
342 if not self.editBar.get_edit_text():
343 self.mode = value
344
345 def debug(self):
346 """convenient method to reset screen and launch (i)p(u)db"""
347 log.info('Entered debug mode')
348 try:
349 import pudb
350 pudb.set_trace()
351 except ImportError:
352 import os
353 os.system('reset')
354 try:
355 import ipdb
356 ipdb.set_trace()
357 except ImportError:
358 import pdb
359 pdb.set_trace()
360
361 def redraw(self):
362 """redraw the screen"""
363 try:
364 self.loop.draw_screen()
365 except AttributeError:
366 pass
367
368 def start(self):
369 self.connect_bridge()
370 self.loop.run()
371
372 def post_init(self):
373 try:
374 config.apply_config(self)
375 except Exception as e:
376 log.error(u"configuration error: {}".format(e))
377 popup = self.alert(_(u"Configuration Error"), _(u"Something went wrong while reading the configuration, please check :messages"))
378 if self.options.profile:
379 self._early_popup = popup
380 else:
381 self.show_pop_up(popup)
382 super(LiberviaTUIApp, self).post_init(self.main_widget)
383
384 def keys_to_text(self, keys):
385 """Generator return normal text from urwid keys"""
386 for k in keys:
387 if k == 'tab':
388 yield u'\t'
389 elif k == 'enter':
390 yield u'\n'
391 elif is_wide_char(k,0) or (len(k)==1 and ord(k) >= 32):
392 yield k
393
394 def input_filter(self, input_, raw):
395 if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
396 return
397
398 ## paste detection/handling
399 if (len(input_) > 1 and # XXX: it may be needed to increase this value if buffer
400 not isinstance(input_[0], tuple) and # or other things result in several chars at once
401 not 'window resize' in input_): # (e.g. using LiberviaTUI through ssh). Need some testing
402 # and experience to adjust value.
403 if input_[0] == 'begin paste' and not self._bracketed_paste:
404 log.info(u"Bracketed paste mode detected")
405 self._bracketed_paste = True
406
407 if self._bracketed_paste:
408 # after this block, extra will contain non pasted keys
409 # and input_ will contain pasted keys
410 try:
411 begin_idx = input_.index('begin paste')
412 except ValueError:
413 # this is not a paste, maybe we have something buffering
414 # or bracketed mode is set in conf but not enabled in term
415 extra = input_
416 input_ = []
417 else:
418 try:
419 end_idx = input_.index('end paste')
420 except ValueError:
421 log.warning(u"missing end paste sequence, discarding paste")
422 extra = input_[:begin_idx]
423 del input_[begin_idx:]
424 else:
425 extra = input_[:begin_idx] + input_[end_idx+1:]
426 input_ = input_[begin_idx+1:end_idx]
427 else:
428 extra = None
429
430 log.debug(u"Paste detected (len {})".format(len(input_)))
431 try:
432 edit_bar = self.editBar
433 except AttributeError:
434 log.warning(u"Paste treated as normal text: there is no edit bar yet")
435 if extra is None:
436 extra = []
437 extra.extend(input_)
438 else:
439 if self.main_widget.focus == edit_bar:
440 # XXX: if a paste is detected, we append it directly to the edit bar text
441 # so the user can check it and press [enter] if it's OK
442 buf_paste = u''.join(self.keys_to_text(input_))
443 pos = edit_bar.edit_pos
444 edit_bar.set_edit_text(u'{}{}{}'.format(edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:]))
445 edit_bar.edit_pos+=len(buf_paste)
446 else:
447 # we are not on the edit_bar,
448 # so we treat pasted text as normal text
449 if extra is None:
450 extra = []
451 extra.extend(input_)
452 if not extra:
453 return
454 input_ = extra
455 ## end of paste detection/handling
456
457 for i in input_:
458 if isinstance(i,tuple):
459 if i[0] == 'mouse press':
460 if i[1] == 4: #Mouse wheel up
461 input_[input_.index(i)] = a_key['HISTORY_PREV']
462 if i[1] == 5: #Mouse wheel down
463 input_[input_.index(i)] = a_key['HISTORY_NEXT']
464 return input_
465
466 def key_handler(self, input_):
467 if input_ == a_key['MENU_HIDE']:
468 """User want to (un)hide the menu roller"""
469 try:
470 self.main_widget.hide_switch('menu')
471 except AttributeError:
472 pass
473 elif input_ == a_key['NOTIFICATION_NEXT']:
474 """User wants to see next notification"""
475 self.notif_bar.show_next()
476 elif input_ == a_key['OVERLAY_HIDE']:
477 """User wants to (un)hide overlay window"""
478 if isinstance(self.loop.widget,urwid.Overlay):
479 self.__saved_overlay = self.loop.widget
480 self.loop.widget = self.main_widget
481 else:
482 if self.__saved_overlay:
483 self.loop.widget = self.__saved_overlay
484 self.__saved_overlay = None
485
486 elif input_ == a_key['DEBUG'] and 'D' in self.bridge.version_get(): #Debug only for dev versions
487 self.debug()
488 elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
489 try:
490 for wid, options in self.center_part.contents:
491 if self.contact_lists_pile is wid:
492 self.center_part.contents.remove((wid, options))
493 break
494 else:
495 self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
496 except AttributeError:
497 #The main widget is not built (probably in Profile Manager)
498 pass
499 elif input_ == 'window resize':
500 width,height = self.loop.screen_size
501 if height<=5 and width<=35:
502 if not 'save_main_widget' in dir(self):
503 self.save_main_widget = self.loop.widget
504 self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !")))
505 else:
506 if 'save_main_widget' in dir(self):
507 self.loop.widget = self.save_main_widget
508 del self.save_main_widget
509 try:
510 return self.menu_roller.check_shortcuts(input_)
511 except AttributeError:
512 return input_
513
514 def add_menus(self, menu, type_filter, menu_data=None):
515 """Add cached menus to instance
516 @param menu: sat_widgets.Menu instance
517 @param type_filter: menu type like is sat.core.sat_main.import_menu
518 @param menu_data: data to send with these menus
519
520 """
521 def add_menu_cb(callback_id):
522 self.action_launch(callback_id, menu_data, profile=self.current_profile)
523 for id_, type_, path, path_i18n, extra in self.bridge.menus_get("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
524 if type_ != type_filter:
525 continue
526 if len(path) != 2:
527 raise NotImplementedError("Menu with a path != 2 are not implemented yet")
528 menu.add_menu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
529
530
531 def _build_menu_roller(self):
532 menu = sat_widgets.Menu(self.loop)
533 general = _("General")
534 menu.add_menu(general, _("Connect"), self.on_connect_request)
535 menu.add_menu(general, _("Disconnect"), self.on_disconnect_request)
536 menu.add_menu(general, _("Parameters"), self.on_param)
537 menu.add_menu(general, _("About"), self.on_about_request)
538 menu.add_menu(general, _("Exit"), self.on_exit_request, a_key['APP_QUIT'])
539 menu.add_menu(_("Contacts")) # add empty menu to save the place in the menu order
540 groups = _("Groups")
541 menu.add_menu(groups)
542 menu.add_menu(groups, _("Join room"), self.on_join_room_request, a_key['ROOM_JOIN'])
543 #additionals menus
544 #FIXME: do this in a more generic way (in quickapp)
545 self.add_menus(menu, C.MENU_GLOBAL)
546
547 menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
548 return menu_roller
549
550 def _build_main_widget(self):
551 self.contact_lists_pile = urwid.Pile([])
552 #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
553 self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
554
555 self.editBar = EditBar(self)
556 self.menu_roller = self._build_menu_roller()
557 self.main_widget = LiberviaTUITopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
558 return self.main_widget
559
560 def plugging_profiles(self):
561 self.loop.widget = self._build_main_widget()
562 self.redraw()
563 try:
564 # if a popup arrived before main widget is build, we need to show it now
565 self.show_pop_up(self._early_popup)
566 except AttributeError:
567 pass
568 else:
569 del self._early_popup
570
571 def profile_plugged(self, profile):
572 QuickApp.profile_plugged(self, profile)
573 contact_list = self.widgets.get_or_create_widget(ContactList, None, on_new_widget=None, on_click=self.contact_selected, on_change=lambda w: self.redraw(), profile=profile)
574 self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
575 return contact_list
576
577 def is_hidden(self):
578 """Tells if the frontend window is hidden.
579
580 @return bool
581 """
582 return False # FIXME: implement when necessary
583
584 def alert(self, title, message):
585 """Shortcut method to create an alert message
586
587 Alert will have an "OK" button, which remove it if pressed
588 @param title(unicode): title of the dialog
589 @param message(unicode): body of the dialog
590 @return (urwid_satext.Alert): the created Alert instance
591 """
592 popup = sat_widgets.Alert(title, message)
593 popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
594 self.show_pop_up(popup, width=75, height=20)
595 return popup
596
597 def remove_pop_up(self, widget=None):
598 """Remove current pop-up, and if there is other in queue, show it
599
600 @param widget(None, urwid.Widget): if not None remove this popup from front or queue
601 """
602 # TODO: refactor popup management in a cleaner way
603 # buttons' callback use themselve as first argument, and we never use
604 # a Button directly in a popup, so we consider urwid.Button as None
605 if widget is not None and not isinstance(widget, urwid.Button):
606 if isinstance(self.loop.widget, urwid.Overlay):
607 current_popup = self.loop.widget.top_w
608 if not current_popup == widget:
609 try:
610 self.notif_bar.remove_pop_up(widget)
611 except ValueError:
612 log.warning(u"Trying to remove an unknown widget {}".format(widget))
613 return
614 self.loop.widget = self.main_widget
615 next_popup = self.notif_bar.get_next_popup()
616 if next_popup:
617 #we still have popup to show, we display it
618 self.show_pop_up(next_popup)
619 else:
620 self.redraw()
621
622 def show_pop_up(self, pop_up_widget, width=None, height=None, align='center',
623 valign='middle'):
624 """Show a pop-up window if possible, else put it in queue
625
626 @param pop_up_widget: pop up to show
627 @param width(int, None): width of the popup
628 None to use default
629 @param height(int, None): height of the popup
630 None to use default
631 @param align: same as for [urwid.Overlay]
632 """
633 if width == None:
634 width = 75 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) else 135
635 if height == None:
636 height = 20 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) else 40
637 if not isinstance(self.loop.widget, urwid.Overlay):
638 display_widget = urwid.Overlay(
639 pop_up_widget, self.main_widget, align, width, valign, height)
640 self.loop.widget = display_widget
641 self.redraw()
642 else:
643 self.notif_bar.add_pop_up(pop_up_widget)
644
645 def bar_notify(self, message):
646 """"Notify message to user via notification bar"""
647 self.notif_bar.add_message(message)
648 self.redraw()
649
650 def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
651 if widget is None or widget is not None and widget != self.selected_widget:
652 # we ignore notification if the widget is selected but we can
653 # still do a desktop notification is the X window has not the focus
654 super(LiberviaTUIApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile)
655 # we don't want notifications without message on desktop
656 if message is not None and not self.x_notify.has_focus():
657 if message is None:
658 message = _("{app}: a new event has just happened{entity}").format(
659 app=C.APP_NAME,
660 entity=u' ({})'.format(entity) if entity else '')
661 self.x_notify.send_notification(message)
662
663
664 def new_widget(self, widget, user_action=False):
665 """Method called when a new widget is created
666
667 if suitable, the widget will be displayed
668 @param widget(widget.LiberviaTUIWidget): created widget
669 @param user_action(bool): if True, the widget has been created following an
670 explicit user action. In this case, the widget may get focus immediately
671 """
672 # FIXME: when several widgets are possible (e.g. with :split)
673 # do not replace current widget when self.selected_widget != None
674 if user_action or self.selected_widget is None:
675 self.select_widget(widget)
676
677 def select_widget(self, widget):
678 """Display a widget if possible,
679
680 else add it in the notification bar queue
681 @param widget: BoxWidget
682 """
683 assert len(self.center_part.widget_list)<=2
684 wid_idx = len(self.center_part.widget_list)-1
685 self.center_part.widget_list[wid_idx] = widget
686 try:
687 self.menu_roller.remove_menu(C.MENU_ID_WIDGET)
688 except KeyError:
689 log.debug("No menu to delete")
690 self.selected_widget = widget
691 try:
692 on_selected = self.selected_widget.on_selected
693 except AttributeError:
694 pass
695 else:
696 on_selected()
697 self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
698 self.contact_lists.select(None)
699
700 for wid in self.visible_widgets: # FIXME: check if widgets.get_widgets is not more appropriate
701 if isinstance(wid, Chat):
702 contact_list = self.contact_lists[wid.profile]
703 contact_list.select(wid.target)
704
705 self.redraw()
706
707 def remove_window(self):
708 """Remove window showed on the right column"""
709 #TODO: better Window management than this hack
710 assert len(self.center_part.widget_list) <= 2
711 wid_idx = len(self.center_part.widget_list)-1
712 self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(''))
713 self.center_part.focus_position = 0
714 self.redraw()
715
716 def add_progress(self, pid, message, profile):
717 """Follow a SàT progression
718
719 @param pid: progression id
720 @param message: message to show to identify the progression
721 """
722 self.progress_wid.add(pid, message, profile)
723
724 def set_progress(self, percentage):
725 """Set the progression shown in notification bar"""
726 self.notif_bar.set_progress(percentage)
727
728 def contact_selected(self, contact_list, entity):
729 self.clear_notifs(entity, profile=contact_list.profile)
730 if entity.resource:
731 # we have clicked on a private MUC conversation
732 chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, force_hash = Chat.get_private_hash(contact_list.profile, entity), profile=contact_list.profile)
733 else:
734 chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
735 self.select_widget(chat_widget)
736 self.menu_roller.add_menu(_('Chat menu'), chat_widget.get_menu(), C.MENU_ID_WIDGET)
737
738 def _dialog_ok_cb(self, widget, data):
739 popup, answer_cb, answer_data = data
740 self.remove_pop_up(popup)
741 if answer_cb is not None:
742 answer_cb(True, answer_data)
743
744 def _dialog_cancel_cb(self, widget, data):
745 popup, answer_cb, answer_data = data
746 self.remove_pop_up(popup)
747 if answer_cb is not None:
748 answer_cb(False, answer_data)
749
750 def show_dialog(self, message, title="", type="info", answer_cb = None, answer_data = None):
751 if type == 'info':
752 popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
753 if answer_cb is None:
754 popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
755 elif type == 'error':
756 popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
757 if answer_cb is None:
758 popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
759 elif type == 'yes/no':
760 popup = sat_widgets.ConfirmDialog(message)
761 popup.set_callback('yes', self._dialog_ok_cb, (popup, answer_cb, answer_data))
762 popup.set_callback('no', self._dialog_cancel_cb, (popup, answer_cb, answer_data))
763 else:
764 popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
765 if answer_cb is None:
766 popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
767 log.error(u'unmanaged dialog type: {}'.format(type))
768 self.show_pop_up(popup)
769
770 def dialog_failure(self, failure):
771 """Show a failure that has been returned by an asynchronous bridge method.
772
773 @param failure (defer.Failure): Failure instance
774 """
775 self.alert(failure.classname, failure.message)
776
777 def on_notification(self, notif_bar):
778 """Called when a new notification has been received"""
779 if not isinstance(self.main_widget, LiberviaTUITopWidget):
780 #if we are not in the main configuration, we ignore the notifications bar
781 return
782 if self.notif_bar.can_hide():
783 #No notification left, we can hide the bar
784 self.main_widget.hide('notif_bar')
785 else:
786 self.main_widget.show('notif_bar')
787 self.redraw() # FIXME: invalidate cache in a more efficient way
788
789 def _action_manager_unknown_error(self):
790 self.alert(_("Error"), _(u"Unmanaged action"))
791
792 def room_joined_handler(self, room_jid_s, room_nicks, user_nick, subject, profile):
793 super(LiberviaTUIApp, self).room_joined_handler(room_jid_s, room_nicks, user_nick, subject, profile)
794 # if self.selected_widget is None:
795 # for contact_list in self.widgets.get_widgets(ContactList):
796 # if profile in contact_list.profiles:
797 # contact_list.set_focus(jid.JID(room_jid_s), True)
798
799 def progress_started_handler(self, pid, metadata, profile):
800 super(LiberviaTUIApp, self).progress_started_handler(pid, metadata, profile)
801 self.add_progress(pid, metadata.get('name', _(u'unkown')), profile)
802
803 def progress_finished_handler(self, pid, metadata, profile):
804 log.info(u"Progress {} finished".format(pid))
805 super(LiberviaTUIApp, self).progress_finished_handler(pid, metadata, profile)
806
807 def progress_error_handler(self, pid, err_msg, profile):
808 log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
809 super(LiberviaTUIApp, self).progress_error_handler(pid, err_msg, profile)
810
811
812 ##DIALOGS CALLBACKS##
813 def on_join_room(self, button, edit):
814 self.remove_pop_up()
815 room_jid = jid.JID(edit.get_edit_text())
816 self.bridge.muc_join(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialog_failure)
817
818 #MENU EVENTS#
819 def on_connect_request(self, menu):
820 QuickApp.connect(self, self.current_profile)
821
822 def on_disconnect_request(self, menu):
823 self.disconnect(self.current_profile)
824
825 def on_param(self, menu):
826 def success(params):
827 ui = xmlui.create(self, xml_data=params, profile=self.current_profile)
828 ui.show()
829
830 def failure(error):
831 self.alert(_("Error"), _("Can't get parameters (%s)") % error)
832 self.bridge.param_ui_get(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
833
834 def on_exit_request(self, menu):
835 QuickApp.on_exit(self)
836 try:
837 if self._bracketed_mode_set: # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf)
838 log.debug("unsetting bracketed paste mode")
839 sys.stdout.write("\033[?2004l")
840 except AttributeError:
841 pass
842 raise urwid.ExitMainLoop()
843
844 def on_join_room_request(self, menu):
845 """User wants to join a MUC room"""
846 pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt=self.bridge.muc_get_default_service(), ok_cb=self.on_join_room)
847 pop_up_widget.set_callback('cancel', lambda dummy: self.remove_pop_up(pop_up_widget))
848 self.show_pop_up(pop_up_widget)
849
850 def on_about_request(self, menu):
851 self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get())
852
853 #MISC CALLBACKS#
854
855 def set_presence_status(self, show='', status=None, profile=C.PROF_KEY_NONE):
856 contact_list_wid = self.widgets.get_widget(ContactList, profiles=profile)
857 if contact_list_wid is not None:
858 contact_list_wid.status_bar.set_presence_status(show, status)
859 else:
860 log.warning(u"No ContactList widget found for profile {}".format(profile))
861
862 if __name__ == '__main__':
863 LiberviaTUIApp().start()