Mercurial > libervia-backend
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() |