# HG changeset patch # User Goffi # Date 1685715925 -7200 # Node ID b620a8e882e1ec9fb75fb6c13356dcea4f189950 # Parent 47401850dec65e68beddf18a204b7e487526fe8a refactoring: rename `libervia.frontends.primitivus` to `libervia.tui` diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/__init__.py diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/base.py --- a/libervia/frontends/primitivus/base.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,863 +0,0 @@ -#!/usr/bin/env python3 - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from libervia.backend.core.i18n import _, D_ -from libervia.frontends.primitivus.constants import Const as C -from libervia.backend.core import log_config -log_config.sat_configure(C.LOG_BACKEND_STANDARD, C) -from libervia.backend.core import log as logging -log = logging.getLogger(__name__) -from libervia.backend.tools import config as sat_config -import urwid -from urwid.util import is_wide_char -from urwid_satext import sat_widgets -from libervia.frontends.quick_frontend.quick_app import QuickApp -from libervia.frontends.quick_frontend import quick_utils -from libervia.frontends.quick_frontend import quick_chat -from libervia.frontends.primitivus.profile_manager import ProfileManager -from libervia.frontends.primitivus.contact_list import ContactList -from libervia.frontends.primitivus.chat import Chat -from libervia.frontends.primitivus import xmlui -from libervia.frontends.primitivus.progress import Progress -from libervia.frontends.primitivus.notify import Notify -from libervia.frontends.primitivus.keys import action_key_map as a_key -from libervia.frontends.primitivus import config -from libervia.frontends.tools.misc import InputHistory -from libervia.backend.tools.common import dynamic_import -from libervia.frontends.tools import jid -import signal -import sys -## bridge handling -# we get bridge name from conf and initialise the right class accordingly -main_config = sat_config.parse_main_conf() -bridge_name = sat_config.config_get(main_config, '', 'bridge', 'dbus') -if 'dbus' not in bridge_name: - print(u"only D-Bus bridge is currently supported") - sys.exit(3) - - -class EditBar(sat_widgets.ModalEdit): - """ - The modal edit bar where you would enter messages and commands. - """ - - def __init__(self, host): - modes = {None: (C.MODE_NORMAL, u''), - a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '), - a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode - super(EditBar, self).__init__(modes) - self.host = host - self.set_completion_method(self._text_completion) - urwid.connect_signal(self, 'click', self.on_text_entered) - - def _text_completion(self, text, completion_data, mode): - if mode == C.MODE_INSERTION: - if self.host.selected_widget is not None: - try: - completion = self.host.selected_widget.completion - except AttributeError: - return text - else: - return completion(text, completion_data) - else: - return text - - def on_text_entered(self, editBar): - """Called when text is entered in the main edit bar""" - if self.mode == C.MODE_INSERTION: - if isinstance(self.host.selected_widget, quick_chat.QuickChat): - chat_widget = self.host.selected_widget - self.host.message_send( - chat_widget.target, - {'': editBar.get_edit_text()}, # TODO: handle language - mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat - errback=lambda failure: self.host.show_dialog(_("Error while sending message ({})").format(failure), type="error"), - profile_key=chat_widget.profile - ) - editBar.set_edit_text('') - elif self.mode == C.MODE_COMMAND: - self.command_handler() - - def command_handler(self): - #TODO: separate class with auto documentation (with introspection) - # and completion method - tokens = self.get_edit_text().split(' ') - command, args = tokens[0], tokens[1:] - if command == 'quit': - self.host.on_exit() - raise urwid.ExitMainLoop() - elif command == 'messages': - wid = sat_widgets.GenericList(logging.memory_get()) - self.host.select_widget(wid) - # FIXME: reactivate the command - # elif command == 'presence': - # values = [value for value in commonConst.PRESENCE.keys()] - # values = [value if value else 'online' for value in values] # the empty value actually means 'online' - # if args and args[0] in values: - # presence = '' if args[0] == 'online' else args[0] - # self.host.status_bar.on_change(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence])) - # else: - # self.host.status_bar.on_presence_click() - # elif command == 'status': - # if args: - # self.host.status_bar.on_change(user_data=sat_widgets.AdvancedEdit(args[0])) - # else: - # self.host.status_bar.on_status_click() - elif command == 'history': - widget = self.host.selected_widget - if isinstance(widget, quick_chat.QuickChat): - try: - limit = int(args[0]) - except (IndexError, ValueError): - limit = 50 - widget.update_history(size=limit, profile=widget.profile) - elif command == 'search': - widget = self.host.selected_widget - if isinstance(widget, quick_chat.QuickChat): - pattern = " ".join(args) - if not pattern: - self.host.notif_bar.add_message(D_("Please specify the globbing pattern to search for")) - else: - widget.update_history(size=C.HISTORY_LIMIT_NONE, filters={'search': pattern}, profile=widget.profile) - elif command == 'filter': - # FIXME: filter is now only for current widget, - # need to be able to set it globally or per widget - widget = self.host.selected_widget - # FIXME: Q&D way, need to be more generic - if isinstance(widget, quick_chat.QuickChat): - widget.set_filter(args) - elif command in ('topic', 'suject', 'title'): - try: - new_title = args[0].strip() - except IndexError: - new_title = None - widget = self.host.selected_widget - if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP: - widget.on_subject_dialog(new_title) - else: - return - self.set_edit_text('') - - def _history_cb(self, text): - self.set_edit_text(text) - self.set_edit_pos(len(text)) - - def keypress(self, size, key): - """Callback when a key is pressed. Send "composing" states - and move the index of the temporary history stack.""" - if key == a_key['MODAL_ESCAPE']: - # first save the text to the current mode, then change to NORMAL - self.host._update_input_history(self.get_edit_text(), mode=self.mode) - self.host._update_input_history(mode=C.MODE_NORMAL) - if self._mode == C.MODE_NORMAL and key in self._modes: - self.host._update_input_history(mode=self._modes[key][0]) - if key == a_key['HISTORY_PREV']: - self.host._update_input_history(self.get_edit_text(), -1, self._history_cb, self.mode) - return - elif key == a_key['HISTORY_NEXT']: - self.host._update_input_history(self.get_edit_text(), +1, self._history_cb, self.mode) - return - elif key == a_key['EDIT_ENTER']: - self.host._update_input_history(self.get_edit_text(), mode=self.mode) - else: - if (self._mode == C.MODE_INSERTION - and isinstance(self.host.selected_widget, quick_chat.QuickChat) - and key not in sat_widgets.FOCUS_KEYS - and key not in (a_key['HISTORY_PREV'], a_key['HISTORY_NEXT']) - and self.host.sync): - self.host.bridge.chat_state_composing(self.host.selected_widget.target, self.host.selected_widget.profile) - - return super(EditBar, self).keypress(size, key) - - -class PrimitivusTopWidget(sat_widgets.FocusPile): - """Top most widget used in Primitivus""" - _focus_inversed = True - positions = ('menu', 'body', 'notif_bar', 'edit_bar') - can_hide = ('menu', 'notif_bar') - - def __init__(self, body, menu, notif_bar, edit_bar): - self._body = body - self._menu = menu - self._notif_bar = notif_bar - self._edit_bar = edit_bar - self._hidden = {'notif_bar'} - self._focus_extra = False - super(PrimitivusTopWidget, self).__init__([('pack', self._menu), self._body, ('pack', self._edit_bar)]) - for position in self.positions: - setattr(self, - position, - property(lambda: self, self.widget_get(position=position), - lambda pos, new_wid: self.widget_set(new_wid, position=pos)) - ) - self.focus_position = len(self.contents)-1 - - def get_visible_positions(self, keep=None): - """Return positions that are not hidden in the right order - - @param keep: if not None, this position will be keep in the right order, even if it's hidden - (can be useful to find its index) - @return (list): list of visible positions - """ - return [pos for pos in self.positions if (keep and pos == keep) or pos not in self._hidden] - - def keypress(self, size, key): - """Manage FOCUS keys that focus directly a main part (one of self.positions) - - To avoid key conflicts, a combinaison must be made with FOCUS_EXTRA then an other key - """ - if key == a_key['FOCUS_EXTRA']: - self._focus_extra = True - return - if self._focus_extra: - self._focus_extra = False - if key in ('m', '1'): - focus = 'menu' - elif key in ('b', '2'): - focus = 'body' - elif key in ('n', '3'): - focus = 'notif_bar' - elif key in ('e', '4'): - focus = 'edit_bar' - else: - return super(PrimitivusTopWidget, self).keypress(size, key) - - if focus in self._hidden: - return - - self.focus_position = self.get_visible_positions().index(focus) - return - - return super(PrimitivusTopWidget, self).keypress(size, key) - - def widget_get(self, position): - if not position in self.positions: - raise ValueError("Unknown position {}".format(position)) - return getattr(self, "_{}".format(position)) - - def widget_set(self, widget, position): - if not position in self.positions: - raise ValueError("Unknown position {}".format(position)) - return setattr(self, "_{}".format(position), widget) - - def hide_switch(self, position): - if not position in self.can_hide: - raise ValueError("Can't switch position {}".format(position)) - hide = not position in self._hidden - widget = self.widget_get(position) - idx = self.get_visible_positions(position).index(position) - if hide: - del self.contents[idx] - self._hidden.add(position) - else: - self.contents.insert(idx, (widget, ('pack', None))) - self._hidden.remove(position) - - def show(self, position): - if position in self._hidden: - self.hide_switch(position) - - def hide(self, position): - if not position in self._hidden: - self.hide_switch(position) - - -class PrimitivusApp(QuickApp, InputHistory): - MB_HANDLER = False - AVATARS_HANDLER = False - - def __init__(self): - bridge_module = dynamic_import.bridge(bridge_name, 'libervia.frontends.bridge') - if bridge_module is None: - log.error(u"Can't import {} bridge".format(bridge_name)) - sys.exit(3) - else: - log.debug(u"Loading {} bridge".format(bridge_name)) - QuickApp.__init__(self, bridge_factory=bridge_module.bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False) - ## main loop setup ## - event_loop = urwid.GLibEventLoop if 'dbus' in bridge_name else urwid.TwistedEventLoop - self.loop = urwid.MainLoop(urwid.SolidFill(), C.PALETTE, event_loop=event_loop(), input_filter=self.input_filter, unhandled_input=self.key_handler) - - @classmethod - def run(cls): - cls().start() - - def on_bridge_connected(self): - - ##misc setup## - self._visible_widgets = set() - self.notif_bar = sat_widgets.NotificationBar() - urwid.connect_signal(self.notif_bar, 'change', self.on_notification) - - self.progress_wid = self.widgets.get_or_create_widget(Progress, None, on_new_widget=None) - urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.select_widget(self.progress_wid)) - self.__saved_overlay = None - - self.x_notify = Notify() - - # we already manage exit with a_key['APP_QUIT'], so we don't want C-c - signal.signal(signal.SIGINT, signal.SIG_IGN) - sat_conf = sat_config.parse_main_conf() - self._bracketed_paste = C.bool( - sat_config.config_get(sat_conf, C.CONFIG_SECTION, 'bracketed_paste', 'false') - ) - if self._bracketed_paste: - log.debug("setting bracketed paste mode as requested") - sys.stdout.write("\033[?2004h") - self._bracketed_mode_set = True - - self.loop.widget = self.main_widget = ProfileManager(self) - self.post_init() - - @property - def visible_widgets(self): - return self._visible_widgets - - @property - def mode(self): - return self.editBar.mode - - @mode.setter - def mode(self, value): - self.editBar.mode = value - - def mode_hint(self, value): - """Change mode if make sens (i.e.: if there is nothing in the editBar)""" - if not self.editBar.get_edit_text(): - self.mode = value - - def debug(self): - """convenient method to reset screen and launch (i)p(u)db""" - log.info('Entered debug mode') - try: - import pudb - pudb.set_trace() - except ImportError: - import os - os.system('reset') - try: - import ipdb - ipdb.set_trace() - except ImportError: - import pdb - pdb.set_trace() - - def redraw(self): - """redraw the screen""" - try: - self.loop.draw_screen() - except AttributeError: - pass - - def start(self): - self.connect_bridge() - self.loop.run() - - def post_init(self): - try: - config.apply_config(self) - except Exception as e: - log.error(u"configuration error: {}".format(e)) - popup = self.alert(_(u"Configuration Error"), _(u"Something went wrong while reading the configuration, please check :messages")) - if self.options.profile: - self._early_popup = popup - else: - self.show_pop_up(popup) - super(PrimitivusApp, self).post_init(self.main_widget) - - def keys_to_text(self, keys): - """Generator return normal text from urwid keys""" - for k in keys: - if k == 'tab': - yield u'\t' - elif k == 'enter': - yield u'\n' - elif is_wide_char(k,0) or (len(k)==1 and ord(k) >= 32): - yield k - - def input_filter(self, input_, raw): - if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']: - return - - ## paste detection/handling - if (len(input_) > 1 and # XXX: it may be needed to increase this value if buffer - not isinstance(input_[0], tuple) and # or other things result in several chars at once - not 'window resize' in input_): # (e.g. using Primitivus through ssh). Need some testing - # and experience to adjust value. - if input_[0] == 'begin paste' and not self._bracketed_paste: - log.info(u"Bracketed paste mode detected") - self._bracketed_paste = True - - if self._bracketed_paste: - # after this block, extra will contain non pasted keys - # and input_ will contain pasted keys - try: - begin_idx = input_.index('begin paste') - except ValueError: - # this is not a paste, maybe we have something buffering - # or bracketed mode is set in conf but not enabled in term - extra = input_ - input_ = [] - else: - try: - end_idx = input_.index('end paste') - except ValueError: - log.warning(u"missing end paste sequence, discarding paste") - extra = input_[:begin_idx] - del input_[begin_idx:] - else: - extra = input_[:begin_idx] + input_[end_idx+1:] - input_ = input_[begin_idx+1:end_idx] - else: - extra = None - - log.debug(u"Paste detected (len {})".format(len(input_))) - try: - edit_bar = self.editBar - except AttributeError: - log.warning(u"Paste treated as normal text: there is no edit bar yet") - if extra is None: - extra = [] - extra.extend(input_) - else: - if self.main_widget.focus == edit_bar: - # XXX: if a paste is detected, we append it directly to the edit bar text - # so the user can check it and press [enter] if it's OK - buf_paste = u''.join(self.keys_to_text(input_)) - pos = edit_bar.edit_pos - edit_bar.set_edit_text(u'{}{}{}'.format(edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:])) - edit_bar.edit_pos+=len(buf_paste) - else: - # we are not on the edit_bar, - # so we treat pasted text as normal text - if extra is None: - extra = [] - extra.extend(input_) - if not extra: - return - input_ = extra - ## end of paste detection/handling - - for i in input_: - if isinstance(i,tuple): - if i[0] == 'mouse press': - if i[1] == 4: #Mouse wheel up - input_[input_.index(i)] = a_key['HISTORY_PREV'] - if i[1] == 5: #Mouse wheel down - input_[input_.index(i)] = a_key['HISTORY_NEXT'] - return input_ - - def key_handler(self, input_): - if input_ == a_key['MENU_HIDE']: - """User want to (un)hide the menu roller""" - try: - self.main_widget.hide_switch('menu') - except AttributeError: - pass - elif input_ == a_key['NOTIFICATION_NEXT']: - """User wants to see next notification""" - self.notif_bar.show_next() - elif input_ == a_key['OVERLAY_HIDE']: - """User wants to (un)hide overlay window""" - if isinstance(self.loop.widget,urwid.Overlay): - self.__saved_overlay = self.loop.widget - self.loop.widget = self.main_widget - else: - if self.__saved_overlay: - self.loop.widget = self.__saved_overlay - self.__saved_overlay = None - - elif input_ == a_key['DEBUG'] and 'D' in self.bridge.version_get(): #Debug only for dev versions - self.debug() - elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists - try: - for wid, options in self.center_part.contents: - if self.contact_lists_pile is wid: - self.center_part.contents.remove((wid, options)) - break - else: - self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False))) - except AttributeError: - #The main widget is not built (probably in Profile Manager) - pass - elif input_ == 'window resize': - width,height = self.loop.screen_size - if height<=5 and width<=35: - if not 'save_main_widget' in dir(self): - self.save_main_widget = self.loop.widget - self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !"))) - else: - if 'save_main_widget' in dir(self): - self.loop.widget = self.save_main_widget - del self.save_main_widget - try: - return self.menu_roller.check_shortcuts(input_) - except AttributeError: - return input_ - - def add_menus(self, menu, type_filter, menu_data=None): - """Add cached menus to instance - @param menu: sat_widgets.Menu instance - @param type_filter: menu type like is sat.core.sat_main.import_menu - @param menu_data: data to send with these menus - - """ - def add_menu_cb(callback_id): - self.action_launch(callback_id, menu_data, profile=self.current_profile) - for id_, type_, path, path_i18n, extra in self.bridge.menus_get("", C.NO_SECURITY_LIMIT ): # TODO: manage extra - if type_ != type_filter: - continue - if len(path) != 2: - raise NotImplementedError("Menu with a path != 2 are not implemented yet") - menu.add_menu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_)) - - - def _build_menu_roller(self): - menu = sat_widgets.Menu(self.loop) - general = _("General") - menu.add_menu(general, _("Connect"), self.on_connect_request) - menu.add_menu(general, _("Disconnect"), self.on_disconnect_request) - menu.add_menu(general, _("Parameters"), self.on_param) - menu.add_menu(general, _("About"), self.on_about_request) - menu.add_menu(general, _("Exit"), self.on_exit_request, a_key['APP_QUIT']) - menu.add_menu(_("Contacts")) # add empty menu to save the place in the menu order - groups = _("Groups") - menu.add_menu(groups) - menu.add_menu(groups, _("Join room"), self.on_join_room_request, a_key['ROOM_JOIN']) - #additionals menus - #FIXME: do this in a more generic way (in quickapp) - self.add_menus(menu, C.MENU_GLOBAL) - - menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)]) - return menu_roller - - def _build_main_widget(self): - self.contact_lists_pile = urwid.Pile([]) - #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))]) - self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))]) - - self.editBar = EditBar(self) - self.menu_roller = self._build_menu_roller() - self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar) - return self.main_widget - - def plugging_profiles(self): - self.loop.widget = self._build_main_widget() - self.redraw() - try: - # if a popup arrived before main widget is build, we need to show it now - self.show_pop_up(self._early_popup) - except AttributeError: - pass - else: - del self._early_popup - - def profile_plugged(self, profile): - QuickApp.profile_plugged(self, profile) - 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) - self.contact_lists_pile.contents.append((contact_list, ('weight', 1))) - return contact_list - - def is_hidden(self): - """Tells if the frontend window is hidden. - - @return bool - """ - return False # FIXME: implement when necessary - - def alert(self, title, message): - """Shortcut method to create an alert message - - Alert will have an "OK" button, which remove it if pressed - @param title(unicode): title of the dialog - @param message(unicode): body of the dialog - @return (urwid_satext.Alert): the created Alert instance - """ - popup = sat_widgets.Alert(title, message) - popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) - self.show_pop_up(popup, width=75, height=20) - return popup - - def remove_pop_up(self, widget=None): - """Remove current pop-up, and if there is other in queue, show it - - @param widget(None, urwid.Widget): if not None remove this popup from front or queue - """ - # TODO: refactor popup management in a cleaner way - # buttons' callback use themselve as first argument, and we never use - # a Button directly in a popup, so we consider urwid.Button as None - if widget is not None and not isinstance(widget, urwid.Button): - if isinstance(self.loop.widget, urwid.Overlay): - current_popup = self.loop.widget.top_w - if not current_popup == widget: - try: - self.notif_bar.remove_pop_up(widget) - except ValueError: - log.warning(u"Trying to remove an unknown widget {}".format(widget)) - return - self.loop.widget = self.main_widget - next_popup = self.notif_bar.get_next_popup() - if next_popup: - #we still have popup to show, we display it - self.show_pop_up(next_popup) - else: - self.redraw() - - def show_pop_up(self, pop_up_widget, width=None, height=None, align='center', - valign='middle'): - """Show a pop-up window if possible, else put it in queue - - @param pop_up_widget: pop up to show - @param width(int, None): width of the popup - None to use default - @param height(int, None): height of the popup - None to use default - @param align: same as for [urwid.Overlay] - """ - if width == None: - width = 75 if isinstance(pop_up_widget, xmlui.PrimitivusNoteDialog) else 135 - if height == None: - height = 20 if isinstance(pop_up_widget, xmlui.PrimitivusNoteDialog) else 40 - if not isinstance(self.loop.widget, urwid.Overlay): - display_widget = urwid.Overlay( - pop_up_widget, self.main_widget, align, width, valign, height) - self.loop.widget = display_widget - self.redraw() - else: - self.notif_bar.add_pop_up(pop_up_widget) - - def bar_notify(self, message): - """"Notify message to user via notification bar""" - self.notif_bar.add_message(message) - self.redraw() - - def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE): - if widget is None or widget is not None and widget != self.selected_widget: - # we ignore notification if the widget is selected but we can - # still do a desktop notification is the X window has not the focus - super(PrimitivusApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile) - # we don't want notifications without message on desktop - if message is not None and not self.x_notify.has_focus(): - if message is None: - message = _("{app}: a new event has just happened{entity}").format( - app=C.APP_NAME, - entity=u' ({})'.format(entity) if entity else '') - self.x_notify.send_notification(message) - - - def new_widget(self, widget, user_action=False): - """Method called when a new widget is created - - if suitable, the widget will be displayed - @param widget(widget.PrimitivusWidget): created widget - @param user_action(bool): if True, the widget has been created following an - explicit user action. In this case, the widget may get focus immediately - """ - # FIXME: when several widgets are possible (e.g. with :split) - # do not replace current widget when self.selected_widget != None - if user_action or self.selected_widget is None: - self.select_widget(widget) - - def select_widget(self, widget): - """Display a widget if possible, - - else add it in the notification bar queue - @param widget: BoxWidget - """ - assert len(self.center_part.widget_list)<=2 - wid_idx = len(self.center_part.widget_list)-1 - self.center_part.widget_list[wid_idx] = widget - try: - self.menu_roller.remove_menu(C.MENU_ID_WIDGET) - except KeyError: - log.debug("No menu to delete") - self.selected_widget = widget - try: - on_selected = self.selected_widget.on_selected - except AttributeError: - pass - else: - on_selected() - self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now - self.contact_lists.select(None) - - for wid in self.visible_widgets: # FIXME: check if widgets.get_widgets is not more appropriate - if isinstance(wid, Chat): - contact_list = self.contact_lists[wid.profile] - contact_list.select(wid.target) - - self.redraw() - - def remove_window(self): - """Remove window showed on the right column""" - #TODO: better Window management than this hack - assert len(self.center_part.widget_list) <= 2 - wid_idx = len(self.center_part.widget_list)-1 - self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text('')) - self.center_part.focus_position = 0 - self.redraw() - - def add_progress(self, pid, message, profile): - """Follow a SàT progression - - @param pid: progression id - @param message: message to show to identify the progression - """ - self.progress_wid.add(pid, message, profile) - - def set_progress(self, percentage): - """Set the progression shown in notification bar""" - self.notif_bar.set_progress(percentage) - - def contact_selected(self, contact_list, entity): - self.clear_notifs(entity, profile=contact_list.profile) - if entity.resource: - # we have clicked on a private MUC conversation - 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) - else: - chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, profile=contact_list.profile) - self.select_widget(chat_widget) - self.menu_roller.add_menu(_('Chat menu'), chat_widget.get_menu(), C.MENU_ID_WIDGET) - - def _dialog_ok_cb(self, widget, data): - popup, answer_cb, answer_data = data - self.remove_pop_up(popup) - if answer_cb is not None: - answer_cb(True, answer_data) - - def _dialog_cancel_cb(self, widget, data): - popup, answer_cb, answer_data = data - self.remove_pop_up(popup) - if answer_cb is not None: - answer_cb(False, answer_data) - - def show_dialog(self, message, title="", type="info", answer_cb = None, answer_data = None): - if type == 'info': - popup = sat_widgets.Alert(title, message, ok_cb=answer_cb) - if answer_cb is None: - popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) - elif type == 'error': - popup = sat_widgets.Alert(title, message, ok_cb=answer_cb) - if answer_cb is None: - popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) - elif type == 'yes/no': - popup = sat_widgets.ConfirmDialog(message) - popup.set_callback('yes', self._dialog_ok_cb, (popup, answer_cb, answer_data)) - popup.set_callback('no', self._dialog_cancel_cb, (popup, answer_cb, answer_data)) - else: - popup = sat_widgets.Alert(title, message, ok_cb=answer_cb) - if answer_cb is None: - popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) - log.error(u'unmanaged dialog type: {}'.format(type)) - self.show_pop_up(popup) - - def dialog_failure(self, failure): - """Show a failure that has been returned by an asynchronous bridge method. - - @param failure (defer.Failure): Failure instance - """ - self.alert(failure.classname, failure.message) - - def on_notification(self, notif_bar): - """Called when a new notification has been received""" - if not isinstance(self.main_widget, PrimitivusTopWidget): - #if we are not in the main configuration, we ignore the notifications bar - return - if self.notif_bar.can_hide(): - #No notification left, we can hide the bar - self.main_widget.hide('notif_bar') - else: - self.main_widget.show('notif_bar') - self.redraw() # FIXME: invalidate cache in a more efficient way - - def _action_manager_unknown_error(self): - self.alert(_("Error"), _(u"Unmanaged action")) - - def room_joined_handler(self, room_jid_s, room_nicks, user_nick, subject, profile): - super(PrimitivusApp, self).room_joined_handler(room_jid_s, room_nicks, user_nick, subject, profile) - # if self.selected_widget is None: - # for contact_list in self.widgets.get_widgets(ContactList): - # if profile in contact_list.profiles: - # contact_list.set_focus(jid.JID(room_jid_s), True) - - def progress_started_handler(self, pid, metadata, profile): - super(PrimitivusApp, self).progress_started_handler(pid, metadata, profile) - self.add_progress(pid, metadata.get('name', _(u'unkown')), profile) - - def progress_finished_handler(self, pid, metadata, profile): - log.info(u"Progress {} finished".format(pid)) - super(PrimitivusApp, self).progress_finished_handler(pid, metadata, profile) - - def progress_error_handler(self, pid, err_msg, profile): - log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg)) - super(PrimitivusApp, self).progress_error_handler(pid, err_msg, profile) - - - ##DIALOGS CALLBACKS## - def on_join_room(self, button, edit): - self.remove_pop_up() - room_jid = jid.JID(edit.get_edit_text()) - self.bridge.muc_join(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialog_failure) - - #MENU EVENTS# - def on_connect_request(self, menu): - QuickApp.connect(self, self.current_profile) - - def on_disconnect_request(self, menu): - self.disconnect(self.current_profile) - - def on_param(self, menu): - def success(params): - ui = xmlui.create(self, xml_data=params, profile=self.current_profile) - ui.show() - - def failure(error): - self.alert(_("Error"), _("Can't get parameters (%s)") % error) - self.bridge.param_ui_get(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure) - - def on_exit_request(self, menu): - QuickApp.on_exit(self) - try: - if self._bracketed_mode_set: # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf) - log.debug("unsetting bracketed paste mode") - sys.stdout.write("\033[?2004l") - except AttributeError: - pass - raise urwid.ExitMainLoop() - - def on_join_room_request(self, menu): - """User wants to join a MUC room""" - 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) - pop_up_widget.set_callback('cancel', lambda dummy: self.remove_pop_up(pop_up_widget)) - self.show_pop_up(pop_up_widget) - - def on_about_request(self, menu): - self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get()) - - #MISC CALLBACKS# - - def set_presence_status(self, show='', status=None, profile=C.PROF_KEY_NONE): - contact_list_wid = self.widgets.get_widget(ContactList, profiles=profile) - if contact_list_wid is not None: - contact_list_wid.status_bar.set_presence_status(show, status) - else: - log.warning(u"No ContactList widget found for profile {}".format(profile)) - -if __name__ == '__main__': - PrimitivusApp().start() diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/chat.py --- a/libervia/frontends/primitivus/chat.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,708 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from functools import total_ordering -from pathlib import Path -import bisect -import urwid -from urwid_satext import sat_widgets -from libervia.backend.core.i18n import _ -from libervia.backend.core import log as logging -from libervia.frontends.quick_frontend import quick_widgets -from libervia.frontends.quick_frontend import quick_chat -from libervia.frontends.quick_frontend import quick_games -from libervia.frontends.primitivus import game_tarot -from libervia.frontends.primitivus.constants import Const as C -from libervia.frontends.primitivus.keys import action_key_map as a_key -from libervia.frontends.primitivus.widget import PrimitivusWidget -from libervia.frontends.primitivus.contact_list import ContactList - - -log = logging.getLogger(__name__) - - -OCCUPANTS_FOOTER = _("{} occupants") - - -class MessageWidget(urwid.WidgetWrap, quick_chat.MessageWidget): - def __init__(self, mess_data): - """ - @param mess_data(quick_chat.Message, None): message data - None: used only for non text widgets (e.g.: focus separator) - """ - self.mess_data = mess_data - mess_data.widgets.add(self) - super(MessageWidget, self).__init__(urwid.Text(self.markup)) - - @property - def markup(self): - return ( - self._generate_info_markup() - if self.mess_data.type == C.MESS_TYPE_INFO - else self._generate_markup() - ) - - @property - def info_type(self): - return self.mess_data.info_type - - @property - def parent(self): - return self.mess_data.parent - - @property - def message(self): - """Return currently displayed message""" - return self.mess_data.main_message - - @message.setter - def message(self, value): - self.mess_data.message = {"": value} - self.redraw() - - @property - def type(self): - try: - return self.mess_data.type - except AttributeError: - return C.MESS_TYPE_INFO - - def redraw(self): - self._w.set_text(self.markup) - self.mess_data.parent.host.redraw() # FIXME: should not be necessary - - def selectable(self): - return True - - def keypress(self, size, key): - return key - - def get_cursor_coords(self, size): - return 0, 0 - - def render(self, size, focus=False): - # Text widget doesn't render cursor, but we want one - # so we add it here - canvas = urwid.CompositeCanvas(self._w.render(size, focus)) - if focus: - canvas.set_cursor(self.get_cursor_coords(size)) - return canvas - - def _generate_info_markup(self): - return ("info_msg", self.message) - - def _generate_markup(self): - """Generate text markup according to message data and Widget options""" - markup = [] - d = self.mess_data - mention = d.mention - - # message status - if d.status is None: - markup.append(" ") - elif d.status == "delivered": - markup.append(("msg_status_received", "✔")) - else: - log.warning("Unknown status: {}".format(d.status)) - - # timestamp - if self.parent.show_timestamp: - attr = "msg_mention" if mention else "date" - markup.append((attr, "[{}]".format(d.time_text))) - else: - if mention: - markup.append(("msg_mention", "[*]")) - - # nickname - if self.parent.show_short_nick: - markup.append( - ("my_nick" if d.own_mess else "other_nick", "**" if d.own_mess else "*") - ) - else: - markup.append( - ("my_nick" if d.own_mess else "other_nick", "[{}] ".format(d.nick or "")) - ) - - msg = self.message # needed to generate self.selected_lang - - if d.selected_lang: - markup.append(("msg_lang", "[{}] ".format(d.selected_lang))) - - # message body - markup.append(msg) - - return markup - - # events - def update(self, update_dict=None): - """update all the linked message widgets - - @param update_dict(dict, None): key=attribute updated value=new_value - """ - self.redraw() - - -@total_ordering -class OccupantWidget(urwid.WidgetWrap): - def __init__(self, occupant_data): - self.occupant_data = occupant_data - occupant_data.widgets.add(self) - markup = self._generate_markup() - text = sat_widgets.ClickableText(markup) - urwid.connect_signal( - text, - "click", - self.occupant_data.parent._occupants_clicked, - user_args=[self.occupant_data], - ) - super(OccupantWidget, self).__init__(text) - - def __hash__(self): - return id(self) - - def __eq__(self, other): - if other is None: - return False - return self.occupant_data.nick == other.occupant_data.nick - - def __lt__(self, other): - return self.occupant_data.nick.lower() < other.occupant_data.nick.lower() - - @property - def markup(self): - return self._generate_markup() - - @property - def parent(self): - return self.mess_data.parent - - @property - def nick(self): - return self.occupant_data.nick - - def redraw(self): - self._w.set_text(self.markup) - self.occupant_data.parent.host.redraw() # FIXME: should not be necessary - - def selectable(self): - return True - - def keypress(self, size, key): - return key - - def get_cursor_coords(self, size): - return 0, 0 - - def render(self, size, focus=False): - # Text widget doesn't render cursor, but we want one - # so we add it here - canvas = urwid.CompositeCanvas(self._w.render(size, focus)) - if focus: - canvas.set_cursor(self.get_cursor_coords(size)) - return canvas - - def _generate_markup(self): - # TODO: role and affiliation are shown in a Q&D way - # should be more intuitive and themable - o = self.occupant_data - markup = [] - markup.append( - ("info_msg", "{}{} ".format(o.role[0].upper(), o.affiliation[0].upper())) - ) - markup.append(o.nick) - if o.state is not None: - markup.append(" {}".format(C.CHAT_STATE_ICON[o.state])) - return markup - - # events - def update(self, update_dict=None): - self.redraw() - - -class OccupantsWidget(urwid.WidgetWrap): - def __init__(self, parent): - self.parent = parent - self.occupants_walker = urwid.SimpleListWalker([]) - self.occupants_footer = urwid.Text("", align="center") - self.update_footer() - occupants_widget = urwid.Frame( - urwid.ListBox(self.occupants_walker), footer=self.occupants_footer - ) - super(OccupantsWidget, self).__init__(occupants_widget) - occupants_list = sorted(list(self.parent.occupants.keys()), key=lambda o: o.lower()) - for occupant in occupants_list: - occupant_data = self.parent.occupants[occupant] - self.occupants_walker.append(OccupantWidget(occupant_data)) - - def clear(self): - del self.occupants_walker[:] - - def update_footer(self): - """update footer widget""" - txt = OCCUPANTS_FOOTER.format(len(self.parent.occupants)) - self.occupants_footer.set_text(txt) - - def get_nicks(self, start=""): - """Return nicks of all occupants - - @param start(unicode): only return nicknames which start with this text - """ - return [ - w.nick - for w in self.occupants_walker - if isinstance(w, OccupantWidget) and w.nick.startswith(start) - ] - - def addUser(self, occupant_data): - """add a user to the list""" - bisect.insort(self.occupants_walker, OccupantWidget(occupant_data)) - self.update_footer() - self.parent.host.redraw() # FIXME: should not be necessary - - def removeUser(self, occupant_data): - """remove a user from the list""" - for widget in occupant_data.widgets: - self.occupants_walker.remove(widget) - self.update_footer() - self.parent.host.redraw() # FIXME: should not be necessary - - -class Chat(PrimitivusWidget, quick_chat.QuickChat): - def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, - subject=None, statuses=None, profiles=None): - self.filters = [] # list of filter callbacks to apply - self.mess_walker = urwid.SimpleListWalker([]) - self.mess_widgets = urwid.ListBox(self.mess_walker) - self.chat_widget = urwid.Frame(self.mess_widgets) - self.chat_colums = urwid.Columns([("weight", 8, self.chat_widget)]) - self.pile = urwid.Pile([self.chat_colums]) - PrimitivusWidget.__init__(self, self.pile, target) - quick_chat.QuickChat.__init__( - self, host, target, type_, nick, occupants, subject, statuses, - profiles=profiles - ) - - # we must adapt the behaviour with the type - if type_ == C.CHAT_GROUP: - if len(self.chat_colums.contents) == 1: - self.occupants_widget = OccupantsWidget(self) - self.occupants_panel = sat_widgets.VerticalSeparator( - self.occupants_widget - ) - self._append_occupants_panel() - self.host.addListener("presence", self.presence_listener, [profiles]) - - # focus marker is a separator indicated last visible message before focus was lost - self.focus_marker = None # link to current marker - self.focus_marker_set = None # True if a new marker has been inserted - self.show_timestamp = True - self.show_short_nick = False - self.show_title = 1 # 0: clip title; 1: full title; 2: no title - self.post_init() - - @property - def message_widgets_rev(self): - return reversed(self.mess_walker) - - def keypress(self, size, key): - if key == a_key["OCCUPANTS_HIDE"]: # user wants to (un)hide the occupants panel - if self.type == C.CHAT_GROUP: - widgets = [widget for (widget, options) in self.chat_colums.contents] - if self.occupants_panel in widgets: - self._remove_occupants_panel() - else: - self._append_occupants_panel() - elif key == a_key["TIMESTAMP_HIDE"]: # user wants to (un)hide timestamp - self.show_timestamp = not self.show_timestamp - self.redraw() - elif key == a_key["SHORT_NICKNAME"]: # user wants to (not) use short nick - self.show_short_nick = not self.show_short_nick - self.redraw() - elif (key == a_key["SUBJECT_SWITCH"]): - # user wants to (un)hide group's subject or change its apperance - if self.subject: - self.show_title = (self.show_title + 1) % 3 - if self.show_title == 0: - self.set_subject(self.subject, "clip") - elif self.show_title == 1: - self.set_subject(self.subject, "space") - elif self.show_title == 2: - self.chat_widget.header = None - self._invalidate() - elif key == a_key["GOTO_BOTTOM"]: # user wants to focus last message - self.mess_widgets.focus_position = len(self.mess_walker) - 1 - - return super(Chat, self).keypress(size, key) - - def completion(self, text, completion_data): - """Completion method which complete nicknames in group chat - - for params, see [sat_widgets.AdvancedEdit] - """ - if self.type != C.CHAT_GROUP: - return text - - space = text.rfind(" ") - start = text[space + 1 :] - words = self.occupants_widget.get_nicks(start) - if not words: - return text - try: - word_idx = words.index(completion_data["last_word"]) + 1 - except (KeyError, ValueError): - word_idx = 0 - else: - if word_idx == len(words): - word_idx = 0 - word = completion_data["last_word"] = words[word_idx] - return "{}{}{}".format(text[: space + 1], word, ": " if space < 0 else "") - - def get_menu(self): - """Return Menu bar""" - menu = sat_widgets.Menu(self.host.loop) - if self.type == C.CHAT_GROUP: - self.host.add_menus(menu, C.MENU_ROOM, {"room_jid": self.target.bare}) - game = _("Game") - menu.add_menu(game, "Tarot", self.on_tarot_request) - elif self.type == C.CHAT_ONE2ONE: - # FIXME: self.target is a bare jid, we need to check that - contact_list = self.host.contact_lists[self.profile] - if not self.target.resource: - full_jid = contact_list.get_full_jid(self.target) - else: - full_jid = self.target - self.host.add_menus(menu, C.MENU_SINGLE, {"jid": full_jid}) - return menu - - def set_filter(self, args): - """set filtering of messages - - @param args(list[unicode]): filters following syntax "[filter]=[value]" - empty list to clear all filters - only lang=XX is handled for now - """ - del self.filters[:] - if args: - if args[0].startswith("lang="): - lang = args[0][5:].strip() - self.filters.append(lambda mess_data: lang in mess_data.message) - - self.print_messages() - - def presence_listener(self, entity, show, priority, statuses, profile): - """Update entity's presence status - - @param entity (jid.JID): entity updated - @param show: availability - @param priority: resource's priority - @param statuses: dict of statuses - @param profile: %(doc_profile)s - """ - # FIXME: disable for refactoring, need to be checked and re-enabled - return - # assert self.type == C.CHAT_GROUP - # if entity.bare != self.target: - # return - # self.update(entity) - - def create_message(self, message): - self.appendMessage(message) - - def _scrollDown(self): - """scroll down message only if we are already at the bottom (minus 1)""" - current_focus = self.mess_widgets.focus_position - bottom = len(self.mess_walker) - 1 - if current_focus == bottom - 1: - self.mess_widgets.focus_position = bottom # scroll down - self.host.redraw() # FIXME: should not be necessary - - def appendMessage(self, message, minor_notifs=True): - """Create a MessageWidget and append it - - Can merge info messages together if desirable (e.g.: multiple joined/leave) - @param message(quick_chat.Message): message to add - @param minor_notifs(boolean): if True, basic notifications are allowed - If False, notification are not shown except if we have an important one - (like a mention). - False is generally used when printing history, when we don't want every - message to be notified. - """ - if message.attachments: - # FIXME: Q&D way to see attachments in Primitivus - # it should be done in a more user friendly way - for lang, body in message.message.items(): - for attachment in message.attachments: - if 'url' in attachment: - body+=f"\n{attachment['url']}" - elif 'path' in attachment: - path = Path(attachment['path']) - body+=f"\n{path.as_uri()}" - else: - log.warning(f'No "url" nor "path" in attachment: {attachment}') - message.message[lang] = body - - if self.filters: - if not all([f(message) for f in self.filters]): - return - - if self.handle_user_moved(message): - return - - if ((self.host.selected_widget != self or not self.host.x_notify.has_focus()) - and self.focus_marker_set is not None): - if not self.focus_marker_set and not self._locked and self.mess_walker: - if self.focus_marker is not None: - try: - self.mess_walker.remove(self.focus_marker) - except ValueError: - # self.focus_marker may not be in mess_walker anymore if - # mess_walker has been cleared, e.g. when showing search - # result or using :history command - pass - self.focus_marker = urwid.Divider("—") - self.mess_walker.append(self.focus_marker) - self.focus_marker_set = True - self._scrollDown() - else: - if self.focus_marker_set: - self.focus_marker_set = False - - wid = MessageWidget(message) - self.mess_walker.append(wid) - self._scrollDown() - if self.is_user_moved(message): - return # no notification for moved messages - - # notifications - - if self._locked: - # we don't want notifications when locked - # because that's history messages - return - - if wid.mess_data.mention: - from_jid = wid.mess_data.from_jid - msg = _( - "You have been mentioned by {nick} in {room}".format( - nick=wid.mess_data.nick, room=self.target - ) - ) - self.host.notify( - C.NOTIFY_MENTION, from_jid, msg, widget=self, profile=self.profile - ) - elif not minor_notifs: - return - elif self.type == C.CHAT_ONE2ONE: - from_jid = wid.mess_data.from_jid - msg = _("{entity} is talking to you".format(entity=from_jid)) - self.host.notify( - C.NOTIFY_MESSAGE, from_jid, msg, widget=self, profile=self.profile - ) - else: - self.host.notify( - C.NOTIFY_MESSAGE, self.target, widget=self, profile=self.profile - ) - - def addUser(self, nick): - occupant = super(Chat, self).addUser(nick) - self.occupants_widget.addUser(occupant) - - def removeUser(self, occupant_data): - occupant = super(Chat, self).removeUser(occupant_data) - if occupant is not None: - self.occupants_widget.removeUser(occupant) - - def occupants_clear(self): - super(Chat, self).occupants_clear() - self.occupants_widget.clear() - - def _occupants_clicked(self, occupant, clicked_wid): - assert self.type == C.CHAT_GROUP - contact_list = self.host.contact_lists[self.profile] - - # we have a click on a nick, we need to create the widget if it doesn't exists - self.get_or_create_private_widget(occupant.jid) - - # now we select the new window - for contact_list in self.host.widgets.get_widgets( - ContactList, profiles=(self.profile,) - ): - contact_list.set_focus(occupant.jid, True) - - def _append_occupants_panel(self): - self.chat_colums.contents.append((self.occupants_panel, ("weight", 2, False))) - - def _remove_occupants_panel(self): - for widget, options in self.chat_colums.contents: - if widget is self.occupants_panel: - self.chat_colums.contents.remove((widget, options)) - break - - def add_game_panel(self, widget): - """Insert a game panel to this Chat dialog. - - @param widget (Widget): the game panel - """ - assert len(self.pile.contents) == 1 - self.pile.contents.insert(0, (widget, ("weight", 1))) - self.pile.contents.insert(1, (urwid.Filler(urwid.Divider("-"), ("fixed", 1)))) - self.host.redraw() - - def remove_game_panel(self, widget): - """Remove the game panel from this Chat dialog. - - @param widget (Widget): the game panel - """ - assert len(self.pile.contents) == 3 - del self.pile.contents[0] - self.host.redraw() - - def set_subject(self, subject, wrap="space"): - """Set title for a group chat""" - quick_chat.QuickChat.set_subject(self, subject) - self.subj_wid = urwid.Text( - str(subject.replace("\n", "|") if wrap == "clip" else subject), - align="left" if wrap == "clip" else "center", - wrap=wrap, - ) - self.chat_widget.header = urwid.AttrMap(self.subj_wid, "title") - self.host.redraw() - - ## Messages - - def print_messages(self, clear=True): - """generate message widgets - - @param clear(bool): clear message before printing if true - """ - if clear: - del self.mess_walker[:] - for message in self.messages.values(): - self.appendMessage(message, minor_notifs=False) - - def redraw(self): - """redraw all messages""" - for w in self.mess_walker: - try: - w.redraw() - except AttributeError: - pass - - def update_history(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile="@NONE@"): - del self.mess_walker[:] - if filters and "search" in filters: - self.mess_walker.append( - urwid.Text( - _("Results for searching the globbing pattern: {}").format( - filters["search"] - ) - ) - ) - self.mess_walker.append( - urwid.Text(_("Type ':history ' to reset the chat history")) - ) - super(Chat, self).update_history(size, filters, profile) - - def _on_history_printed(self): - """Refresh or scroll down the focus after the history is printed""" - self.print_messages(clear=False) - super(Chat, self)._on_history_printed() - - def on_private_created(self, widget): - self.host.contact_lists[widget.profile].set_special( - widget.target, C.CONTACT_SPECIAL_GROUP - ) - - def on_selected(self): - self.focus_marker_set = False - - def notify(self, contact="somebody", msg=""): - """Notify the user of a new message if primitivus doesn't have the focus. - - @param contact (unicode): contact who wrote to the users - @param msg (unicode): the message that has been received - """ - # FIXME: not called anymore after refactoring - if msg == "": - return - if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2: - # we don't change focus if user is not at the bottom - # as that mean that he is probably watching discussion history - self.mess_widgets.focus_position = len(self.mess_walker) - 1 - self.host.redraw() - if not self.host.x_notify.has_focus(): - if self.type == C.CHAT_ONE2ONE: - self.host.x_notify.send_notification( - _("Primitivus: %s is talking to you") % contact - ) - elif self.nick is not None and self.nick.lower() in msg.lower(): - self.host.x_notify.send_notification( - _("Primitivus: %(user)s mentioned you in room '%(room)s'") - % {"user": contact, "room": self.target} - ) - - # MENU EVENTS # - def on_tarot_request(self, menu): - # TODO: move this to plugin_misc_tarot with dynamic menu - if len(self.occupants) != 4: - self.host.show_pop_up( - sat_widgets.Alert( - _("Can't start game"), - _( - "You need to be exactly 4 peoples in the room to start a Tarot game" - ), - ok_cb=self.host.remove_pop_up, - ) - ) - else: - self.host.bridge.tarot_game_create( - self.target, list(self.occupants), self.profile - ) - - # MISC EVENTS # - - def on_delete(self): - # FIXME: to be checked after refactoring - super(Chat, self).on_delete() - if self.type == C.CHAT_GROUP: - self.host.removeListener("presence", self.presence_listener) - - def on_chat_state(self, from_jid, state, profile): - super(Chat, self).on_chat_state(from_jid, state, profile) - if self.type == C.CHAT_ONE2ONE: - self.title_dynamic = C.CHAT_STATE_ICON[state] - self.host.redraw() # FIXME: should not be necessary - - def _on_subject_dialog_cb(self, button, dialog): - self.change_subject(dialog.text) - self.host.remove_pop_up(dialog) - - def on_subject_dialog(self, new_subject=None): - dialog = sat_widgets.InputDialog( - _("Change title"), - _("Enter the new title"), - default_txt=new_subject if new_subject is not None else self.subject, - ) - dialog.set_callback("ok", self._on_subject_dialog_cb, dialog) - dialog.set_callback("cancel", lambda __: self.host.remove_pop_up(dialog)) - self.host.show_pop_up(dialog) - - -quick_widgets.register(quick_chat.QuickChat, Chat) -quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame) diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/config.py --- a/libervia/frontends/primitivus/config.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""This module manage configuration specific to Primitivus""" - -from libervia.frontends.primitivus.constants import Const as C -from libervia.frontends.primitivus.keys import action_key_map -import configparser - - -def apply_config(host): - """Parse configuration and apply found change - - raise: can raise various Exceptions if configuration is not good - """ - config = configparser.SafeConfigParser() - config.read(C.CONFIG_FILES) - try: - options = config.items(C.CONFIG_SECTION) - except configparser.NoSectionError: - options = [] - shortcuts = {} - for name, value in options: - if name.startswith(C.CONFIG_OPT_KEY_PREFIX.lower()): - action = name[len(C.CONFIG_OPT_KEY_PREFIX) :].upper() - shortcut = value - if not action or not shortcut: - raise ValueError("Bad option: {} = {}".format(name, value)) - shortcuts[action] = shortcut - if name == "disable_mouse": - host.loop.screen.set_mouse_tracking(False) - - action_key_map.replace(shortcuts) - action_key_map.check_namespaces() diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/constants.py --- a/libervia/frontends/primitivus/constants.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.frontends.quick_frontend import constants - - -class Const(constants.Const): - - APP_NAME = "Libervia TUI" - APP_COMPONENT = "TUI" - APP_NAME_ALT = "Primitivus" - APP_NAME_FILE = "libervia_tui" - CONFIG_SECTION = APP_COMPONENT.lower() - PALETTE = [ - ("title", "black", "light gray", "standout,underline"), - ("title_focus", "white,bold", "light gray", "standout,underline"), - ("selected", "default", "dark red"), - ("selected_focus", "default,bold", "dark red"), - ("default", "default", "default"), - ("default_focus", "default,bold", "default"), - ("cl_notifs", "yellow", "default"), - ("cl_notifs_focus", "yellow,bold", "default"), - ("cl_mention", "light red", "default"), - ("cl_mention_focus", "dark red,bold", "default"), - # Messages - ("date", "light gray", "default"), - ("my_nick", "dark red,bold", "default"), - ("other_nick", "dark cyan,bold", "default"), - ("info_msg", "yellow", "default", "bold"), - ("msg_lang", "dark cyan", "default"), - ("msg_mention", "dark red, bold", "default"), - ("msg_status_received", "light green, bold", "default"), - ("menubar", "light gray,bold", "dark red"), - ("menubar_focus", "light gray,bold", "dark green"), - ("selected_menu", "light gray,bold", "dark green"), - ("menuitem", "light gray,bold", "dark red"), - ("menuitem_focus", "light gray,bold", "dark green"), - ("notifs", "black,bold", "yellow"), - ("notifs_focus", "dark red", "yellow"), - ("card_neutral", "dark gray", "white", "standout,underline"), - ("card_neutral_selected", "dark gray", "dark green", "standout,underline"), - ("card_special", "brown", "white", "standout,underline"), - ("card_special_selected", "brown", "dark green", "standout,underline"), - ("card_red", "dark red", "white", "standout,underline"), - ("card_red_selected", "dark red", "dark green", "standout,underline"), - ("card_black", "black", "white", "standout,underline"), - ("card_black_selected", "black", "dark green", "standout,underline"), - ("directory", "dark cyan, bold", "default"), - ("directory_focus", "dark cyan, bold", "dark green"), - ("separator", "brown", "default"), - ("warning", "light red", "default"), - ("progress_normal", "default", "brown"), - ("progress_complete", "default", "dark green"), - ("show_disconnected", "dark gray", "default"), - ("show_normal", "default", "default"), - ("show_normal_focus", "default, bold", "default"), - ("show_chat", "dark green", "default"), - ("show_chat_focus", "dark green, bold", "default"), - ("show_away", "brown", "default"), - ("show_away_focus", "brown, bold", "default"), - ("show_dnd", "dark red", "default"), - ("show_dnd_focus", "dark red, bold", "default"), - ("show_xa", "dark red", "default"), - ("show_xa_focus", "dark red, bold", "default"), - ("resource", "light blue", "default"), - ("resource_main", "dark blue", "default"), - ("status", "yellow", "default"), - ("status_focus", "yellow, bold", "default"), - ("param_selected", "default, bold", "dark red"), - ("table_selected", "default, bold", "default"), - ] - PRESENCE = { - "unavailable": ("⨯", "show_disconnected"), - "": ("✔", "show_normal"), - "chat": ("✆", "show_chat"), - "away": ("✈", "show_away"), - "dnd": ("✖", "show_dnd"), - "xa": ("☄", "show_xa"), - } - LOG_OPT_SECTION = APP_NAME.lower() - LOG_OPT_OUTPUT = ( - "output", - constants.Const.LOG_OPT_OUTPUT_SEP + constants.Const.LOG_OPT_OUTPUT_MEMORY, - ) - CONFIG_OPT_KEY_PREFIX = "KEY_" - - MENU_ID_MAIN = "MAIN_MENU" - MENU_ID_WIDGET = "WIDGET_MENU" - - MODE_NORMAL = "NORMAL" - MODE_INSERTION = "INSERTION" - MODE_COMMAND = "COMMAND" - - GROUP_DATA_FOLDED = "folded" diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/contact_list.py --- a/libervia/frontends/primitivus/contact_list.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,364 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -import urwid -from urwid_satext import sat_widgets -from libervia.frontends.quick_frontend.quick_contact_list import QuickContactList -from libervia.frontends.primitivus.status import StatusBar -from libervia.frontends.primitivus.constants import Const as C -from libervia.frontends.primitivus.keys import action_key_map as a_key -from libervia.frontends.primitivus.widget import PrimitivusWidget -from libervia.frontends.tools import jid -from libervia.backend.core import log as logging - -log = logging.getLogger(__name__) -from libervia.frontends.quick_frontend import quick_widgets - - -class ContactList(PrimitivusWidget, QuickContactList): - PROFILES_MULTIPLE = False - PROFILES_ALLOW_NONE = False - signals = ["click", "change"] - # FIXME: Only single profile is managed so far - - def __init__( - self, host, target, on_click=None, on_change=None, user_data=None, profiles=None - ): - QuickContactList.__init__(self, host, profiles) - self.contact_list = self.host.contact_lists[self.profile] - - # we now build the widget - self.status_bar = StatusBar(host) - self.frame = sat_widgets.FocusFrame(self._build_list(), None, self.status_bar) - PrimitivusWidget.__init__(self, self.frame, _("Contacts")) - if on_click: - urwid.connect_signal(self, "click", on_click, user_data) - if on_change: - urwid.connect_signal(self, "change", on_change, user_data) - self.host.addListener("notification", self.on_notification, [self.profile]) - self.host.addListener("notificationsClear", self.on_notification, [self.profile]) - self.post_init() - - def update(self, entities=None, type_=None, profile=None): - """Update display, keep focus""" - # FIXME: full update is done each time, must handle entities, type_ and profile - widget, position = self.frame.body.get_focus() - self.frame.body = self._build_list() - if position: - try: - self.frame.body.focus_position = position - except IndexError: - pass - self._invalidate() - self.host.redraw() # FIXME: check if can be avoided - - def keypress(self, size, key): - # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent, - # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element - if key in sat_widgets.FOCUS_KEYS: - if ( - key == a_key["FOCUS_SWITCH"] - or (key == a_key["FOCUS_UP"] and self.frame.focus_position == "body") - or (key == a_key["FOCUS_DOWN"] and self.frame.focus_position == "footer") - ): - return key - if key == a_key["STATUS_HIDE"]: # user wants to (un)hide contacts' statuses - self.contact_list.show_status = not self.contact_list.show_status - self.update() - elif ( - key == a_key["DISCONNECTED_HIDE"] - ): # user wants to (un)hide disconnected contacts - self.host.bridge.param_set( - C.SHOW_OFFLINE_CONTACTS, - C.bool_const(not self.contact_list.show_disconnected), - "General", - profile_key=self.profile, - ) - elif key == a_key["RESOURCES_HIDE"]: # user wants to (un)hide contacts resources - self.contact_list.show_resources(not self.contact_list.show_resources) - self.update() - return super(ContactList, self).keypress(size, key) - - # QuickWidget methods - - @staticmethod - def get_widget_hash(target, profiles): - profiles = sorted(profiles) - return tuple(profiles) - - # modify the contact list - - def set_focus(self, text, select=False): - """give focus to the first element that matches the given text. You can also - pass in text a libervia.frontends.tools.jid.JID (it's a subclass of unicode). - - @param text: contact group name, contact or muc userhost, muc private dialog jid - @param select: if True, the element is also clicked - """ - idx = 0 - for widget in self.frame.body.body: - try: - if isinstance(widget, sat_widgets.ClickableText): - # contact group - value = widget.get_value() - elif isinstance(widget, sat_widgets.SelectableText): - # contact or muc - value = widget.data - else: - # Divider instance - continue - # there's sometimes a leading space - if text.strip() == value.strip(): - self.frame.body.focus_position = idx - if select: - self._contact_clicked(False, widget, True) - return - except AttributeError: - pass - idx += 1 - - log.debug("Not element found for {} in set_focus".format(text)) - - # events - - def _group_clicked(self, group_wid): - group = group_wid.get_value() - data = self.contact_list.get_group_data(group) - data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False) - self.set_focus(group) - self.update() - - def _contact_clicked(self, use_bare_jid, contact_wid, selected): - """Method called when a contact is clicked - - @param use_bare_jid: True if use_bare_jid is set in self._build_entity_widget. - @param contact_wid: widget of the contact, must have the entity set in data attribute - @param selected: boolean returned by the widget, telling if it is selected - """ - entity = contact_wid.data - self.host.mode_hint(C.MODE_INSERTION) - self._emit("click", entity) - - def on_notification(self, entity, notif, profile): - notifs = list(self.host.get_notifs(C.ENTITY_ALL, profile=self.profile)) - if notifs: - self.title_dynamic = "({})".format(len(notifs)) - else: - self.title_dynamic = None - self.host.redraw() # FIXME: should not be necessary - - # Methods to build the widget - - def _build_entity_widget( - self, - entity, - keys=None, - use_bare_jid=False, - with_notifs=True, - with_show_attr=True, - markup_prepend=None, - markup_append=None, - special=False, - ): - """Build one contact markup data - - @param entity (jid.JID): entity to build - @param keys (iterable): value to markup, in preferred order. - The first available key will be used. - If key starts with "cache_", it will be checked in cache, - else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')). - If nothing full or keys is None, full entity is used. - @param use_bare_jid (bool): if True, use bare jid for selected comparisons - @param with_notifs (bool): if True, show notification count - @param with_show_attr (bool): if True, show color corresponding to presence status - @param markup_prepend (list): markup to prepend to the generated one before building the widget - @param markup_append (list): markup to append to the generated one before building the widget - @param special (bool): True if entity is a special one - @return (list): markup data are expected by Urwid text widgets - """ - markup = [] - if use_bare_jid: - selected = {entity.bare for entity in self.contact_list._selected} - else: - selected = self.contact_list._selected - if keys is None: - entity_txt = entity - else: - cache = self.contact_list.getCache(entity) - for key in keys: - if key.startswith("cache_"): - entity_txt = cache.get(key[6:]) - else: - entity_txt = getattr(entity, key) - if entity_txt: - break - if not entity_txt: - entity_txt = entity - - if with_show_attr: - show = self.contact_list.getCache(entity, C.PRESENCE_SHOW, default=None) - if show is None: - show = C.PRESENCE_UNAVAILABLE - show_icon, entity_attr = C.PRESENCE.get(show, ("", "default")) - markup.insert(0, "{} ".format(show_icon)) - else: - entity_attr = "default" - - notifs = list( - self.host.get_notifs(entity, exact_jid=special, profile=self.profile) - ) - mentions = list( - self.host.get_notifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile) - ) - if notifs or mentions: - attr = 'cl_mention' if mentions else 'cl_notifs' - header = [(attr, "({})".format(len(notifs) + len(mentions))), " "] - else: - header = "" - - markup.append((entity_attr, entity_txt)) - if markup_prepend: - markup.insert(0, markup_prepend) - if markup_append: - markup.extend(markup_append) - - widget = sat_widgets.SelectableText( - markup, selected=entity in selected, header=header - ) - widget.data = entity - widget.comp = entity_txt.lower() # value to use for sorting - urwid.connect_signal( - widget, "change", self._contact_clicked, user_args=[use_bare_jid] - ) - return widget - - def _build_entities(self, content, entities): - """Add entity representation in widget list - - @param content: widget list, e.g. SimpleListWalker - @param entities (iterable): iterable of JID to display - """ - if not entities: - return - widgets = [] # list of built widgets - - for entity in entities: - if ( - entity in self.contact_list._specials - or not self.contact_list.entity_visible(entity) - ): - continue - markup_extra = [] - if self.contact_list.show_resources: - for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES): - resource_disp = ( - "resource_main" - if resource - == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE) - else "resource", - "\n " + resource, - ) - markup_extra.append(resource_disp) - if self.contact_list.show_status: - status = self.contact_list.getCache( - jid.JID("%s/%s" % (entity, resource)), "status", default=None - ) - status_disp = ("status", "\n " + status) if status else "" - markup_extra.append(status_disp) - - else: - if self.contact_list.show_status: - status = self.contact_list.getCache(entity, "status", default=None) - status_disp = ("status", "\n " + status) if status else "" - markup_extra.append(status_disp) - widget = self._build_entity_widget( - entity, - ("cache_nick", "cache_name", "node"), - use_bare_jid=True, - markup_append=markup_extra, - ) - widgets.append(widget) - - widgets.sort(key=lambda widget: widget.comp) - - for widget in widgets: - content.append(widget) - - def _build_specials(self, content): - """Build the special entities""" - specials = sorted(self.contact_list.get_specials()) - current = None - for entity in specials: - if current is not None and current.bare == entity.bare: - # nested entity (e.g. MUC private conversations) - widget = self._build_entity_widget( - entity, ("resource",), markup_prepend=" ", special=True - ) - else: - # the special widgets - if entity.resource: - widget = self._build_entity_widget(entity, ("resource",), special=True) - else: - widget = self._build_entity_widget( - entity, - ("cache_nick", "cache_name", "node"), - with_show_attr=False, - special=True, - ) - content.append(widget) - - def _build_list(self): - """Build the main contact list widget""" - content = urwid.SimpleListWalker([]) - - self._build_specials(content) - if self.contact_list._specials: - content.append(urwid.Divider("=")) - - groups = list(self.contact_list._groups) - groups.sort(key=lambda x: x.lower() if x else '') - for group in groups: - data = self.contact_list.get_group_data(group) - folded = data.get(C.GROUP_DATA_FOLDED, False) - jids = list(data["jids"]) - if group is not None and ( - self.contact_list.any_entity_visible(jids) - or self.contact_list.show_empty_groups - ): - header = "[-]" if not folded else "[+]" - widget = sat_widgets.ClickableText(group, header=header + " ") - content.append(widget) - urwid.connect_signal(widget, "click", self._group_clicked) - if not folded: - self._build_entities(content, jids) - not_in_roster = ( - set(self.contact_list._cache) - .difference(self.contact_list._roster) - .difference(self.contact_list._specials) - .difference((self.contact_list.whoami.bare,)) - ) - if not_in_roster: - content.append(urwid.Divider("-")) - self._build_entities(content, not_in_roster) - - return urwid.ListBox(content) - - -quick_widgets.register(QuickContactList, ContactList) diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/game_tarot.py --- a/libervia/frontends/primitivus/game_tarot.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,397 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -import urwid -from urwid_satext import sat_widgets -from libervia.frontends.tools.games import TarotCard -from libervia.frontends.quick_frontend.quick_game_tarot import QuickTarotGame -from libervia.frontends.primitivus import xmlui -from libervia.frontends.primitivus.keys import action_key_map as a_key - - -class CardDisplayer(urwid.Text): - """Show a card""" - - signals = ["click"] - - def __init__(self, card): - self.__selected = False - self.card = card - urwid.Text.__init__(self, card.get_attr_text()) - - def selectable(self): - return True - - def keypress(self, size, key): - if key == a_key["CARD_SELECT"]: - self.select(not self.__selected) - self._emit("click") - return key - - def mouse_event(self, size, event, button, x, y, focus): - if urwid.is_mouse_event(event) and button == 1: - self.select(not self.__selected) - self._emit("click") - return True - - return False - - def select(self, state=True): - self.__selected = state - attr, txt = self.card.get_attr_text() - if self.__selected: - attr += "_selected" - self.set_text((attr, txt)) - self._invalidate() - - def is_selected(self): - return self.__selected - - def get_card(self): - return self.card - - def render(self, size, focus=False): - canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus)) - if focus: - canvas.set_cursor((0, 0)) - return canvas - - -class Hand(urwid.WidgetWrap): - """Used to display several cards, and manage a hand""" - - signals = ["click"] - - def __init__(self, hand=[], selectable=False, on_click=None, user_data=None): - """@param hand: list of Card""" - self.__selectable = selectable - self.columns = urwid.Columns([], dividechars=1) - if on_click: - urwid.connect_signal(self, "click", on_click, user_data) - if hand: - self.update(hand) - urwid.WidgetWrap.__init__(self, self.columns) - - def selectable(self): - return self.__selectable - - def keypress(self, size, key): - - if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]: - return self.columns.keypress(size, key) - else: - # No card displayed, we still have to manage the clicks - if key == a_key["CARD_SELECT"]: - self._emit("click", None) - return key - - def get_selected(self): - """Return a list of selected cards""" - _selected = [] - for wid in self.columns.widget_list: - if isinstance(wid, CardDisplayer) and wid.is_selected(): - _selected.append(wid.get_card()) - return _selected - - def update(self, hand): - """Update the hand displayed in this widget - @param hand: list of Card""" - try: - del self.columns.widget_list[:] - del self.columns.column_types[:] - except IndexError: - pass - self.columns.contents.append((urwid.Text(""), ("weight", 1, False))) - for card in hand: - widget = CardDisplayer(card) - self.columns.widget_list.append(widget) - self.columns.column_types.append(("fixed", 3)) - urwid.connect_signal(widget, "click", self.__on_click) - self.columns.contents.append((urwid.Text(""), ("weight", 1, False))) - self.columns.focus_position = 1 - - def __on_click(self, card_wid): - self._emit("click", card_wid) - - -class Card(TarotCard): - """This class is used to represent a card, logically - and give a text representation with attributes""" - - SIZE = 3 # size of a displayed card - - def __init__(self, suit, value): - """@param file: path of the PNG file""" - TarotCard.__init__(self, (suit, value)) - - def get_attr_text(self): - """return text representation of the card with attributes""" - try: - value = "%02i" % int(self.value) - except ValueError: - value = self.value[0].upper() + self.value[1] - if self.suit == "atout": - if self.value == "excuse": - suit = "c" - else: - suit = "A" - color = "neutral" - elif self.suit == "pique": - suit = "♠" - color = "black" - elif self.suit == "trefle": - suit = "♣" - color = "black" - elif self.suit == "coeur": - suit = "♥" - color = "red" - elif self.suit == "carreau": - suit = "♦" - color = "red" - if self.bout: - color = "special" - return ("card_%s" % color, "%s%s" % (value, suit)) - - def get_widget(self): - """Return a widget representing the card""" - return CardDisplayer(self) - - -class Table(urwid.FlowWidget): - """Represent the cards currently on the table""" - - def __init__(self): - self.top = self.left = self.bottom = self.right = None - - def put_card(self, location, card): - """Put a card on the table - @param location: where to put the card (top, left, bottom or right) - @param card: Card to play or None""" - assert location in ["top", "left", "bottom", "right"] - assert isinstance(card, Card) or card == None - if [getattr(self, place) for place in ["top", "left", "bottom", "right"]].count( - None - ) == 0: - # If the table is full of card, we remove them - self.top = self.left = self.bottom = self.right = None - setattr(self, location, card) - self._invalidate() - - def rows(self, size, focus=False): - return self.display_widget(size, focus).rows(size, focus) - - def render(self, size, focus=False): - return self.display_widget(size, focus).render(size, focus) - - def display_widget(self, size, focus): - cards = {} - max_col, = size - separator = " - " - margin = max((max_col - Card.SIZE) / 2, 0) * " " - margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * " " - for location in ["top", "left", "bottom", "right"]: - card = getattr(self, location) - cards[location] = card.get_attr_text() if card else Card.SIZE * " " - render_wid = [ - urwid.Text([margin, cards["top"]]), - urwid.Text([margin_center, cards["left"], separator, cards["right"]]), - urwid.Text([margin, cards["bottom"]]), - ] - return urwid.Pile(render_wid) - - -class TarotGame(QuickTarotGame, urwid.WidgetWrap): - """Widget for card games""" - - def __init__(self, parent, referee, players): - QuickTarotGame.__init__(self, parent, referee, players) - self.load_cards() - self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), "center")]) - # self.parent.host.debug() - self.table = Table() - self.center = urwid.Columns( - [ - ("fixed", len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))), - urwid.Filler(self.table), - ( - "fixed", - len(self.right_nick), - urwid.Filler(urwid.Text(self.right_nick)), - ), - ] - ) - """urwid.Pile([urwid.Padding(self.top_card_wid,'center'), - urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)), - urwid.Padding(self.center_cards_wid,'center'), - ('fixed',len(self.right_nick),urwid.Text(self.right_nick)) - ]), - urwid.Padding(self.bottom_card_wid,'center') - ])""" - self.hand_wid = Hand(selectable=True, on_click=self.on_click) - self.main_frame = urwid.Frame( - self.center, header=self.top, footer=self.hand_wid, focus_part="footer" - ) - urwid.WidgetWrap.__init__(self, self.main_frame) - self.parent.host.bridge.tarot_game_ready( - self.player_nick, referee, self.parent.profile - ) - - def load_cards(self): - """Load all the cards in memory""" - QuickTarotGame.load_cards(self) - for value in list(map(str, list(range(1, 22)))) + ["excuse"]: - card = Card("atout", value) - self.cards[card.suit, card.value] = card - self.deck.append(card) - for suit in ["pique", "coeur", "carreau", "trefle"]: - for value in list(map(str, list(range(1, 11)))) + ["valet", "cavalier", "dame", "roi"]: - card = Card(suit, value) - self.cards[card.suit, card.value] = card - self.deck.append(card) - - def tarot_game_new_handler(self, hand): - """Start a new game, with given hand""" - if hand is []: # reset the display after the scores have been showed - self.reset_round() - for location in ["top", "left", "bottom", "right"]: - self.table.put_card(location, None) - self.parent.host.redraw() - self.parent.host.bridge.tarot_game_ready( - self.player_nick, self.referee, self.parent.profile - ) - return - QuickTarotGame.tarot_game_new_handler(self, hand) - self.hand_wid.update(self.hand) - self.parent.host.redraw() - - def tarot_game_choose_contrat_handler(self, xml_data): - """Called when the player has to select his contrat - @param xml_data: SàT xml representation of the form""" - form = xmlui.create( - self.parent.host, - xml_data, - title=_("Please choose your contrat"), - flags=["NO_CANCEL"], - profile=self.parent.profile, - ) - form.show(valign="top") - - def tarot_game_show_cards_handler(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - QuickTarotGame.tarot_game_show_cards_handler(self, game_stage, cards, data) - self.center.widget_list[1] = urwid.Filler(Hand(self.to_show)) - self.parent.host.redraw() - - def tarot_game_your_turn_handler(self): - QuickTarotGame.tarot_game_your_turn_handler(self) - - def tarot_game_score_handler(self, xml_data, winners, loosers): - """Called when the round is over, display the scores - @param xml_data: SàT xml representation of the form""" - if not winners and not loosers: - title = _("Draw game") - else: - title = _("You win \o/") if self.player_nick in winners else _("You loose :(") - form = xmlui.create( - self.parent.host, - xml_data, - title=title, - flags=["NO_CANCEL"], - profile=self.parent.profile, - ) - form.show() - - def tarot_game_invalid_cards_handler(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - QuickTarotGame.tarot_game_invalid_cards_handler( - self, phase, played_cards, invalid_cards - ) - self.hand_wid.update(self.hand) - if self._autoplay == None: # No dialog if there is autoplay - self.parent.host.bar_notify(_("Cards played are invalid !")) - self.parent.host.redraw() - - def tarot_game_cards_played_handler(self, player, cards): - """A card has been played by player""" - QuickTarotGame.tarot_game_cards_played_handler(self, player, cards) - self.table.put_card(self.get_player_location(player), self.played[player]) - self._checkState() - self.parent.host.redraw() - - def _checkState(self): - if isinstance( - self.center.widget_list[1].original_widget, Hand - ): # if we have a hand displayed - self.center.widget_list[1] = urwid.Filler( - self.table - ) # we show again the table - if self.state == "chien": - self.to_show = [] - self.state = "wait" - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self.hand_wid.update(self.hand) - - ##EVENTS## - def on_click(self, hand, card_wid): - """Called when user do an action on the hand""" - if not self.state in ["play", "ecart", "wait_for_ecart"]: - # it's not our turn, we ignore the click - card_wid.select(False) - return - self._checkState() - if self.state == "ecart": - if len(self.hand_wid.get_selected()) == 6: - pop_up_widget = sat_widgets.ConfirmDialog( - _("Do you put these cards in chien ?"), - yes_cb=self.on_ecart_done, - no_cb=self.parent.host.remove_pop_up, - ) - self.parent.host.show_pop_up(pop_up_widget) - elif self.state == "play": - card = card_wid.get_card() - self.parent.host.bridge.tarot_game_play_cards( - self.player_nick, - self.referee, - [(card.suit, card.value)], - self.parent.profile, - ) - self.hand.remove(card) - self.hand_wid.update(self.hand) - self.state = "wait" - - def on_ecart_done(self, button): - """Called when player has finished his écart""" - ecart = [] - for card in self.hand_wid.get_selected(): - ecart.append((card.suit, card.value)) - self.hand.remove(card) - self.hand_wid.update(self.hand) - self.parent.host.bridge.tarot_game_play_cards( - self.player_nick, self.referee, ecart, self.parent.profile - ) - self.state = "wait" - self.parent.host.remove_pop_up() diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/keys.py --- a/libervia/frontends/primitivus/keys.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""This file manage the action <=> key map""" - -from urwid_satext.keys import action_key_map - - -action_key_map.update( - { - # Edit bar - ("edit", "MODE_INSERTION"): "i", - ("edit", "MODE_COMMAND"): ":", - ("edit", "HISTORY_PREV"): "up", - ("edit", "HISTORY_NEXT"): "down", - # global - ("global", "MENU_HIDE"): "meta m", - ("global", "NOTIFICATION_NEXT"): "ctrl n", - ("global", "OVERLAY_HIDE"): "ctrl s", - ("global", "DEBUG"): "ctrl d", - ("global", "CONTACTS_HIDE"): "f2", - ( - "global", - "REFRESH_SCREEN", - ): "ctrl l", # ctrl l is used by Urwid to refresh screen - # global menu - ("menu_global", "APP_QUIT"): "ctrl x", - ("menu_global", "ROOM_JOIN"): "meta j", - # primitivus widgets - ("primitivus_widget", "DECORATION_HIDE"): "meta l", - # contact list - ("contact_list", "STATUS_HIDE"): "meta s", - ("contact_list", "DISCONNECTED_HIDE"): "meta d", - ("contact_list", "RESOURCES_HIDE"): "meta r", - # chat panel - ("chat_panel", "OCCUPANTS_HIDE"): "meta p", - ("chat_panel", "TIMESTAMP_HIDE"): "meta t", - ("chat_panel", "SHORT_NICKNAME"): "meta n", - ("chat_panel", "SUBJECT_SWITCH"): "meta s", - ("chat_panel", "GOTO_BOTTOM"): "G", - # card game - ("card_game", "CARD_SELECT"): " ", - # focus - ("focus", "FOCUS_EXTRA"): "ctrl f", - } -) - - -action_key_map.set_close_namespaces(tuple(), ("global", "focus", "menu_global")) -action_key_map.check_namespaces() diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/notify.py --- a/libervia/frontends/primitivus/notify.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import dbus - - -class Notify(object): - """Used to send notification and detect if we have focus""" - - def __init__(self): - - # X11 stuff - self.display = None - self.X11_id = -1 - - try: - from Xlib import display as X_display - - self.display = X_display.Display() - self.X11_id = self.get_focus() - except: - pass - - # Now we try to connect to Freedesktop D-Bus API - try: - bus = dbus.SessionBus() - db_object = bus.get_object( - "org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - follow_name_owner_changes=True, - ) - self.freedesktop_int = dbus.Interface( - db_object, dbus_interface="org.freedesktop.Notifications" - ) - except: - self.freedesktop_int = None - - def get_focus(self): - if not self.display: - return 0 - return self.display.get_input_focus().focus.id - - def has_focus(self): - return (self.get_focus() == self.X11_id) if self.display else True - - def use_x11(self): - return bool(self.display) - - def send_notification(self, summ_mess, body_mess=""): - """Send notification to the user if possible""" - # TODO: check options before sending notifications - if self.freedesktop_int: - self.send_fd_notification(summ_mess, body_mess) - - def send_fd_notification(self, summ_mess, body_mess=""): - """Send notification with the FreeDesktop D-Bus API""" - if self.freedesktop_int: - app_name = "Primitivus" - replaces_id = 0 - app_icon = "" - summary = summ_mess - body = body_mess - actions = dbus.Array(signature="s") - hints = dbus.Dictionary(signature="sv") - expire_timeout = -1 - - self.freedesktop_int.Notify( - app_name, - replaces_id, - app_icon, - summary, - body, - actions, - hints, - expire_timeout, - ) diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/profile_manager.py --- a/libervia/frontends/primitivus/profile_manager.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -from libervia.backend.core import log as logging - -log = logging.getLogger(__name__) -from libervia.frontends.quick_frontend.quick_profile_manager import QuickProfileManager -from libervia.frontends.primitivus.constants import Const as C -from libervia.frontends.primitivus.keys import action_key_map as a_key -from urwid_satext import sat_widgets -import urwid - - -class ProfileManager(QuickProfileManager, urwid.WidgetWrap): - def __init__(self, host, autoconnect=None): - QuickProfileManager.__init__(self, host, autoconnect) - - # login & password box must be created before list because of on_profile_change - self.login_wid = sat_widgets.AdvancedEdit(_("Login:"), align="center") - self.pass_wid = sat_widgets.Password(_("Password:"), align="center") - - style = ["no_first_select"] - profiles = host.bridge.profiles_list_get() - profiles.sort() - self.list_profile = sat_widgets.List( - profiles, style=style, align="center", on_change=self.on_profile_change - ) - - # new & delete buttons - buttons = [ - urwid.Button(_("New"), self.on_new_profile), - urwid.Button(_("Delete"), self.on_delete_profile), - ] - buttons_flow = urwid.GridFlow( - buttons, - max([len(button.get_label()) for button in buttons]) + 4, - 1, - 1, - "center", - ) - - # second part: login information: - divider = urwid.Divider("-") - - # connect button - connect_button = sat_widgets.CustomButton( - _("Connect"), self.on_connect_profiles, align="center" - ) - - # we now build the widget - list_walker = urwid.SimpleFocusListWalker( - [ - buttons_flow, - self.list_profile, - divider, - self.login_wid, - self.pass_wid, - connect_button, - ] - ) - frame_body = urwid.ListBox(list_walker) - frame = urwid.Frame( - frame_body, - urwid.AttrMap(urwid.Text(_("Profile Manager"), align="center"), "title"), - ) - self.main_widget = urwid.LineBox(frame) - urwid.WidgetWrap.__init__(self, self.main_widget) - - self.go(autoconnect) - - def keypress(self, size, key): - if key == a_key["APP_QUIT"]: - self.host.on_exit() - raise urwid.ExitMainLoop() - elif key in (a_key["FOCUS_UP"], a_key["FOCUS_DOWN"]): - focus_diff = 1 if key == a_key["FOCUS_DOWN"] else -1 - list_box = self.main_widget.base_widget.body - current_focus = list_box.body.get_focus()[1] - if current_focus is None: - return - while True: - current_focus += focus_diff - if current_focus < 0 or current_focus >= len(list_box.body): - break - if list_box.body[current_focus].selectable(): - list_box.set_focus( - current_focus, "above" if focus_diff == 1 else "below" - ) - list_box._invalidate() - return - return super(ProfileManager, self).keypress(size, key) - - def cancel_dialog(self, button): - self.host.remove_pop_up() - - def new_profile(self, button, edit): - """Create the profile""" - name = edit.get_edit_text() - self.host.bridge.profile_create( - name, - callback=lambda: self.new_profile_created(name), - errback=self.profile_creation_failure, - ) - - def new_profile_created(self, profile): - # new profile will be selected, and a selected profile assume the session is started - self.host.bridge.profile_start_session( - "", - profile, - callback=lambda __: self.new_profile_session_started(profile), - errback=self.profile_creation_failure, - ) - - def new_profile_session_started(self, profile): - self.host.remove_pop_up() - self.refill_profiles() - self.list_profile.select_value(profile) - self.current.profile = profile - self.get_connection_params(profile) - self.host.redraw() - - def profile_creation_failure(self, reason): - self.host.remove_pop_up() - message = self._get_error_message(reason) - self.host.alert(_("Can't create profile"), message) - - def delete_profile(self, button): - self._delete_profile() - self.host.remove_pop_up() - - def on_new_profile(self, e): - pop_up_widget = sat_widgets.InputDialog( - _("New profile"), - _("Please enter a new profile name"), - cancel_cb=self.cancel_dialog, - ok_cb=self.new_profile, - ) - self.host.show_pop_up(pop_up_widget) - - def on_delete_profile(self, e): - if self.current.profile: - pop_up_widget = sat_widgets.ConfirmDialog( - _("Are you sure you want to delete the profile {} ?").format( - self.current.profile - ), - no_cb=self.cancel_dialog, - yes_cb=self.delete_profile, - ) - self.host.show_pop_up(pop_up_widget) - - def on_connect_profiles(self, button): - """Connect the profiles and start the main widget - - @param button: the connect button - """ - self._on_connect_profiles() - - def reset_fields(self): - """Set profile to None, and reset fields""" - super(ProfileManager, self).reset_fields() - self.list_profile.unselect_all(invisible=True) - - def set_profiles(self, profiles): - """Update the list of profiles""" - self.list_profile.change_values(profiles) - self.host.redraw() - - def get_profiles(self): - return self.list_profile.get_selected_values() - - def get_jid(self): - return self.login_wid.get_edit_text() - - def getPassword(self): - return self.pass_wid.get_edit_text() - - def set_jid(self, jid_): - self.login_wid.set_edit_text(jid_) - self.current.login = jid_ - self.host.redraw() # FIXME: redraw should be avoided - - def set_password(self, password): - self.pass_wid.set_edit_text(password) - self.current.password = password - self.host.redraw() - - def on_profile_change(self, list_wid, widget=None, selected=None): - """This is called when a profile is selected in the profile list. - - @param list_wid: the List widget who sent the event - """ - self.update_connection_params() - focused = list_wid.focus - selected = focused.get_state() if focused is not None else False - if not selected: # profile was just unselected - return - focused.set_state( - False, invisible=True - ) # we don't want the widget to be selected until we are sure we can access it - - def authenticate_cb(data, cb_id, profile): - if C.bool(data.pop("validated", C.BOOL_FALSE)): - self.current.profile = profile - focused.set_state(True, invisible=True) - self.get_connection_params(profile) - self.host.redraw() - self.host.action_manager(data, callback=authenticate_cb, profile=profile) - - self.host.action_launch( - C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text - ) diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/progress.py --- a/libervia/frontends/primitivus/progress.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -import urwid -from urwid_satext import sat_widgets -from libervia.frontends.quick_frontend import quick_widgets - - -class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget): - PROFILES_ALLOW_NONE = True - - def __init__(self, host, target, profiles): - assert target is None and profiles is None - quick_widgets.QuickWidget.__init__(self, host, target) - self.host = host - self.progress_list = urwid.SimpleListWalker([]) - self.progress_dict = {} - listbox = urwid.ListBox(self.progress_list) - buttons = [] - buttons.append(sat_widgets.CustomButton(_("Clear progress list"), self._on_clear)) - max_len = max([button.get_size() for button in buttons]) - buttons_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center") - main_wid = sat_widgets.FocusFrame(listbox, footer=buttons_wid) - urwid.WidgetWrap.__init__(self, main_wid) - - def add(self, progress_id, message, profile): - mess_wid = urwid.Text(message) - progr_wid = urwid.ProgressBar("progress_normal", "progress_complete") - column = urwid.Columns([mess_wid, progr_wid]) - self.progress_dict[(progress_id, profile)] = { - "full": column, - "progress": progr_wid, - "state": "init", - } - self.progress_list.append(column) - self.progress_cb(self.host.loop, (progress_id, message, profile)) - - def progress_cb(self, loop, data): - progress_id, message, profile = data - data = self.host.bridge.progress_get(progress_id, profile) - pbar = self.progress_dict[(progress_id, profile)]["progress"] - if data: - if self.progress_dict[(progress_id, profile)]["state"] == "init": - # first answer, we must construct the bar - self.progress_dict[(progress_id, profile)]["state"] = "progress" - pbar.done = float(data["size"]) - - pbar.set_completion(float(data["position"])) - self.update_not_bar() - else: - if self.progress_dict[(progress_id, profile)]["state"] == "progress": - self.progress_dict[(progress_id, profile)]["state"] = "done" - pbar.set_completion(pbar.done) - self.update_not_bar() - return - - loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile)) - - def _remove_bar(self, progress_id, profile): - wid = self.progress_dict[(progress_id, profile)]["full"] - self.progress_list.remove(wid) - del (self.progress_dict[(progress_id, profile)]) - - def _on_clear(self, button): - to_remove = [] - for progress_id, profile in self.progress_dict: - if self.progress_dict[(progress_id, profile)]["state"] == "done": - to_remove.append((progress_id, profile)) - for progress_id, profile in to_remove: - self._remove_bar(progress_id, profile) - self.update_not_bar() - - def update_not_bar(self): - if not self.progress_dict: - self.host.set_progress(None) - return - progress = 0 - nb_bars = 0 - for progress_id, profile in self.progress_dict: - pbar = self.progress_dict[(progress_id, profile)]["progress"] - progress += pbar.current / pbar.done * 100 - nb_bars += 1 - av_progress = progress / float(nb_bars) - self.host.set_progress(av_progress) diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/status.py --- a/libervia/frontends/primitivus/status.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -import urwid -from urwid_satext import sat_widgets -from libervia.frontends.quick_frontend.constants import Const as commonConst -from libervia.frontends.primitivus.constants import Const as C - - -class StatusBar(urwid.Columns): - def __init__(self, host): - self.host = host - self.presence = sat_widgets.ClickableText("") - status_prefix = urwid.Text("[") - status_suffix = urwid.Text("]") - self.status = sat_widgets.ClickableText("") - self.set_presence_status(C.PRESENCE_UNAVAILABLE, "") - urwid.Columns.__init__( - self, - [ - ("weight", 1, self.presence), - ("weight", 1, status_prefix), - ("weight", 9, self.status), - ("weight", 1, status_suffix), - ], - ) - urwid.connect_signal(self.presence, "click", self.on_presence_click) - urwid.connect_signal(self.status, "click", self.on_status_click) - - def on_presence_click(self, sender=None): - if not self.host.bridge.is_connected( - self.host.current_profile - ): # FIXME: manage multi-profiles - return - options = [commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE] - list_widget = sat_widgets.GenericList( - options=options, option_type=sat_widgets.ClickableText, on_click=self.on_change - ) - decorated = sat_widgets.LabelLine( - list_widget, sat_widgets.SurroundedText(_("Set your presence")) - ) - self.host.show_pop_up(decorated) - - def on_status_click(self, sender=None): - if not self.host.bridge.is_connected( - self.host.current_profile - ): # FIXME: manage multi-profiles - return - pop_up_widget = sat_widgets.InputDialog( - _("Set your status"), - _("New status"), - default_txt=self.status.get_text(), - cancel_cb=lambda _: self.host.remove_pop_up(), - ok_cb=self.on_change, - ) - self.host.show_pop_up(pop_up_widget) - - def on_change(self, sender=None, user_data=None): - new_value = user_data.get_text() - previous = ( - [key for key in C.PRESENCE if C.PRESENCE[key][0] == self.presence.get_text()][ - 0 - ], - self.status.get_text(), - ) - if isinstance(user_data, sat_widgets.ClickableText): - new = ( - [ - key - for key in commonConst.PRESENCE - if commonConst.PRESENCE[key] == new_value - ][0], - previous[1], - ) - elif isinstance(user_data, sat_widgets.AdvancedEdit): - new = (previous[0], new_value[0]) - if new != previous: - statuses = { - C.PRESENCE_STATUSES_DEFAULT: new[1] - } # FIXME: manage multilingual statuses - for ( - profile - ) in ( - self.host.profiles - ): # FIXME: for now all the profiles share the same status - self.host.bridge.presence_set( - show=new[0], statuses=statuses, profile_key=profile - ) - self.set_presence_status(new[0], new[1]) - self.host.remove_pop_up() - - def set_presence_status(self, show, status): - show_icon, show_attr = C.PRESENCE.get(show) - self.presence.set_text(("show_normal", show_icon)) - if status is not None: - self.status.set_text((show_attr, status)) - self.host.redraw() diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/widget.py --- a/libervia/frontends/primitivus/widget.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core import log as logging - -log = logging.getLogger(__name__) -import urwid -from urwid_satext import sat_widgets -from libervia.frontends.primitivus.keys import action_key_map as a_key - - -class PrimitivusWidget(urwid.WidgetWrap): - """Base widget for Primitivus""" - - def __init__(self, w, title=""): - self._title = title - self._title_dynamic = None - self._original_widget = w - urwid.WidgetWrap.__init__(self, self._get_decoration(w)) - - @property - def title(self): - """Text shown in title bar of the widget""" - - # profiles currently managed by frontend - try: - all_profiles = self.host.profiles - except AttributeError: - all_profiles = [] - - # profiles managed by the widget - try: - profiles = self.profiles - except AttributeError: - try: - profiles = [self.profile] - except AttributeError: - profiles = [] - - title_elts = [] - if self._title: - title_elts.append(self._title) - if self._title_dynamic: - title_elts.append(self._title_dynamic) - if len(all_profiles) > 1 and profiles: - title_elts.append("[{}]".format(", ".join(profiles))) - return sat_widgets.SurroundedText(" ".join(title_elts)) - - @title.setter - def title(self, value): - self._title = value - if self.decoration_visible: - self.show_decoration() - - @property - def title_dynamic(self): - """Dynamic part of title""" - return self._title_dynamic - - @title_dynamic.setter - def title_dynamic(self, value): - self._title_dynamic = value - if self.decoration_visible: - self.show_decoration() - - @property - def decoration_visible(self): - """True if the decoration is visible""" - return isinstance(self._w, sat_widgets.LabelLine) - - def keypress(self, size, key): - if key == a_key["DECORATION_HIDE"]: # user wants to (un)hide widget decoration - show = not self.decoration_visible - self.show_decoration(show) - else: - return super(PrimitivusWidget, self).keypress(size, key) - - def _get_decoration(self, widget): - return sat_widgets.LabelLine(widget, self.title) - - def show_decoration(self, show=True): - """Show/Hide the decoration around the window""" - self._w = ( - self._get_decoration(self._original_widget) if show else self._original_widget - ) - - def get_menu(self): - raise NotImplementedError diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/primitivus/xmlui.py --- a/libervia/frontends/primitivus/xmlui.py Fri Jun 02 14:54:26 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,528 +0,0 @@ -#!/usr/bin/env python3 - - -# Primitivus: a SAT frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -import urwid -import copy -from libervia.backend.core import exceptions -from urwid_satext import sat_widgets -from urwid_satext import files_management -from libervia.backend.core.log import getLogger - -log = getLogger(__name__) -from libervia.frontends.primitivus.constants import Const as C -from libervia.frontends.primitivus.widget import PrimitivusWidget -from libervia.frontends.tools import xmlui - - -class PrimitivusEvents(object): - """ Used to manage change event of Primitivus widgets """ - - def _event_callback(self, ctrl, *args, **kwargs): - """" Call xmlui callback and ignore any extra argument """ - args[-1](ctrl) - - def _xmlui_on_change(self, callback): - """ Call callback with widget as only argument """ - urwid.connect_signal(self, "change", self._event_callback, callback) - - -class PrimitivusEmptyWidget(xmlui.EmptyWidget, urwid.Text): - def __init__(self, _xmlui_parent): - urwid.Text.__init__(self, "") - - -class PrimitivusTextWidget(xmlui.TextWidget, urwid.Text): - def __init__(self, _xmlui_parent, value, read_only=False): - urwid.Text.__init__(self, value) - - -class PrimitivusLabelWidget(xmlui.LabelWidget, PrimitivusTextWidget): - def __init__(self, _xmlui_parent, value): - super(PrimitivusLabelWidget, self).__init__(_xmlui_parent, value + ": ") - - -class PrimitivusJidWidget(xmlui.JidWidget, PrimitivusTextWidget): - pass - - -class PrimitivusDividerWidget(xmlui.DividerWidget, urwid.Divider): - def __init__(self, _xmlui_parent, style="line"): - if style == "line": - div_char = "─" - elif style == "dot": - div_char = "·" - elif style == "dash": - div_char = "-" - elif style == "plain": - div_char = "█" - elif style == "blank": - div_char = " " - else: - log.warning(_("Unknown div_char")) - div_char = "─" - - urwid.Divider.__init__(self, div_char) - - -class PrimitivusStringWidget( - xmlui.StringWidget, sat_widgets.AdvancedEdit, PrimitivusEvents -): - def __init__(self, _xmlui_parent, value, read_only=False): - sat_widgets.AdvancedEdit.__init__(self, edit_text=value) - self.read_only = read_only - - def selectable(self): - if self.read_only: - return False - return super(PrimitivusStringWidget, self).selectable() - - def _xmlui_set_value(self, value): - self.set_edit_text(value) - - def _xmlui_get_value(self): - return self.get_edit_text() - - -class PrimitivusJidInputWidget(xmlui.JidInputWidget, PrimitivusStringWidget): - pass - - -class PrimitivusPasswordWidget( - xmlui.PasswordWidget, sat_widgets.Password, PrimitivusEvents -): - def __init__(self, _xmlui_parent, value, read_only=False): - sat_widgets.Password.__init__(self, edit_text=value) - self.read_only = read_only - - def selectable(self): - if self.read_only: - return False - return super(PrimitivusPasswordWidget, self).selectable() - - def _xmlui_set_value(self, value): - self.set_edit_text(value) - - def _xmlui_get_value(self): - return self.get_edit_text() - - -class PrimitivusTextBoxWidget( - xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, PrimitivusEvents -): - def __init__(self, _xmlui_parent, value, read_only=False): - sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True) - self.read_only = read_only - - def selectable(self): - if self.read_only: - return False - return super(PrimitivusTextBoxWidget, self).selectable() - - def _xmlui_set_value(self, value): - self.set_edit_text(value) - - def _xmlui_get_value(self): - return self.get_edit_text() - - -class PrimitivusBoolWidget(xmlui.BoolWidget, urwid.CheckBox, PrimitivusEvents): - def __init__(self, _xmlui_parent, state, read_only=False): - urwid.CheckBox.__init__(self, "", state=state) - self.read_only = read_only - - def selectable(self): - if self.read_only: - return False - return super(PrimitivusBoolWidget, self).selectable() - - def _xmlui_set_value(self, value): - self.set_state(value == "true") - - def _xmlui_get_value(self): - return C.BOOL_TRUE if self.get_state() else C.BOOL_FALSE - - -class PrimitivusIntWidget(xmlui.IntWidget, sat_widgets.AdvancedEdit, PrimitivusEvents): - def __init__(self, _xmlui_parent, value, read_only=False): - sat_widgets.AdvancedEdit.__init__(self, edit_text=value) - self.read_only = read_only - - def selectable(self): - if self.read_only: - return False - return super(PrimitivusIntWidget, self).selectable() - - def _xmlui_set_value(self, value): - self.set_edit_text(value) - - def _xmlui_get_value(self): - return self.get_edit_text() - - -class PrimitivusButtonWidget( - xmlui.ButtonWidget, sat_widgets.CustomButton, PrimitivusEvents -): - def __init__(self, _xmlui_parent, value, click_callback): - sat_widgets.CustomButton.__init__(self, value, on_press=click_callback) - - def _xmlui_on_click(self, callback): - urwid.connect_signal(self, "click", callback) - - -class PrimitivusListWidget(xmlui.ListWidget, sat_widgets.List, PrimitivusEvents): - def __init__(self, _xmlui_parent, options, selected, flags): - sat_widgets.List.__init__(self, options=options, style=flags) - self._xmlui_select_values(selected) - - def _xmlui_select_value(self, value): - return self.select_value(value) - - def _xmlui_select_values(self, values): - return self.select_values(values) - - def _xmlui_get_selected_values(self): - return [option.value for option in self.get_selected_values()] - - def _xmlui_add_values(self, values, select=True): - current_values = self.get_all_values() - new_values = copy.deepcopy(current_values) - for value in values: - if value not in current_values: - new_values.append(value) - if select: - selected = self._xmlui_get_selected_values() - self.change_values(new_values) - if select: - for value in values: - if value not in selected: - selected.append(value) - self._xmlui_select_values(selected) - - -class PrimitivusJidsListWidget(xmlui.ListWidget, sat_widgets.List, PrimitivusEvents): - def __init__(self, _xmlui_parent, jids, styles): - sat_widgets.List.__init__( - self, - options=jids + [""], # the empty field is here to add new jids if needed - option_type=lambda txt, align: sat_widgets.AdvancedEdit( - edit_text=txt, align=align - ), - on_change=self._on_change, - ) - self.delete = 0 - - def _on_change(self, list_widget, jid_widget=None, text=None): - if jid_widget is not None: - if jid_widget != list_widget.contents[-1] and not text: - # if a field is empty, we delete the line (except for the last line) - list_widget.contents.remove(jid_widget) - elif jid_widget == list_widget.contents[-1] and text: - # we always want an empty field as last value to be able to add jids - list_widget.contents.append(sat_widgets.AdvancedEdit()) - - def _xmlui_get_selected_values(self): - # XXX: there is not selection in this list, so we return all non empty values - return [jid_ for jid_ in self.get_all_values() if jid_] - - -class PrimitivusAdvancedListContainer( - xmlui.AdvancedListContainer, sat_widgets.TableContainer, PrimitivusEvents -): - def __init__(self, _xmlui_parent, columns, selectable="no"): - options = {"ADAPT": ()} - if selectable != "no": - options["HIGHLIGHT"] = () - sat_widgets.TableContainer.__init__( - self, columns=columns, options=options, row_selectable=selectable != "no" - ) - - def _xmlui_append(self, widget): - self.add_widget(widget) - - def _xmlui_add_row(self, idx): - self.set_row_index(idx) - - def _xmlui_get_selected_widgets(self): - return self.get_selected_widgets() - - def _xmlui_get_selected_index(self): - return self.get_selected_index() - - def _xmlui_on_select(self, callback): - """ Call callback with widget as only argument """ - urwid.connect_signal(self, "click", self._event_callback, callback) - - -class PrimitivusPairsContainer(xmlui.PairsContainer, sat_widgets.TableContainer): - def __init__(self, _xmlui_parent): - options = {"ADAPT": (0,), "HIGHLIGHT": (0,)} - if self._xmlui_main.type == "param": - options["FOCUS_ATTR"] = "param_selected" - sat_widgets.TableContainer.__init__(self, columns=2, options=options) - - def _xmlui_append(self, widget): - if isinstance(widget, PrimitivusEmptyWidget): - # we don't want highlight on empty widgets - widget = urwid.AttrMap(widget, "default") - self.add_widget(widget) - - -class PrimitivusLabelContainer(PrimitivusPairsContainer, xmlui.LabelContainer): - pass - - -class PrimitivusTabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer): - def __init__(self, _xmlui_parent): - sat_widgets.TabsContainer.__init__(self) - - def _xmlui_append(self, widget): - self.body.append(widget) - - def _xmlui_add_tab(self, label, selected): - tab = PrimitivusVerticalContainer(None) - self.add_tab(label, tab, selected) - return tab - - -class PrimitivusVerticalContainer(xmlui.VerticalContainer, urwid.ListBox): - BOX_HEIGHT = 5 - - def __init__(self, _xmlui_parent): - urwid.ListBox.__init__(self, urwid.SimpleListWalker([])) - self._last_size = None - - def _xmlui_append(self, widget): - if "flow" not in widget.sizing(): - widget = urwid.BoxAdapter(widget, self.BOX_HEIGHT) - self.body.append(widget) - - def render(self, size, focus=False): - if size != self._last_size: - (maxcol, maxrow) = size - if self.body: - widget = self.body[0] - if isinstance(widget, urwid.BoxAdapter): - widget.height = maxrow - self._last_size = size - return super(PrimitivusVerticalContainer, self).render(size, focus) - - -### Dialogs ### - - -class PrimitivusDialog(object): - def __init__(self, _xmlui_parent): - self.host = _xmlui_parent.host - - def _xmlui_show(self): - self.host.show_pop_up(self) - - def _xmlui_close(self): - self.host.remove_pop_up(self) - - -class PrimitivusMessageDialog(PrimitivusDialog, xmlui.MessageDialog, sat_widgets.Alert): - def __init__(self, _xmlui_parent, title, message, level): - PrimitivusDialog.__init__(self, _xmlui_parent) - xmlui.MessageDialog.__init__(self, _xmlui_parent) - sat_widgets.Alert.__init__( - self, title, message, ok_cb=lambda __: self._xmlui_close() - ) - - -class PrimitivusNoteDialog(xmlui.NoteDialog, PrimitivusMessageDialog): - # TODO: separate NoteDialog - pass - - -class PrimitivusConfirmDialog( - PrimitivusDialog, xmlui.ConfirmDialog, sat_widgets.ConfirmDialog -): - def __init__(self, _xmlui_parent, title, message, level, buttons_set): - PrimitivusDialog.__init__(self, _xmlui_parent) - xmlui.ConfirmDialog.__init__(self, _xmlui_parent) - sat_widgets.ConfirmDialog.__init__( - self, - title, - message, - no_cb=lambda __: self._xmlui_cancelled(), - yes_cb=lambda __: self._xmlui_validated(), - ) - - -class PrimitivusFileDialog( - PrimitivusDialog, xmlui.FileDialog, files_management.FileDialog -): - def __init__(self, _xmlui_parent, title, message, level, filetype): - # TODO: message is not managed yet - PrimitivusDialog.__init__(self, _xmlui_parent) - xmlui.FileDialog.__init__(self, _xmlui_parent) - style = [] - if filetype == C.XMLUI_DATA_FILETYPE_DIR: - style.append("dir") - files_management.FileDialog.__init__( - self, - ok_cb=lambda path: self._xmlui_validated({"path": path}), - cancel_cb=lambda __: self._xmlui_cancelled(), - message=message, - title=title, - style=style, - ) - - -class GenericFactory(object): - def __getattr__(self, attr): - if attr.startswith("create"): - cls = globals()[ - "Primitivus" + attr[6:] - ] # XXX: we prefix with "Primitivus" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names - return cls - - -class WidgetFactory(GenericFactory): - def __getattr__(self, attr): - if attr.startswith("create"): - cls = GenericFactory.__getattr__(self, attr) - cls._xmlui_main = self._xmlui_main - return cls - - -class XMLUIPanel(xmlui.XMLUIPanel, PrimitivusWidget): - widget_factory = WidgetFactory() - - def __init__( - self, - host, - parsed_xml, - title=None, - flags=None, - callback=None, - ignore=None, - whitelist=None, - profile=C.PROF_KEY_NONE, - ): - self.widget_factory._xmlui_main = self - self._dest = None - xmlui.XMLUIPanel.__init__( - self, - host, - parsed_xml, - title=title, - flags=flags, - callback=callback, - ignore=ignore, - profile=profile, - ) - PrimitivusWidget.__init__(self, self.main_cont, self.xmlui_title) - - - def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None): - # Small hack to always have a VerticalContainer as main container in Primitivus. - # this used to be the default behaviour for all frontends, but now - # TabsContainer can also be the main container. - if _xmlui_parent is self: - node = current_node.childNodes[0] - if node.nodeName == "container" and node.getAttribute("type") == "tabs": - _xmlui_parent = self.widget_factory.createVerticalContainer(self) - self.main_cont = _xmlui_parent - return super(XMLUIPanel, self)._parse_childs(_xmlui_parent, current_node, wanted, - data) - - - def construct_ui(self, parsed_dom): - def post_treat(): - assert self.main_cont.body - - if self.type in ("form", "popup"): - buttons = [] - if self.type == "form": - buttons.append(urwid.Button(_("Submit"), self.on_form_submitted)) - if not "NO_CANCEL" in self.flags: - buttons.append(urwid.Button(_("Cancel"), self.on_form_cancelled)) - else: - buttons.append( - urwid.Button(_("OK"), on_press=lambda __: self._xmlui_close()) - ) - max_len = max([len(button.get_label()) for button in buttons]) - grid_wid = urwid.GridFlow(buttons, max_len + 4, 1, 0, "center") - self.main_cont.body.append(grid_wid) - elif self.type == "param": - tabs_cont = self.main_cont.body[0].base_widget - assert isinstance(tabs_cont, sat_widgets.TabsContainer) - buttons = [] - buttons.append(sat_widgets.CustomButton(_("Save"), self.on_save_params)) - buttons.append( - sat_widgets.CustomButton( - _("Cancel"), lambda x: self.host.remove_window() - ) - ) - max_len = max([button.get_size() for button in buttons]) - grid_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center") - tabs_cont.add_footer(grid_wid) - - xmlui.XMLUIPanel.construct_ui(self, parsed_dom, post_treat) - urwid.WidgetWrap.__init__(self, self.main_cont) - - def show(self, show_type=None, valign="middle"): - """Show the constructed UI - @param show_type: how to show the UI: - - None (follow XMLUI's recommendation) - - 'popup' - - 'window' - @param valign: vertical alignment when show_type is 'popup'. - Ignored when show_type is 'window'. - - """ - if show_type is None: - if self.type in ("window", "param"): - show_type = "window" - elif self.type in ("popup", "form"): - show_type = "popup" - - if show_type not in ("popup", "window"): - raise ValueError("Invalid show_type [%s]" % show_type) - - self._dest = show_type - if show_type == "popup": - self.host.show_pop_up(self, valign=valign) - elif show_type == "window": - self.host.new_widget(self, user_action=self.user_action) - else: - assert False - self.host.redraw() - - def _xmlui_close(self): - if self._dest == "window": - self.host.remove_window() - elif self._dest == "popup": - self.host.remove_pop_up(self) - else: - raise exceptions.InternalError( - "self._dest unknown, are you sure you have called XMLUI.show ?" - ) - - -class XMLUIDialog(xmlui.XMLUIDialog): - dialog_factory = GenericFactory() - - -xmlui.register_class(xmlui.CLASS_PANEL, XMLUIPanel) -xmlui.register_class(xmlui.CLASS_DIALOG, XMLUIDialog) -create = xmlui.create diff -r 47401850dec6 -r b620a8e882e1 libervia/frontends/quick_frontend/quick_profile_manager.py --- a/libervia/frontends/quick_frontend/quick_profile_manager.py Fri Jun 02 14:54:26 2023 +0200 +++ b/libervia/frontends/quick_frontend/quick_profile_manager.py Fri Jun 02 16:25:25 2023 +0200 @@ -21,7 +21,7 @@ from libervia.backend.core import log as logging log = logging.getLogger(__name__) -from libervia.frontends.primitivus.constants import Const as C +from libervia.tui.constants import Const as C class ProfileRecord(object): diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/__init__.py diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/base.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,863 @@ +#!/usr/bin/env python3 + +# Libervia TUI +# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from libervia.backend.core.i18n import _, D_ +from libervia.tui.constants import Const as C +from libervia.backend.core import log_config +log_config.sat_configure(C.LOG_BACKEND_STANDARD, C) +from libervia.backend.core import log as logging +log = logging.getLogger(__name__) +from libervia.backend.tools import config as sat_config +import urwid +from urwid.util import is_wide_char +from urwid_satext import sat_widgets +from libervia.frontends.quick_frontend.quick_app import QuickApp +from libervia.frontends.quick_frontend import quick_utils +from libervia.frontends.quick_frontend import quick_chat +from libervia.tui.profile_manager import ProfileManager +from libervia.tui.contact_list import ContactList +from libervia.tui.chat import Chat +from libervia.tui import xmlui +from libervia.tui.progress import Progress +from libervia.tui.notify import Notify +from libervia.tui.keys import action_key_map as a_key +from libervia.tui import config +from libervia.frontends.tools.misc import InputHistory +from libervia.backend.tools.common import dynamic_import +from libervia.frontends.tools import jid +import signal +import sys +## bridge handling +# we get bridge name from conf and initialise the right class accordingly +main_config = sat_config.parse_main_conf() +bridge_name = sat_config.config_get(main_config, '', 'bridge', 'dbus') +if 'dbus' not in bridge_name: + print(u"only D-Bus bridge is currently supported") + sys.exit(3) + + +class EditBar(sat_widgets.ModalEdit): + """ + The modal edit bar where you would enter messages and commands. + """ + + def __init__(self, host): + modes = {None: (C.MODE_NORMAL, u''), + a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '), + a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode + super(EditBar, self).__init__(modes) + self.host = host + self.set_completion_method(self._text_completion) + urwid.connect_signal(self, 'click', self.on_text_entered) + + def _text_completion(self, text, completion_data, mode): + if mode == C.MODE_INSERTION: + if self.host.selected_widget is not None: + try: + completion = self.host.selected_widget.completion + except AttributeError: + return text + else: + return completion(text, completion_data) + else: + return text + + def on_text_entered(self, editBar): + """Called when text is entered in the main edit bar""" + if self.mode == C.MODE_INSERTION: + if isinstance(self.host.selected_widget, quick_chat.QuickChat): + chat_widget = self.host.selected_widget + self.host.message_send( + chat_widget.target, + {'': editBar.get_edit_text()}, # TODO: handle language + mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat + errback=lambda failure: self.host.show_dialog(_("Error while sending message ({})").format(failure), type="error"), + profile_key=chat_widget.profile + ) + editBar.set_edit_text('') + elif self.mode == C.MODE_COMMAND: + self.command_handler() + + def command_handler(self): + #TODO: separate class with auto documentation (with introspection) + # and completion method + tokens = self.get_edit_text().split(' ') + command, args = tokens[0], tokens[1:] + if command == 'quit': + self.host.on_exit() + raise urwid.ExitMainLoop() + elif command == 'messages': + wid = sat_widgets.GenericList(logging.memory_get()) + self.host.select_widget(wid) + # FIXME: reactivate the command + # elif command == 'presence': + # values = [value for value in commonConst.PRESENCE.keys()] + # values = [value if value else 'online' for value in values] # the empty value actually means 'online' + # if args and args[0] in values: + # presence = '' if args[0] == 'online' else args[0] + # self.host.status_bar.on_change(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence])) + # else: + # self.host.status_bar.on_presence_click() + # elif command == 'status': + # if args: + # self.host.status_bar.on_change(user_data=sat_widgets.AdvancedEdit(args[0])) + # else: + # self.host.status_bar.on_status_click() + elif command == 'history': + widget = self.host.selected_widget + if isinstance(widget, quick_chat.QuickChat): + try: + limit = int(args[0]) + except (IndexError, ValueError): + limit = 50 + widget.update_history(size=limit, profile=widget.profile) + elif command == 'search': + widget = self.host.selected_widget + if isinstance(widget, quick_chat.QuickChat): + pattern = " ".join(args) + if not pattern: + self.host.notif_bar.add_message(D_("Please specify the globbing pattern to search for")) + else: + widget.update_history(size=C.HISTORY_LIMIT_NONE, filters={'search': pattern}, profile=widget.profile) + elif command == 'filter': + # FIXME: filter is now only for current widget, + # need to be able to set it globally or per widget + widget = self.host.selected_widget + # FIXME: Q&D way, need to be more generic + if isinstance(widget, quick_chat.QuickChat): + widget.set_filter(args) + elif command in ('topic', 'suject', 'title'): + try: + new_title = args[0].strip() + except IndexError: + new_title = None + widget = self.host.selected_widget + if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP: + widget.on_subject_dialog(new_title) + else: + return + self.set_edit_text('') + + def _history_cb(self, text): + self.set_edit_text(text) + self.set_edit_pos(len(text)) + + def keypress(self, size, key): + """Callback when a key is pressed. Send "composing" states + and move the index of the temporary history stack.""" + if key == a_key['MODAL_ESCAPE']: + # first save the text to the current mode, then change to NORMAL + self.host._update_input_history(self.get_edit_text(), mode=self.mode) + self.host._update_input_history(mode=C.MODE_NORMAL) + if self._mode == C.MODE_NORMAL and key in self._modes: + self.host._update_input_history(mode=self._modes[key][0]) + if key == a_key['HISTORY_PREV']: + self.host._update_input_history(self.get_edit_text(), -1, self._history_cb, self.mode) + return + elif key == a_key['HISTORY_NEXT']: + self.host._update_input_history(self.get_edit_text(), +1, self._history_cb, self.mode) + return + elif key == a_key['EDIT_ENTER']: + self.host._update_input_history(self.get_edit_text(), mode=self.mode) + else: + if (self._mode == C.MODE_INSERTION + and isinstance(self.host.selected_widget, quick_chat.QuickChat) + and key not in sat_widgets.FOCUS_KEYS + and key not in (a_key['HISTORY_PREV'], a_key['HISTORY_NEXT']) + and self.host.sync): + self.host.bridge.chat_state_composing(self.host.selected_widget.target, self.host.selected_widget.profile) + + return super(EditBar, self).keypress(size, key) + + +class LiberviaTUITopWidget(sat_widgets.FocusPile): + """Top most widget used in LiberviaTUI""" + _focus_inversed = True + positions = ('menu', 'body', 'notif_bar', 'edit_bar') + can_hide = ('menu', 'notif_bar') + + def __init__(self, body, menu, notif_bar, edit_bar): + self._body = body + self._menu = menu + self._notif_bar = notif_bar + self._edit_bar = edit_bar + self._hidden = {'notif_bar'} + self._focus_extra = False + super(LiberviaTUITopWidget, self).__init__([('pack', self._menu), self._body, ('pack', self._edit_bar)]) + for position in self.positions: + setattr(self, + position, + property(lambda: self, self.widget_get(position=position), + lambda pos, new_wid: self.widget_set(new_wid, position=pos)) + ) + self.focus_position = len(self.contents)-1 + + def get_visible_positions(self, keep=None): + """Return positions that are not hidden in the right order + + @param keep: if not None, this position will be keep in the right order, even if it's hidden + (can be useful to find its index) + @return (list): list of visible positions + """ + return [pos for pos in self.positions if (keep and pos == keep) or pos not in self._hidden] + + def keypress(self, size, key): + """Manage FOCUS keys that focus directly a main part (one of self.positions) + + To avoid key conflicts, a combinaison must be made with FOCUS_EXTRA then an other key + """ + if key == a_key['FOCUS_EXTRA']: + self._focus_extra = True + return + if self._focus_extra: + self._focus_extra = False + if key in ('m', '1'): + focus = 'menu' + elif key in ('b', '2'): + focus = 'body' + elif key in ('n', '3'): + focus = 'notif_bar' + elif key in ('e', '4'): + focus = 'edit_bar' + else: + return super(LiberviaTUITopWidget, self).keypress(size, key) + + if focus in self._hidden: + return + + self.focus_position = self.get_visible_positions().index(focus) + return + + return super(LiberviaTUITopWidget, self).keypress(size, key) + + def widget_get(self, position): + if not position in self.positions: + raise ValueError("Unknown position {}".format(position)) + return getattr(self, "_{}".format(position)) + + def widget_set(self, widget, position): + if not position in self.positions: + raise ValueError("Unknown position {}".format(position)) + return setattr(self, "_{}".format(position), widget) + + def hide_switch(self, position): + if not position in self.can_hide: + raise ValueError("Can't switch position {}".format(position)) + hide = not position in self._hidden + widget = self.widget_get(position) + idx = self.get_visible_positions(position).index(position) + if hide: + del self.contents[idx] + self._hidden.add(position) + else: + self.contents.insert(idx, (widget, ('pack', None))) + self._hidden.remove(position) + + def show(self, position): + if position in self._hidden: + self.hide_switch(position) + + def hide(self, position): + if not position in self._hidden: + self.hide_switch(position) + + +class LiberviaTUIApp(QuickApp, InputHistory): + MB_HANDLER = False + AVATARS_HANDLER = False + + def __init__(self): + bridge_module = dynamic_import.bridge(bridge_name, 'libervia.frontends.bridge') + if bridge_module is None: + log.error(u"Can't import {} bridge".format(bridge_name)) + sys.exit(3) + else: + log.debug(u"Loading {} bridge".format(bridge_name)) + QuickApp.__init__(self, bridge_factory=bridge_module.bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False) + ## main loop setup ## + event_loop = urwid.GLibEventLoop if 'dbus' in bridge_name else urwid.TwistedEventLoop + self.loop = urwid.MainLoop(urwid.SolidFill(), C.PALETTE, event_loop=event_loop(), input_filter=self.input_filter, unhandled_input=self.key_handler) + + @classmethod + def run(cls): + cls().start() + + def on_bridge_connected(self): + + ##misc setup## + self._visible_widgets = set() + self.notif_bar = sat_widgets.NotificationBar() + urwid.connect_signal(self.notif_bar, 'change', self.on_notification) + + self.progress_wid = self.widgets.get_or_create_widget(Progress, None, on_new_widget=None) + urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.select_widget(self.progress_wid)) + self.__saved_overlay = None + + self.x_notify = Notify() + + # we already manage exit with a_key['APP_QUIT'], so we don't want C-c + signal.signal(signal.SIGINT, signal.SIG_IGN) + sat_conf = sat_config.parse_main_conf() + self._bracketed_paste = C.bool( + sat_config.config_get(sat_conf, C.CONFIG_SECTION, 'bracketed_paste', 'false') + ) + if self._bracketed_paste: + log.debug("setting bracketed paste mode as requested") + sys.stdout.write("\033[?2004h") + self._bracketed_mode_set = True + + self.loop.widget = self.main_widget = ProfileManager(self) + self.post_init() + + @property + def visible_widgets(self): + return self._visible_widgets + + @property + def mode(self): + return self.editBar.mode + + @mode.setter + def mode(self, value): + self.editBar.mode = value + + def mode_hint(self, value): + """Change mode if make sens (i.e.: if there is nothing in the editBar)""" + if not self.editBar.get_edit_text(): + self.mode = value + + def debug(self): + """convenient method to reset screen and launch (i)p(u)db""" + log.info('Entered debug mode') + try: + import pudb + pudb.set_trace() + except ImportError: + import os + os.system('reset') + try: + import ipdb + ipdb.set_trace() + except ImportError: + import pdb + pdb.set_trace() + + def redraw(self): + """redraw the screen""" + try: + self.loop.draw_screen() + except AttributeError: + pass + + def start(self): + self.connect_bridge() + self.loop.run() + + def post_init(self): + try: + config.apply_config(self) + except Exception as e: + log.error(u"configuration error: {}".format(e)) + popup = self.alert(_(u"Configuration Error"), _(u"Something went wrong while reading the configuration, please check :messages")) + if self.options.profile: + self._early_popup = popup + else: + self.show_pop_up(popup) + super(LiberviaTUIApp, self).post_init(self.main_widget) + + def keys_to_text(self, keys): + """Generator return normal text from urwid keys""" + for k in keys: + if k == 'tab': + yield u'\t' + elif k == 'enter': + yield u'\n' + elif is_wide_char(k,0) or (len(k)==1 and ord(k) >= 32): + yield k + + def input_filter(self, input_, raw): + if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']: + return + + ## paste detection/handling + if (len(input_) > 1 and # XXX: it may be needed to increase this value if buffer + not isinstance(input_[0], tuple) and # or other things result in several chars at once + not 'window resize' in input_): # (e.g. using LiberviaTUI through ssh). Need some testing + # and experience to adjust value. + if input_[0] == 'begin paste' and not self._bracketed_paste: + log.info(u"Bracketed paste mode detected") + self._bracketed_paste = True + + if self._bracketed_paste: + # after this block, extra will contain non pasted keys + # and input_ will contain pasted keys + try: + begin_idx = input_.index('begin paste') + except ValueError: + # this is not a paste, maybe we have something buffering + # or bracketed mode is set in conf but not enabled in term + extra = input_ + input_ = [] + else: + try: + end_idx = input_.index('end paste') + except ValueError: + log.warning(u"missing end paste sequence, discarding paste") + extra = input_[:begin_idx] + del input_[begin_idx:] + else: + extra = input_[:begin_idx] + input_[end_idx+1:] + input_ = input_[begin_idx+1:end_idx] + else: + extra = None + + log.debug(u"Paste detected (len {})".format(len(input_))) + try: + edit_bar = self.editBar + except AttributeError: + log.warning(u"Paste treated as normal text: there is no edit bar yet") + if extra is None: + extra = [] + extra.extend(input_) + else: + if self.main_widget.focus == edit_bar: + # XXX: if a paste is detected, we append it directly to the edit bar text + # so the user can check it and press [enter] if it's OK + buf_paste = u''.join(self.keys_to_text(input_)) + pos = edit_bar.edit_pos + edit_bar.set_edit_text(u'{}{}{}'.format(edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:])) + edit_bar.edit_pos+=len(buf_paste) + else: + # we are not on the edit_bar, + # so we treat pasted text as normal text + if extra is None: + extra = [] + extra.extend(input_) + if not extra: + return + input_ = extra + ## end of paste detection/handling + + for i in input_: + if isinstance(i,tuple): + if i[0] == 'mouse press': + if i[1] == 4: #Mouse wheel up + input_[input_.index(i)] = a_key['HISTORY_PREV'] + if i[1] == 5: #Mouse wheel down + input_[input_.index(i)] = a_key['HISTORY_NEXT'] + return input_ + + def key_handler(self, input_): + if input_ == a_key['MENU_HIDE']: + """User want to (un)hide the menu roller""" + try: + self.main_widget.hide_switch('menu') + except AttributeError: + pass + elif input_ == a_key['NOTIFICATION_NEXT']: + """User wants to see next notification""" + self.notif_bar.show_next() + elif input_ == a_key['OVERLAY_HIDE']: + """User wants to (un)hide overlay window""" + if isinstance(self.loop.widget,urwid.Overlay): + self.__saved_overlay = self.loop.widget + self.loop.widget = self.main_widget + else: + if self.__saved_overlay: + self.loop.widget = self.__saved_overlay + self.__saved_overlay = None + + elif input_ == a_key['DEBUG'] and 'D' in self.bridge.version_get(): #Debug only for dev versions + self.debug() + elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists + try: + for wid, options in self.center_part.contents: + if self.contact_lists_pile is wid: + self.center_part.contents.remove((wid, options)) + break + else: + self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False))) + except AttributeError: + #The main widget is not built (probably in Profile Manager) + pass + elif input_ == 'window resize': + width,height = self.loop.screen_size + if height<=5 and width<=35: + if not 'save_main_widget' in dir(self): + self.save_main_widget = self.loop.widget + self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !"))) + else: + if 'save_main_widget' in dir(self): + self.loop.widget = self.save_main_widget + del self.save_main_widget + try: + return self.menu_roller.check_shortcuts(input_) + except AttributeError: + return input_ + + def add_menus(self, menu, type_filter, menu_data=None): + """Add cached menus to instance + @param menu: sat_widgets.Menu instance + @param type_filter: menu type like is sat.core.sat_main.import_menu + @param menu_data: data to send with these menus + + """ + def add_menu_cb(callback_id): + self.action_launch(callback_id, menu_data, profile=self.current_profile) + for id_, type_, path, path_i18n, extra in self.bridge.menus_get("", C.NO_SECURITY_LIMIT ): # TODO: manage extra + if type_ != type_filter: + continue + if len(path) != 2: + raise NotImplementedError("Menu with a path != 2 are not implemented yet") + menu.add_menu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_)) + + + def _build_menu_roller(self): + menu = sat_widgets.Menu(self.loop) + general = _("General") + menu.add_menu(general, _("Connect"), self.on_connect_request) + menu.add_menu(general, _("Disconnect"), self.on_disconnect_request) + menu.add_menu(general, _("Parameters"), self.on_param) + menu.add_menu(general, _("About"), self.on_about_request) + menu.add_menu(general, _("Exit"), self.on_exit_request, a_key['APP_QUIT']) + menu.add_menu(_("Contacts")) # add empty menu to save the place in the menu order + groups = _("Groups") + menu.add_menu(groups) + menu.add_menu(groups, _("Join room"), self.on_join_room_request, a_key['ROOM_JOIN']) + #additionals menus + #FIXME: do this in a more generic way (in quickapp) + self.add_menus(menu, C.MENU_GLOBAL) + + menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)]) + return menu_roller + + def _build_main_widget(self): + self.contact_lists_pile = urwid.Pile([]) + #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))]) + self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))]) + + self.editBar = EditBar(self) + self.menu_roller = self._build_menu_roller() + self.main_widget = LiberviaTUITopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar) + return self.main_widget + + def plugging_profiles(self): + self.loop.widget = self._build_main_widget() + self.redraw() + try: + # if a popup arrived before main widget is build, we need to show it now + self.show_pop_up(self._early_popup) + except AttributeError: + pass + else: + del self._early_popup + + def profile_plugged(self, profile): + QuickApp.profile_plugged(self, profile) + 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) + self.contact_lists_pile.contents.append((contact_list, ('weight', 1))) + return contact_list + + def is_hidden(self): + """Tells if the frontend window is hidden. + + @return bool + """ + return False # FIXME: implement when necessary + + def alert(self, title, message): + """Shortcut method to create an alert message + + Alert will have an "OK" button, which remove it if pressed + @param title(unicode): title of the dialog + @param message(unicode): body of the dialog + @return (urwid_satext.Alert): the created Alert instance + """ + popup = sat_widgets.Alert(title, message) + popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) + self.show_pop_up(popup, width=75, height=20) + return popup + + def remove_pop_up(self, widget=None): + """Remove current pop-up, and if there is other in queue, show it + + @param widget(None, urwid.Widget): if not None remove this popup from front or queue + """ + # TODO: refactor popup management in a cleaner way + # buttons' callback use themselve as first argument, and we never use + # a Button directly in a popup, so we consider urwid.Button as None + if widget is not None and not isinstance(widget, urwid.Button): + if isinstance(self.loop.widget, urwid.Overlay): + current_popup = self.loop.widget.top_w + if not current_popup == widget: + try: + self.notif_bar.remove_pop_up(widget) + except ValueError: + log.warning(u"Trying to remove an unknown widget {}".format(widget)) + return + self.loop.widget = self.main_widget + next_popup = self.notif_bar.get_next_popup() + if next_popup: + #we still have popup to show, we display it + self.show_pop_up(next_popup) + else: + self.redraw() + + def show_pop_up(self, pop_up_widget, width=None, height=None, align='center', + valign='middle'): + """Show a pop-up window if possible, else put it in queue + + @param pop_up_widget: pop up to show + @param width(int, None): width of the popup + None to use default + @param height(int, None): height of the popup + None to use default + @param align: same as for [urwid.Overlay] + """ + if width == None: + width = 75 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) else 135 + if height == None: + height = 20 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) else 40 + if not isinstance(self.loop.widget, urwid.Overlay): + display_widget = urwid.Overlay( + pop_up_widget, self.main_widget, align, width, valign, height) + self.loop.widget = display_widget + self.redraw() + else: + self.notif_bar.add_pop_up(pop_up_widget) + + def bar_notify(self, message): + """"Notify message to user via notification bar""" + self.notif_bar.add_message(message) + self.redraw() + + def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE): + if widget is None or widget is not None and widget != self.selected_widget: + # we ignore notification if the widget is selected but we can + # still do a desktop notification is the X window has not the focus + super(LiberviaTUIApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile) + # we don't want notifications without message on desktop + if message is not None and not self.x_notify.has_focus(): + if message is None: + message = _("{app}: a new event has just happened{entity}").format( + app=C.APP_NAME, + entity=u' ({})'.format(entity) if entity else '') + self.x_notify.send_notification(message) + + + def new_widget(self, widget, user_action=False): + """Method called when a new widget is created + + if suitable, the widget will be displayed + @param widget(widget.LiberviaTUIWidget): created widget + @param user_action(bool): if True, the widget has been created following an + explicit user action. In this case, the widget may get focus immediately + """ + # FIXME: when several widgets are possible (e.g. with :split) + # do not replace current widget when self.selected_widget != None + if user_action or self.selected_widget is None: + self.select_widget(widget) + + def select_widget(self, widget): + """Display a widget if possible, + + else add it in the notification bar queue + @param widget: BoxWidget + """ + assert len(self.center_part.widget_list)<=2 + wid_idx = len(self.center_part.widget_list)-1 + self.center_part.widget_list[wid_idx] = widget + try: + self.menu_roller.remove_menu(C.MENU_ID_WIDGET) + except KeyError: + log.debug("No menu to delete") + self.selected_widget = widget + try: + on_selected = self.selected_widget.on_selected + except AttributeError: + pass + else: + on_selected() + self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now + self.contact_lists.select(None) + + for wid in self.visible_widgets: # FIXME: check if widgets.get_widgets is not more appropriate + if isinstance(wid, Chat): + contact_list = self.contact_lists[wid.profile] + contact_list.select(wid.target) + + self.redraw() + + def remove_window(self): + """Remove window showed on the right column""" + #TODO: better Window management than this hack + assert len(self.center_part.widget_list) <= 2 + wid_idx = len(self.center_part.widget_list)-1 + self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text('')) + self.center_part.focus_position = 0 + self.redraw() + + def add_progress(self, pid, message, profile): + """Follow a SàT progression + + @param pid: progression id + @param message: message to show to identify the progression + """ + self.progress_wid.add(pid, message, profile) + + def set_progress(self, percentage): + """Set the progression shown in notification bar""" + self.notif_bar.set_progress(percentage) + + def contact_selected(self, contact_list, entity): + self.clear_notifs(entity, profile=contact_list.profile) + if entity.resource: + # we have clicked on a private MUC conversation + 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) + else: + chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, profile=contact_list.profile) + self.select_widget(chat_widget) + self.menu_roller.add_menu(_('Chat menu'), chat_widget.get_menu(), C.MENU_ID_WIDGET) + + def _dialog_ok_cb(self, widget, data): + popup, answer_cb, answer_data = data + self.remove_pop_up(popup) + if answer_cb is not None: + answer_cb(True, answer_data) + + def _dialog_cancel_cb(self, widget, data): + popup, answer_cb, answer_data = data + self.remove_pop_up(popup) + if answer_cb is not None: + answer_cb(False, answer_data) + + def show_dialog(self, message, title="", type="info", answer_cb = None, answer_data = None): + if type == 'info': + popup = sat_widgets.Alert(title, message, ok_cb=answer_cb) + if answer_cb is None: + popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) + elif type == 'error': + popup = sat_widgets.Alert(title, message, ok_cb=answer_cb) + if answer_cb is None: + popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) + elif type == 'yes/no': + popup = sat_widgets.ConfirmDialog(message) + popup.set_callback('yes', self._dialog_ok_cb, (popup, answer_cb, answer_data)) + popup.set_callback('no', self._dialog_cancel_cb, (popup, answer_cb, answer_data)) + else: + popup = sat_widgets.Alert(title, message, ok_cb=answer_cb) + if answer_cb is None: + popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup)) + log.error(u'unmanaged dialog type: {}'.format(type)) + self.show_pop_up(popup) + + def dialog_failure(self, failure): + """Show a failure that has been returned by an asynchronous bridge method. + + @param failure (defer.Failure): Failure instance + """ + self.alert(failure.classname, failure.message) + + def on_notification(self, notif_bar): + """Called when a new notification has been received""" + if not isinstance(self.main_widget, LiberviaTUITopWidget): + #if we are not in the main configuration, we ignore the notifications bar + return + if self.notif_bar.can_hide(): + #No notification left, we can hide the bar + self.main_widget.hide('notif_bar') + else: + self.main_widget.show('notif_bar') + self.redraw() # FIXME: invalidate cache in a more efficient way + + def _action_manager_unknown_error(self): + self.alert(_("Error"), _(u"Unmanaged action")) + + def room_joined_handler(self, room_jid_s, room_nicks, user_nick, subject, profile): + super(LiberviaTUIApp, self).room_joined_handler(room_jid_s, room_nicks, user_nick, subject, profile) + # if self.selected_widget is None: + # for contact_list in self.widgets.get_widgets(ContactList): + # if profile in contact_list.profiles: + # contact_list.set_focus(jid.JID(room_jid_s), True) + + def progress_started_handler(self, pid, metadata, profile): + super(LiberviaTUIApp, self).progress_started_handler(pid, metadata, profile) + self.add_progress(pid, metadata.get('name', _(u'unkown')), profile) + + def progress_finished_handler(self, pid, metadata, profile): + log.info(u"Progress {} finished".format(pid)) + super(LiberviaTUIApp, self).progress_finished_handler(pid, metadata, profile) + + def progress_error_handler(self, pid, err_msg, profile): + log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg)) + super(LiberviaTUIApp, self).progress_error_handler(pid, err_msg, profile) + + + ##DIALOGS CALLBACKS## + def on_join_room(self, button, edit): + self.remove_pop_up() + room_jid = jid.JID(edit.get_edit_text()) + self.bridge.muc_join(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialog_failure) + + #MENU EVENTS# + def on_connect_request(self, menu): + QuickApp.connect(self, self.current_profile) + + def on_disconnect_request(self, menu): + self.disconnect(self.current_profile) + + def on_param(self, menu): + def success(params): + ui = xmlui.create(self, xml_data=params, profile=self.current_profile) + ui.show() + + def failure(error): + self.alert(_("Error"), _("Can't get parameters (%s)") % error) + self.bridge.param_ui_get(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure) + + def on_exit_request(self, menu): + QuickApp.on_exit(self) + try: + if self._bracketed_mode_set: # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf) + log.debug("unsetting bracketed paste mode") + sys.stdout.write("\033[?2004l") + except AttributeError: + pass + raise urwid.ExitMainLoop() + + def on_join_room_request(self, menu): + """User wants to join a MUC room""" + 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) + pop_up_widget.set_callback('cancel', lambda dummy: self.remove_pop_up(pop_up_widget)) + self.show_pop_up(pop_up_widget) + + def on_about_request(self, menu): + self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get()) + + #MISC CALLBACKS# + + def set_presence_status(self, show='', status=None, profile=C.PROF_KEY_NONE): + contact_list_wid = self.widgets.get_widget(ContactList, profiles=profile) + if contact_list_wid is not None: + contact_list_wid.status_bar.set_presence_status(show, status) + else: + log.warning(u"No ContactList widget found for profile {}".format(profile)) + +if __name__ == '__main__': + LiberviaTUIApp().start() diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/chat.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/chat.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,708 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from functools import total_ordering +from pathlib import Path +import bisect +import urwid +from urwid_satext import sat_widgets +from libervia.backend.core.i18n import _ +from libervia.backend.core import log as logging +from libervia.frontends.quick_frontend import quick_widgets +from libervia.frontends.quick_frontend import quick_chat +from libervia.frontends.quick_frontend import quick_games +from libervia.tui import game_tarot +from libervia.tui.constants import Const as C +from libervia.tui.keys import action_key_map as a_key +from libervia.tui.widget import LiberviaTUIWidget +from libervia.tui.contact_list import ContactList + + +log = logging.getLogger(__name__) + + +OCCUPANTS_FOOTER = _("{} occupants") + + +class MessageWidget(urwid.WidgetWrap, quick_chat.MessageWidget): + def __init__(self, mess_data): + """ + @param mess_data(quick_chat.Message, None): message data + None: used only for non text widgets (e.g.: focus separator) + """ + self.mess_data = mess_data + mess_data.widgets.add(self) + super(MessageWidget, self).__init__(urwid.Text(self.markup)) + + @property + def markup(self): + return ( + self._generate_info_markup() + if self.mess_data.type == C.MESS_TYPE_INFO + else self._generate_markup() + ) + + @property + def info_type(self): + return self.mess_data.info_type + + @property + def parent(self): + return self.mess_data.parent + + @property + def message(self): + """Return currently displayed message""" + return self.mess_data.main_message + + @message.setter + def message(self, value): + self.mess_data.message = {"": value} + self.redraw() + + @property + def type(self): + try: + return self.mess_data.type + except AttributeError: + return C.MESS_TYPE_INFO + + def redraw(self): + self._w.set_text(self.markup) + self.mess_data.parent.host.redraw() # FIXME: should not be necessary + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + def get_cursor_coords(self, size): + return 0, 0 + + def render(self, size, focus=False): + # Text widget doesn't render cursor, but we want one + # so we add it here + canvas = urwid.CompositeCanvas(self._w.render(size, focus)) + if focus: + canvas.set_cursor(self.get_cursor_coords(size)) + return canvas + + def _generate_info_markup(self): + return ("info_msg", self.message) + + def _generate_markup(self): + """Generate text markup according to message data and Widget options""" + markup = [] + d = self.mess_data + mention = d.mention + + # message status + if d.status is None: + markup.append(" ") + elif d.status == "delivered": + markup.append(("msg_status_received", "✔")) + else: + log.warning("Unknown status: {}".format(d.status)) + + # timestamp + if self.parent.show_timestamp: + attr = "msg_mention" if mention else "date" + markup.append((attr, "[{}]".format(d.time_text))) + else: + if mention: + markup.append(("msg_mention", "[*]")) + + # nickname + if self.parent.show_short_nick: + markup.append( + ("my_nick" if d.own_mess else "other_nick", "**" if d.own_mess else "*") + ) + else: + markup.append( + ("my_nick" if d.own_mess else "other_nick", "[{}] ".format(d.nick or "")) + ) + + msg = self.message # needed to generate self.selected_lang + + if d.selected_lang: + markup.append(("msg_lang", "[{}] ".format(d.selected_lang))) + + # message body + markup.append(msg) + + return markup + + # events + def update(self, update_dict=None): + """update all the linked message widgets + + @param update_dict(dict, None): key=attribute updated value=new_value + """ + self.redraw() + + +@total_ordering +class OccupantWidget(urwid.WidgetWrap): + def __init__(self, occupant_data): + self.occupant_data = occupant_data + occupant_data.widgets.add(self) + markup = self._generate_markup() + text = sat_widgets.ClickableText(markup) + urwid.connect_signal( + text, + "click", + self.occupant_data.parent._occupants_clicked, + user_args=[self.occupant_data], + ) + super(OccupantWidget, self).__init__(text) + + def __hash__(self): + return id(self) + + def __eq__(self, other): + if other is None: + return False + return self.occupant_data.nick == other.occupant_data.nick + + def __lt__(self, other): + return self.occupant_data.nick.lower() < other.occupant_data.nick.lower() + + @property + def markup(self): + return self._generate_markup() + + @property + def parent(self): + return self.mess_data.parent + + @property + def nick(self): + return self.occupant_data.nick + + def redraw(self): + self._w.set_text(self.markup) + self.occupant_data.parent.host.redraw() # FIXME: should not be necessary + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + def get_cursor_coords(self, size): + return 0, 0 + + def render(self, size, focus=False): + # Text widget doesn't render cursor, but we want one + # so we add it here + canvas = urwid.CompositeCanvas(self._w.render(size, focus)) + if focus: + canvas.set_cursor(self.get_cursor_coords(size)) + return canvas + + def _generate_markup(self): + # TODO: role and affiliation are shown in a Q&D way + # should be more intuitive and themable + o = self.occupant_data + markup = [] + markup.append( + ("info_msg", "{}{} ".format(o.role[0].upper(), o.affiliation[0].upper())) + ) + markup.append(o.nick) + if o.state is not None: + markup.append(" {}".format(C.CHAT_STATE_ICON[o.state])) + return markup + + # events + def update(self, update_dict=None): + self.redraw() + + +class OccupantsWidget(urwid.WidgetWrap): + def __init__(self, parent): + self.parent = parent + self.occupants_walker = urwid.SimpleListWalker([]) + self.occupants_footer = urwid.Text("", align="center") + self.update_footer() + occupants_widget = urwid.Frame( + urwid.ListBox(self.occupants_walker), footer=self.occupants_footer + ) + super(OccupantsWidget, self).__init__(occupants_widget) + occupants_list = sorted(list(self.parent.occupants.keys()), key=lambda o: o.lower()) + for occupant in occupants_list: + occupant_data = self.parent.occupants[occupant] + self.occupants_walker.append(OccupantWidget(occupant_data)) + + def clear(self): + del self.occupants_walker[:] + + def update_footer(self): + """update footer widget""" + txt = OCCUPANTS_FOOTER.format(len(self.parent.occupants)) + self.occupants_footer.set_text(txt) + + def get_nicks(self, start=""): + """Return nicks of all occupants + + @param start(unicode): only return nicknames which start with this text + """ + return [ + w.nick + for w in self.occupants_walker + if isinstance(w, OccupantWidget) and w.nick.startswith(start) + ] + + def addUser(self, occupant_data): + """add a user to the list""" + bisect.insort(self.occupants_walker, OccupantWidget(occupant_data)) + self.update_footer() + self.parent.host.redraw() # FIXME: should not be necessary + + def removeUser(self, occupant_data): + """remove a user from the list""" + for widget in occupant_data.widgets: + self.occupants_walker.remove(widget) + self.update_footer() + self.parent.host.redraw() # FIXME: should not be necessary + + +class Chat(LiberviaTUIWidget, quick_chat.QuickChat): + def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, + subject=None, statuses=None, profiles=None): + self.filters = [] # list of filter callbacks to apply + self.mess_walker = urwid.SimpleListWalker([]) + self.mess_widgets = urwid.ListBox(self.mess_walker) + self.chat_widget = urwid.Frame(self.mess_widgets) + self.chat_colums = urwid.Columns([("weight", 8, self.chat_widget)]) + self.pile = urwid.Pile([self.chat_colums]) + LiberviaTUIWidget.__init__(self, self.pile, target) + quick_chat.QuickChat.__init__( + self, host, target, type_, nick, occupants, subject, statuses, + profiles=profiles + ) + + # we must adapt the behaviour with the type + if type_ == C.CHAT_GROUP: + if len(self.chat_colums.contents) == 1: + self.occupants_widget = OccupantsWidget(self) + self.occupants_panel = sat_widgets.VerticalSeparator( + self.occupants_widget + ) + self._append_occupants_panel() + self.host.addListener("presence", self.presence_listener, [profiles]) + + # focus marker is a separator indicated last visible message before focus was lost + self.focus_marker = None # link to current marker + self.focus_marker_set = None # True if a new marker has been inserted + self.show_timestamp = True + self.show_short_nick = False + self.show_title = 1 # 0: clip title; 1: full title; 2: no title + self.post_init() + + @property + def message_widgets_rev(self): + return reversed(self.mess_walker) + + def keypress(self, size, key): + if key == a_key["OCCUPANTS_HIDE"]: # user wants to (un)hide the occupants panel + if self.type == C.CHAT_GROUP: + widgets = [widget for (widget, options) in self.chat_colums.contents] + if self.occupants_panel in widgets: + self._remove_occupants_panel() + else: + self._append_occupants_panel() + elif key == a_key["TIMESTAMP_HIDE"]: # user wants to (un)hide timestamp + self.show_timestamp = not self.show_timestamp + self.redraw() + elif key == a_key["SHORT_NICKNAME"]: # user wants to (not) use short nick + self.show_short_nick = not self.show_short_nick + self.redraw() + elif (key == a_key["SUBJECT_SWITCH"]): + # user wants to (un)hide group's subject or change its apperance + if self.subject: + self.show_title = (self.show_title + 1) % 3 + if self.show_title == 0: + self.set_subject(self.subject, "clip") + elif self.show_title == 1: + self.set_subject(self.subject, "space") + elif self.show_title == 2: + self.chat_widget.header = None + self._invalidate() + elif key == a_key["GOTO_BOTTOM"]: # user wants to focus last message + self.mess_widgets.focus_position = len(self.mess_walker) - 1 + + return super(Chat, self).keypress(size, key) + + def completion(self, text, completion_data): + """Completion method which complete nicknames in group chat + + for params, see [sat_widgets.AdvancedEdit] + """ + if self.type != C.CHAT_GROUP: + return text + + space = text.rfind(" ") + start = text[space + 1 :] + words = self.occupants_widget.get_nicks(start) + if not words: + return text + try: + word_idx = words.index(completion_data["last_word"]) + 1 + except (KeyError, ValueError): + word_idx = 0 + else: + if word_idx == len(words): + word_idx = 0 + word = completion_data["last_word"] = words[word_idx] + return "{}{}{}".format(text[: space + 1], word, ": " if space < 0 else "") + + def get_menu(self): + """Return Menu bar""" + menu = sat_widgets.Menu(self.host.loop) + if self.type == C.CHAT_GROUP: + self.host.add_menus(menu, C.MENU_ROOM, {"room_jid": self.target.bare}) + game = _("Game") + menu.add_menu(game, "Tarot", self.on_tarot_request) + elif self.type == C.CHAT_ONE2ONE: + # FIXME: self.target is a bare jid, we need to check that + contact_list = self.host.contact_lists[self.profile] + if not self.target.resource: + full_jid = contact_list.get_full_jid(self.target) + else: + full_jid = self.target + self.host.add_menus(menu, C.MENU_SINGLE, {"jid": full_jid}) + return menu + + def set_filter(self, args): + """set filtering of messages + + @param args(list[unicode]): filters following syntax "[filter]=[value]" + empty list to clear all filters + only lang=XX is handled for now + """ + del self.filters[:] + if args: + if args[0].startswith("lang="): + lang = args[0][5:].strip() + self.filters.append(lambda mess_data: lang in mess_data.message) + + self.print_messages() + + def presence_listener(self, entity, show, priority, statuses, profile): + """Update entity's presence status + + @param entity (jid.JID): entity updated + @param show: availability + @param priority: resource's priority + @param statuses: dict of statuses + @param profile: %(doc_profile)s + """ + # FIXME: disable for refactoring, need to be checked and re-enabled + return + # assert self.type == C.CHAT_GROUP + # if entity.bare != self.target: + # return + # self.update(entity) + + def create_message(self, message): + self.appendMessage(message) + + def _scrollDown(self): + """scroll down message only if we are already at the bottom (minus 1)""" + current_focus = self.mess_widgets.focus_position + bottom = len(self.mess_walker) - 1 + if current_focus == bottom - 1: + self.mess_widgets.focus_position = bottom # scroll down + self.host.redraw() # FIXME: should not be necessary + + def appendMessage(self, message, minor_notifs=True): + """Create a MessageWidget and append it + + Can merge info messages together if desirable (e.g.: multiple joined/leave) + @param message(quick_chat.Message): message to add + @param minor_notifs(boolean): if True, basic notifications are allowed + If False, notification are not shown except if we have an important one + (like a mention). + False is generally used when printing history, when we don't want every + message to be notified. + """ + if message.attachments: + # FIXME: Q&D way to see attachments in LiberviaTUI + # it should be done in a more user friendly way + for lang, body in message.message.items(): + for attachment in message.attachments: + if 'url' in attachment: + body+=f"\n{attachment['url']}" + elif 'path' in attachment: + path = Path(attachment['path']) + body+=f"\n{path.as_uri()}" + else: + log.warning(f'No "url" nor "path" in attachment: {attachment}') + message.message[lang] = body + + if self.filters: + if not all([f(message) for f in self.filters]): + return + + if self.handle_user_moved(message): + return + + if ((self.host.selected_widget != self or not self.host.x_notify.has_focus()) + and self.focus_marker_set is not None): + if not self.focus_marker_set and not self._locked and self.mess_walker: + if self.focus_marker is not None: + try: + self.mess_walker.remove(self.focus_marker) + except ValueError: + # self.focus_marker may not be in mess_walker anymore if + # mess_walker has been cleared, e.g. when showing search + # result or using :history command + pass + self.focus_marker = urwid.Divider("—") + self.mess_walker.append(self.focus_marker) + self.focus_marker_set = True + self._scrollDown() + else: + if self.focus_marker_set: + self.focus_marker_set = False + + wid = MessageWidget(message) + self.mess_walker.append(wid) + self._scrollDown() + if self.is_user_moved(message): + return # no notification for moved messages + + # notifications + + if self._locked: + # we don't want notifications when locked + # because that's history messages + return + + if wid.mess_data.mention: + from_jid = wid.mess_data.from_jid + msg = _( + "You have been mentioned by {nick} in {room}".format( + nick=wid.mess_data.nick, room=self.target + ) + ) + self.host.notify( + C.NOTIFY_MENTION, from_jid, msg, widget=self, profile=self.profile + ) + elif not minor_notifs: + return + elif self.type == C.CHAT_ONE2ONE: + from_jid = wid.mess_data.from_jid + msg = _("{entity} is talking to you".format(entity=from_jid)) + self.host.notify( + C.NOTIFY_MESSAGE, from_jid, msg, widget=self, profile=self.profile + ) + else: + self.host.notify( + C.NOTIFY_MESSAGE, self.target, widget=self, profile=self.profile + ) + + def addUser(self, nick): + occupant = super(Chat, self).addUser(nick) + self.occupants_widget.addUser(occupant) + + def removeUser(self, occupant_data): + occupant = super(Chat, self).removeUser(occupant_data) + if occupant is not None: + self.occupants_widget.removeUser(occupant) + + def occupants_clear(self): + super(Chat, self).occupants_clear() + self.occupants_widget.clear() + + def _occupants_clicked(self, occupant, clicked_wid): + assert self.type == C.CHAT_GROUP + contact_list = self.host.contact_lists[self.profile] + + # we have a click on a nick, we need to create the widget if it doesn't exists + self.get_or_create_private_widget(occupant.jid) + + # now we select the new window + for contact_list in self.host.widgets.get_widgets( + ContactList, profiles=(self.profile,) + ): + contact_list.set_focus(occupant.jid, True) + + def _append_occupants_panel(self): + self.chat_colums.contents.append((self.occupants_panel, ("weight", 2, False))) + + def _remove_occupants_panel(self): + for widget, options in self.chat_colums.contents: + if widget is self.occupants_panel: + self.chat_colums.contents.remove((widget, options)) + break + + def add_game_panel(self, widget): + """Insert a game panel to this Chat dialog. + + @param widget (Widget): the game panel + """ + assert len(self.pile.contents) == 1 + self.pile.contents.insert(0, (widget, ("weight", 1))) + self.pile.contents.insert(1, (urwid.Filler(urwid.Divider("-"), ("fixed", 1)))) + self.host.redraw() + + def remove_game_panel(self, widget): + """Remove the game panel from this Chat dialog. + + @param widget (Widget): the game panel + """ + assert len(self.pile.contents) == 3 + del self.pile.contents[0] + self.host.redraw() + + def set_subject(self, subject, wrap="space"): + """Set title for a group chat""" + quick_chat.QuickChat.set_subject(self, subject) + self.subj_wid = urwid.Text( + str(subject.replace("\n", "|") if wrap == "clip" else subject), + align="left" if wrap == "clip" else "center", + wrap=wrap, + ) + self.chat_widget.header = urwid.AttrMap(self.subj_wid, "title") + self.host.redraw() + + ## Messages + + def print_messages(self, clear=True): + """generate message widgets + + @param clear(bool): clear message before printing if true + """ + if clear: + del self.mess_walker[:] + for message in self.messages.values(): + self.appendMessage(message, minor_notifs=False) + + def redraw(self): + """redraw all messages""" + for w in self.mess_walker: + try: + w.redraw() + except AttributeError: + pass + + def update_history(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile="@NONE@"): + del self.mess_walker[:] + if filters and "search" in filters: + self.mess_walker.append( + urwid.Text( + _("Results for searching the globbing pattern: {}").format( + filters["search"] + ) + ) + ) + self.mess_walker.append( + urwid.Text(_("Type ':history ' to reset the chat history")) + ) + super(Chat, self).update_history(size, filters, profile) + + def _on_history_printed(self): + """Refresh or scroll down the focus after the history is printed""" + self.print_messages(clear=False) + super(Chat, self)._on_history_printed() + + def on_private_created(self, widget): + self.host.contact_lists[widget.profile].set_special( + widget.target, C.CONTACT_SPECIAL_GROUP + ) + + def on_selected(self): + self.focus_marker_set = False + + def notify(self, contact="somebody", msg=""): + """Notify the user of a new message if Libervia TUI doesn't have the focus. + + @param contact (unicode): contact who wrote to the users + @param msg (unicode): the message that has been received + """ + # FIXME: not called anymore after refactoring + if msg == "": + return + if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2: + # we don't change focus if user is not at the bottom + # as that mean that he is probably watching discussion history + self.mess_widgets.focus_position = len(self.mess_walker) - 1 + self.host.redraw() + if not self.host.x_notify.has_focus(): + if self.type == C.CHAT_ONE2ONE: + self.host.x_notify.send_notification( + _("LiberviaTUI: %s is talking to you") % contact + ) + elif self.nick is not None and self.nick.lower() in msg.lower(): + self.host.x_notify.send_notification( + _("LiberviaTUI: %(user)s mentioned you in room '%(room)s'") + % {"user": contact, "room": self.target} + ) + + # MENU EVENTS # + def on_tarot_request(self, menu): + # TODO: move this to plugin_misc_tarot with dynamic menu + if len(self.occupants) != 4: + self.host.show_pop_up( + sat_widgets.Alert( + _("Can't start game"), + _( + "You need to be exactly 4 peoples in the room to start a Tarot game" + ), + ok_cb=self.host.remove_pop_up, + ) + ) + else: + self.host.bridge.tarot_game_create( + self.target, list(self.occupants), self.profile + ) + + # MISC EVENTS # + + def on_delete(self): + # FIXME: to be checked after refactoring + super(Chat, self).on_delete() + if self.type == C.CHAT_GROUP: + self.host.removeListener("presence", self.presence_listener) + + def on_chat_state(self, from_jid, state, profile): + super(Chat, self).on_chat_state(from_jid, state, profile) + if self.type == C.CHAT_ONE2ONE: + self.title_dynamic = C.CHAT_STATE_ICON[state] + self.host.redraw() # FIXME: should not be necessary + + def _on_subject_dialog_cb(self, button, dialog): + self.change_subject(dialog.text) + self.host.remove_pop_up(dialog) + + def on_subject_dialog(self, new_subject=None): + dialog = sat_widgets.InputDialog( + _("Change title"), + _("Enter the new title"), + default_txt=new_subject if new_subject is not None else self.subject, + ) + dialog.set_callback("ok", self._on_subject_dialog_cb, dialog) + dialog.set_callback("cancel", lambda __: self.host.remove_pop_up(dialog)) + self.host.show_pop_up(dialog) + + +quick_widgets.register(quick_chat.QuickChat, Chat) +quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame) diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/config.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""This module manage configuration specific to LiberviaTUI""" + +from libervia.tui.constants import Const as C +from libervia.tui.keys import action_key_map +import configparser + + +def apply_config(host): + """Parse configuration and apply found change + + raise: can raise various Exceptions if configuration is not good + """ + config = configparser.SafeConfigParser() + config.read(C.CONFIG_FILES) + try: + options = config.items(C.CONFIG_SECTION) + except configparser.NoSectionError: + options = [] + shortcuts = {} + for name, value in options: + if name.startswith(C.CONFIG_OPT_KEY_PREFIX.lower()): + action = name[len(C.CONFIG_OPT_KEY_PREFIX) :].upper() + shortcut = value + if not action or not shortcut: + raise ValueError("Bad option: {} = {}".format(name, value)) + shortcuts[action] = shortcut + if name == "disable_mouse": + host.loop.screen.set_mouse_tracking(False) + + action_key_map.replace(shortcuts) + action_key_map.check_namespaces() diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/constants.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.frontends.quick_frontend import constants + + +class Const(constants.Const): + + APP_NAME = "Libervia TUI" + APP_COMPONENT = "TUI" + APP_NAME_ALT = "LiberviaTUI" + APP_NAME_FILE = "libervia_tui" + CONFIG_SECTION = APP_COMPONENT.lower() + PALETTE = [ + ("title", "black", "light gray", "standout,underline"), + ("title_focus", "white,bold", "light gray", "standout,underline"), + ("selected", "default", "dark red"), + ("selected_focus", "default,bold", "dark red"), + ("default", "default", "default"), + ("default_focus", "default,bold", "default"), + ("cl_notifs", "yellow", "default"), + ("cl_notifs_focus", "yellow,bold", "default"), + ("cl_mention", "light red", "default"), + ("cl_mention_focus", "dark red,bold", "default"), + # Messages + ("date", "light gray", "default"), + ("my_nick", "dark red,bold", "default"), + ("other_nick", "dark cyan,bold", "default"), + ("info_msg", "yellow", "default", "bold"), + ("msg_lang", "dark cyan", "default"), + ("msg_mention", "dark red, bold", "default"), + ("msg_status_received", "light green, bold", "default"), + ("menubar", "light gray,bold", "dark red"), + ("menubar_focus", "light gray,bold", "dark green"), + ("selected_menu", "light gray,bold", "dark green"), + ("menuitem", "light gray,bold", "dark red"), + ("menuitem_focus", "light gray,bold", "dark green"), + ("notifs", "black,bold", "yellow"), + ("notifs_focus", "dark red", "yellow"), + ("card_neutral", "dark gray", "white", "standout,underline"), + ("card_neutral_selected", "dark gray", "dark green", "standout,underline"), + ("card_special", "brown", "white", "standout,underline"), + ("card_special_selected", "brown", "dark green", "standout,underline"), + ("card_red", "dark red", "white", "standout,underline"), + ("card_red_selected", "dark red", "dark green", "standout,underline"), + ("card_black", "black", "white", "standout,underline"), + ("card_black_selected", "black", "dark green", "standout,underline"), + ("directory", "dark cyan, bold", "default"), + ("directory_focus", "dark cyan, bold", "dark green"), + ("separator", "brown", "default"), + ("warning", "light red", "default"), + ("progress_normal", "default", "brown"), + ("progress_complete", "default", "dark green"), + ("show_disconnected", "dark gray", "default"), + ("show_normal", "default", "default"), + ("show_normal_focus", "default, bold", "default"), + ("show_chat", "dark green", "default"), + ("show_chat_focus", "dark green, bold", "default"), + ("show_away", "brown", "default"), + ("show_away_focus", "brown, bold", "default"), + ("show_dnd", "dark red", "default"), + ("show_dnd_focus", "dark red, bold", "default"), + ("show_xa", "dark red", "default"), + ("show_xa_focus", "dark red, bold", "default"), + ("resource", "light blue", "default"), + ("resource_main", "dark blue", "default"), + ("status", "yellow", "default"), + ("status_focus", "yellow, bold", "default"), + ("param_selected", "default, bold", "dark red"), + ("table_selected", "default, bold", "default"), + ] + PRESENCE = { + "unavailable": ("⨯", "show_disconnected"), + "": ("✔", "show_normal"), + "chat": ("✆", "show_chat"), + "away": ("✈", "show_away"), + "dnd": ("✖", "show_dnd"), + "xa": ("☄", "show_xa"), + } + LOG_OPT_SECTION = APP_NAME.lower() + LOG_OPT_OUTPUT = ( + "output", + constants.Const.LOG_OPT_OUTPUT_SEP + constants.Const.LOG_OPT_OUTPUT_MEMORY, + ) + CONFIG_OPT_KEY_PREFIX = "KEY_" + + MENU_ID_MAIN = "MAIN_MENU" + MENU_ID_WIDGET = "WIDGET_MENU" + + MODE_NORMAL = "NORMAL" + MODE_INSERTION = "INSERTION" + MODE_COMMAND = "COMMAND" + + GROUP_DATA_FOLDED = "folded" diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/contact_list.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/contact_list.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +import urwid +from urwid_satext import sat_widgets +from libervia.frontends.quick_frontend.quick_contact_list import QuickContactList +from libervia.tui.status import StatusBar +from libervia.tui.constants import Const as C +from libervia.tui.keys import action_key_map as a_key +from libervia.tui.widget import LiberviaTUIWidget +from libervia.frontends.tools import jid +from libervia.backend.core import log as logging + +log = logging.getLogger(__name__) +from libervia.frontends.quick_frontend import quick_widgets + + +class ContactList(LiberviaTUIWidget, QuickContactList): + PROFILES_MULTIPLE = False + PROFILES_ALLOW_NONE = False + signals = ["click", "change"] + # FIXME: Only single profile is managed so far + + def __init__( + self, host, target, on_click=None, on_change=None, user_data=None, profiles=None + ): + QuickContactList.__init__(self, host, profiles) + self.contact_list = self.host.contact_lists[self.profile] + + # we now build the widget + self.status_bar = StatusBar(host) + self.frame = sat_widgets.FocusFrame(self._build_list(), None, self.status_bar) + LiberviaTUIWidget.__init__(self, self.frame, _("Contacts")) + if on_click: + urwid.connect_signal(self, "click", on_click, user_data) + if on_change: + urwid.connect_signal(self, "change", on_change, user_data) + self.host.addListener("notification", self.on_notification, [self.profile]) + self.host.addListener("notificationsClear", self.on_notification, [self.profile]) + self.post_init() + + def update(self, entities=None, type_=None, profile=None): + """Update display, keep focus""" + # FIXME: full update is done each time, must handle entities, type_ and profile + widget, position = self.frame.body.get_focus() + self.frame.body = self._build_list() + if position: + try: + self.frame.body.focus_position = position + except IndexError: + pass + self._invalidate() + self.host.redraw() # FIXME: check if can be avoided + + def keypress(self, size, key): + # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent, + # and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element + if key in sat_widgets.FOCUS_KEYS: + if ( + key == a_key["FOCUS_SWITCH"] + or (key == a_key["FOCUS_UP"] and self.frame.focus_position == "body") + or (key == a_key["FOCUS_DOWN"] and self.frame.focus_position == "footer") + ): + return key + if key == a_key["STATUS_HIDE"]: # user wants to (un)hide contacts' statuses + self.contact_list.show_status = not self.contact_list.show_status + self.update() + elif ( + key == a_key["DISCONNECTED_HIDE"] + ): # user wants to (un)hide disconnected contacts + self.host.bridge.param_set( + C.SHOW_OFFLINE_CONTACTS, + C.bool_const(not self.contact_list.show_disconnected), + "General", + profile_key=self.profile, + ) + elif key == a_key["RESOURCES_HIDE"]: # user wants to (un)hide contacts resources + self.contact_list.show_resources(not self.contact_list.show_resources) + self.update() + return super(ContactList, self).keypress(size, key) + + # QuickWidget methods + + @staticmethod + def get_widget_hash(target, profiles): + profiles = sorted(profiles) + return tuple(profiles) + + # modify the contact list + + def set_focus(self, text, select=False): + """give focus to the first element that matches the given text. You can also + pass in text a libervia.frontends.tools.jid.JID (it's a subclass of unicode). + + @param text: contact group name, contact or muc userhost, muc private dialog jid + @param select: if True, the element is also clicked + """ + idx = 0 + for widget in self.frame.body.body: + try: + if isinstance(widget, sat_widgets.ClickableText): + # contact group + value = widget.get_value() + elif isinstance(widget, sat_widgets.SelectableText): + # contact or muc + value = widget.data + else: + # Divider instance + continue + # there's sometimes a leading space + if text.strip() == value.strip(): + self.frame.body.focus_position = idx + if select: + self._contact_clicked(False, widget, True) + return + except AttributeError: + pass + idx += 1 + + log.debug("Not element found for {} in set_focus".format(text)) + + # events + + def _group_clicked(self, group_wid): + group = group_wid.get_value() + data = self.contact_list.get_group_data(group) + data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False) + self.set_focus(group) + self.update() + + def _contact_clicked(self, use_bare_jid, contact_wid, selected): + """Method called when a contact is clicked + + @param use_bare_jid: True if use_bare_jid is set in self._build_entity_widget. + @param contact_wid: widget of the contact, must have the entity set in data attribute + @param selected: boolean returned by the widget, telling if it is selected + """ + entity = contact_wid.data + self.host.mode_hint(C.MODE_INSERTION) + self._emit("click", entity) + + def on_notification(self, entity, notif, profile): + notifs = list(self.host.get_notifs(C.ENTITY_ALL, profile=self.profile)) + if notifs: + self.title_dynamic = "({})".format(len(notifs)) + else: + self.title_dynamic = None + self.host.redraw() # FIXME: should not be necessary + + # Methods to build the widget + + def _build_entity_widget( + self, + entity, + keys=None, + use_bare_jid=False, + with_notifs=True, + with_show_attr=True, + markup_prepend=None, + markup_append=None, + special=False, + ): + """Build one contact markup data + + @param entity (jid.JID): entity to build + @param keys (iterable): value to markup, in preferred order. + The first available key will be used. + If key starts with "cache_", it will be checked in cache, + else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')). + If nothing full or keys is None, full entity is used. + @param use_bare_jid (bool): if True, use bare jid for selected comparisons + @param with_notifs (bool): if True, show notification count + @param with_show_attr (bool): if True, show color corresponding to presence status + @param markup_prepend (list): markup to prepend to the generated one before building the widget + @param markup_append (list): markup to append to the generated one before building the widget + @param special (bool): True if entity is a special one + @return (list): markup data are expected by Urwid text widgets + """ + markup = [] + if use_bare_jid: + selected = {entity.bare for entity in self.contact_list._selected} + else: + selected = self.contact_list._selected + if keys is None: + entity_txt = entity + else: + cache = self.contact_list.getCache(entity) + for key in keys: + if key.startswith("cache_"): + entity_txt = cache.get(key[6:]) + else: + entity_txt = getattr(entity, key) + if entity_txt: + break + if not entity_txt: + entity_txt = entity + + if with_show_attr: + show = self.contact_list.getCache(entity, C.PRESENCE_SHOW, default=None) + if show is None: + show = C.PRESENCE_UNAVAILABLE + show_icon, entity_attr = C.PRESENCE.get(show, ("", "default")) + markup.insert(0, "{} ".format(show_icon)) + else: + entity_attr = "default" + + notifs = list( + self.host.get_notifs(entity, exact_jid=special, profile=self.profile) + ) + mentions = list( + self.host.get_notifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile) + ) + if notifs or mentions: + attr = 'cl_mention' if mentions else 'cl_notifs' + header = [(attr, "({})".format(len(notifs) + len(mentions))), " "] + else: + header = "" + + markup.append((entity_attr, entity_txt)) + if markup_prepend: + markup.insert(0, markup_prepend) + if markup_append: + markup.extend(markup_append) + + widget = sat_widgets.SelectableText( + markup, selected=entity in selected, header=header + ) + widget.data = entity + widget.comp = entity_txt.lower() # value to use for sorting + urwid.connect_signal( + widget, "change", self._contact_clicked, user_args=[use_bare_jid] + ) + return widget + + def _build_entities(self, content, entities): + """Add entity representation in widget list + + @param content: widget list, e.g. SimpleListWalker + @param entities (iterable): iterable of JID to display + """ + if not entities: + return + widgets = [] # list of built widgets + + for entity in entities: + if ( + entity in self.contact_list._specials + or not self.contact_list.entity_visible(entity) + ): + continue + markup_extra = [] + if self.contact_list.show_resources: + for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES): + resource_disp = ( + "resource_main" + if resource + == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE) + else "resource", + "\n " + resource, + ) + markup_extra.append(resource_disp) + if self.contact_list.show_status: + status = self.contact_list.getCache( + jid.JID("%s/%s" % (entity, resource)), "status", default=None + ) + status_disp = ("status", "\n " + status) if status else "" + markup_extra.append(status_disp) + + else: + if self.contact_list.show_status: + status = self.contact_list.getCache(entity, "status", default=None) + status_disp = ("status", "\n " + status) if status else "" + markup_extra.append(status_disp) + widget = self._build_entity_widget( + entity, + ("cache_nick", "cache_name", "node"), + use_bare_jid=True, + markup_append=markup_extra, + ) + widgets.append(widget) + + widgets.sort(key=lambda widget: widget.comp) + + for widget in widgets: + content.append(widget) + + def _build_specials(self, content): + """Build the special entities""" + specials = sorted(self.contact_list.get_specials()) + current = None + for entity in specials: + if current is not None and current.bare == entity.bare: + # nested entity (e.g. MUC private conversations) + widget = self._build_entity_widget( + entity, ("resource",), markup_prepend=" ", special=True + ) + else: + # the special widgets + if entity.resource: + widget = self._build_entity_widget(entity, ("resource",), special=True) + else: + widget = self._build_entity_widget( + entity, + ("cache_nick", "cache_name", "node"), + with_show_attr=False, + special=True, + ) + content.append(widget) + + def _build_list(self): + """Build the main contact list widget""" + content = urwid.SimpleListWalker([]) + + self._build_specials(content) + if self.contact_list._specials: + content.append(urwid.Divider("=")) + + groups = list(self.contact_list._groups) + groups.sort(key=lambda x: x.lower() if x else '') + for group in groups: + data = self.contact_list.get_group_data(group) + folded = data.get(C.GROUP_DATA_FOLDED, False) + jids = list(data["jids"]) + if group is not None and ( + self.contact_list.any_entity_visible(jids) + or self.contact_list.show_empty_groups + ): + header = "[-]" if not folded else "[+]" + widget = sat_widgets.ClickableText(group, header=header + " ") + content.append(widget) + urwid.connect_signal(widget, "click", self._group_clicked) + if not folded: + self._build_entities(content, jids) + not_in_roster = ( + set(self.contact_list._cache) + .difference(self.contact_list._roster) + .difference(self.contact_list._specials) + .difference((self.contact_list.whoami.bare,)) + ) + if not_in_roster: + content.append(urwid.Divider("-")) + self._build_entities(content, not_in_roster) + + return urwid.ListBox(content) + + +quick_widgets.register(QuickContactList, ContactList) diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/game_tarot.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/game_tarot.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +import urwid +from urwid_satext import sat_widgets +from libervia.frontends.tools.games import TarotCard +from libervia.frontends.quick_frontend.quick_game_tarot import QuickTarotGame +from libervia.tui import xmlui +from libervia.tui.keys import action_key_map as a_key + + +class CardDisplayer(urwid.Text): + """Show a card""" + + signals = ["click"] + + def __init__(self, card): + self.__selected = False + self.card = card + urwid.Text.__init__(self, card.get_attr_text()) + + def selectable(self): + return True + + def keypress(self, size, key): + if key == a_key["CARD_SELECT"]: + self.select(not self.__selected) + self._emit("click") + return key + + def mouse_event(self, size, event, button, x, y, focus): + if urwid.is_mouse_event(event) and button == 1: + self.select(not self.__selected) + self._emit("click") + return True + + return False + + def select(self, state=True): + self.__selected = state + attr, txt = self.card.get_attr_text() + if self.__selected: + attr += "_selected" + self.set_text((attr, txt)) + self._invalidate() + + def is_selected(self): + return self.__selected + + def get_card(self): + return self.card + + def render(self, size, focus=False): + canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus)) + if focus: + canvas.set_cursor((0, 0)) + return canvas + + +class Hand(urwid.WidgetWrap): + """Used to display several cards, and manage a hand""" + + signals = ["click"] + + def __init__(self, hand=[], selectable=False, on_click=None, user_data=None): + """@param hand: list of Card""" + self.__selectable = selectable + self.columns = urwid.Columns([], dividechars=1) + if on_click: + urwid.connect_signal(self, "click", on_click, user_data) + if hand: + self.update(hand) + urwid.WidgetWrap.__init__(self, self.columns) + + def selectable(self): + return self.__selectable + + def keypress(self, size, key): + + if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]: + return self.columns.keypress(size, key) + else: + # No card displayed, we still have to manage the clicks + if key == a_key["CARD_SELECT"]: + self._emit("click", None) + return key + + def get_selected(self): + """Return a list of selected cards""" + _selected = [] + for wid in self.columns.widget_list: + if isinstance(wid, CardDisplayer) and wid.is_selected(): + _selected.append(wid.get_card()) + return _selected + + def update(self, hand): + """Update the hand displayed in this widget + @param hand: list of Card""" + try: + del self.columns.widget_list[:] + del self.columns.column_types[:] + except IndexError: + pass + self.columns.contents.append((urwid.Text(""), ("weight", 1, False))) + for card in hand: + widget = CardDisplayer(card) + self.columns.widget_list.append(widget) + self.columns.column_types.append(("fixed", 3)) + urwid.connect_signal(widget, "click", self.__on_click) + self.columns.contents.append((urwid.Text(""), ("weight", 1, False))) + self.columns.focus_position = 1 + + def __on_click(self, card_wid): + self._emit("click", card_wid) + + +class Card(TarotCard): + """This class is used to represent a card, logically + and give a text representation with attributes""" + + SIZE = 3 # size of a displayed card + + def __init__(self, suit, value): + """@param file: path of the PNG file""" + TarotCard.__init__(self, (suit, value)) + + def get_attr_text(self): + """return text representation of the card with attributes""" + try: + value = "%02i" % int(self.value) + except ValueError: + value = self.value[0].upper() + self.value[1] + if self.suit == "atout": + if self.value == "excuse": + suit = "c" + else: + suit = "A" + color = "neutral" + elif self.suit == "pique": + suit = "♠" + color = "black" + elif self.suit == "trefle": + suit = "♣" + color = "black" + elif self.suit == "coeur": + suit = "♥" + color = "red" + elif self.suit == "carreau": + suit = "♦" + color = "red" + if self.bout: + color = "special" + return ("card_%s" % color, "%s%s" % (value, suit)) + + def get_widget(self): + """Return a widget representing the card""" + return CardDisplayer(self) + + +class Table(urwid.FlowWidget): + """Represent the cards currently on the table""" + + def __init__(self): + self.top = self.left = self.bottom = self.right = None + + def put_card(self, location, card): + """Put a card on the table + @param location: where to put the card (top, left, bottom or right) + @param card: Card to play or None""" + assert location in ["top", "left", "bottom", "right"] + assert isinstance(card, Card) or card == None + if [getattr(self, place) for place in ["top", "left", "bottom", "right"]].count( + None + ) == 0: + # If the table is full of card, we remove them + self.top = self.left = self.bottom = self.right = None + setattr(self, location, card) + self._invalidate() + + def rows(self, size, focus=False): + return self.display_widget(size, focus).rows(size, focus) + + def render(self, size, focus=False): + return self.display_widget(size, focus).render(size, focus) + + def display_widget(self, size, focus): + cards = {} + max_col, = size + separator = " - " + margin = max((max_col - Card.SIZE) / 2, 0) * " " + margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * " " + for location in ["top", "left", "bottom", "right"]: + card = getattr(self, location) + cards[location] = card.get_attr_text() if card else Card.SIZE * " " + render_wid = [ + urwid.Text([margin, cards["top"]]), + urwid.Text([margin_center, cards["left"], separator, cards["right"]]), + urwid.Text([margin, cards["bottom"]]), + ] + return urwid.Pile(render_wid) + + +class TarotGame(QuickTarotGame, urwid.WidgetWrap): + """Widget for card games""" + + def __init__(self, parent, referee, players): + QuickTarotGame.__init__(self, parent, referee, players) + self.load_cards() + self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), "center")]) + # self.parent.host.debug() + self.table = Table() + self.center = urwid.Columns( + [ + ("fixed", len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))), + urwid.Filler(self.table), + ( + "fixed", + len(self.right_nick), + urwid.Filler(urwid.Text(self.right_nick)), + ), + ] + ) + """urwid.Pile([urwid.Padding(self.top_card_wid,'center'), + urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)), + urwid.Padding(self.center_cards_wid,'center'), + ('fixed',len(self.right_nick),urwid.Text(self.right_nick)) + ]), + urwid.Padding(self.bottom_card_wid,'center') + ])""" + self.hand_wid = Hand(selectable=True, on_click=self.on_click) + self.main_frame = urwid.Frame( + self.center, header=self.top, footer=self.hand_wid, focus_part="footer" + ) + urwid.WidgetWrap.__init__(self, self.main_frame) + self.parent.host.bridge.tarot_game_ready( + self.player_nick, referee, self.parent.profile + ) + + def load_cards(self): + """Load all the cards in memory""" + QuickTarotGame.load_cards(self) + for value in list(map(str, list(range(1, 22)))) + ["excuse"]: + card = Card("atout", value) + self.cards[card.suit, card.value] = card + self.deck.append(card) + for suit in ["pique", "coeur", "carreau", "trefle"]: + for value in list(map(str, list(range(1, 11)))) + ["valet", "cavalier", "dame", "roi"]: + card = Card(suit, value) + self.cards[card.suit, card.value] = card + self.deck.append(card) + + def tarot_game_new_handler(self, hand): + """Start a new game, with given hand""" + if hand is []: # reset the display after the scores have been showed + self.reset_round() + for location in ["top", "left", "bottom", "right"]: + self.table.put_card(location, None) + self.parent.host.redraw() + self.parent.host.bridge.tarot_game_ready( + self.player_nick, self.referee, self.parent.profile + ) + return + QuickTarotGame.tarot_game_new_handler(self, hand) + self.hand_wid.update(self.hand) + self.parent.host.redraw() + + def tarot_game_choose_contrat_handler(self, xml_data): + """Called when the player has to select his contrat + @param xml_data: SàT xml representation of the form""" + form = xmlui.create( + self.parent.host, + xml_data, + title=_("Please choose your contrat"), + flags=["NO_CANCEL"], + profile=self.parent.profile, + ) + form.show(valign="top") + + def tarot_game_show_cards_handler(self, game_stage, cards, data): + """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" + QuickTarotGame.tarot_game_show_cards_handler(self, game_stage, cards, data) + self.center.widget_list[1] = urwid.Filler(Hand(self.to_show)) + self.parent.host.redraw() + + def tarot_game_your_turn_handler(self): + QuickTarotGame.tarot_game_your_turn_handler(self) + + def tarot_game_score_handler(self, xml_data, winners, loosers): + """Called when the round is over, display the scores + @param xml_data: SàT xml representation of the form""" + if not winners and not loosers: + title = _("Draw game") + else: + title = _("You win \o/") if self.player_nick in winners else _("You loose :(") + form = xmlui.create( + self.parent.host, + xml_data, + title=title, + flags=["NO_CANCEL"], + profile=self.parent.profile, + ) + form.show() + + def tarot_game_invalid_cards_handler(self, phase, played_cards, invalid_cards): + """Invalid cards have been played + @param phase: phase of the game + @param played_cards: all the cards played + @param invalid_cards: cards which are invalid""" + QuickTarotGame.tarot_game_invalid_cards_handler( + self, phase, played_cards, invalid_cards + ) + self.hand_wid.update(self.hand) + if self._autoplay == None: # No dialog if there is autoplay + self.parent.host.bar_notify(_("Cards played are invalid !")) + self.parent.host.redraw() + + def tarot_game_cards_played_handler(self, player, cards): + """A card has been played by player""" + QuickTarotGame.tarot_game_cards_played_handler(self, player, cards) + self.table.put_card(self.get_player_location(player), self.played[player]) + self._checkState() + self.parent.host.redraw() + + def _checkState(self): + if isinstance( + self.center.widget_list[1].original_widget, Hand + ): # if we have a hand displayed + self.center.widget_list[1] = urwid.Filler( + self.table + ) # we show again the table + if self.state == "chien": + self.to_show = [] + self.state = "wait" + elif self.state == "wait_for_ecart": + self.state = "ecart" + self.hand.extend(self.to_show) + self.hand.sort() + self.to_show = [] + self.hand_wid.update(self.hand) + + ##EVENTS## + def on_click(self, hand, card_wid): + """Called when user do an action on the hand""" + if not self.state in ["play", "ecart", "wait_for_ecart"]: + # it's not our turn, we ignore the click + card_wid.select(False) + return + self._checkState() + if self.state == "ecart": + if len(self.hand_wid.get_selected()) == 6: + pop_up_widget = sat_widgets.ConfirmDialog( + _("Do you put these cards in chien ?"), + yes_cb=self.on_ecart_done, + no_cb=self.parent.host.remove_pop_up, + ) + self.parent.host.show_pop_up(pop_up_widget) + elif self.state == "play": + card = card_wid.get_card() + self.parent.host.bridge.tarot_game_play_cards( + self.player_nick, + self.referee, + [(card.suit, card.value)], + self.parent.profile, + ) + self.hand.remove(card) + self.hand_wid.update(self.hand) + self.state = "wait" + + def on_ecart_done(self, button): + """Called when player has finished his écart""" + ecart = [] + for card in self.hand_wid.get_selected(): + ecart.append((card.suit, card.value)) + self.hand.remove(card) + self.hand_wid.update(self.hand) + self.parent.host.bridge.tarot_game_play_cards( + self.player_nick, self.referee, ecart, self.parent.profile + ) + self.state = "wait" + self.parent.host.remove_pop_up() diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/keys.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/keys.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""This file manage the action <=> key map""" + +from urwid_satext.keys import action_key_map + + +action_key_map.update( + { + # Edit bar + ("edit", "MODE_INSERTION"): "i", + ("edit", "MODE_COMMAND"): ":", + ("edit", "HISTORY_PREV"): "up", + ("edit", "HISTORY_NEXT"): "down", + # global + ("global", "MENU_HIDE"): "meta m", + ("global", "NOTIFICATION_NEXT"): "ctrl n", + ("global", "OVERLAY_HIDE"): "ctrl s", + ("global", "DEBUG"): "ctrl d", + ("global", "CONTACTS_HIDE"): "f2", + ( + "global", + "REFRESH_SCREEN", + ): "ctrl l", # ctrl l is used by Urwid to refresh screen + # global menu + ("menu_global", "APP_QUIT"): "ctrl x", + ("menu_global", "ROOM_JOIN"): "meta j", + # Libervia TUI widgets + ("libervia_tui_widget", "DECORATION_HIDE"): "meta l", + # contact list + ("contact_list", "STATUS_HIDE"): "meta s", + ("contact_list", "DISCONNECTED_HIDE"): "meta d", + ("contact_list", "RESOURCES_HIDE"): "meta r", + # chat panel + ("chat_panel", "OCCUPANTS_HIDE"): "meta p", + ("chat_panel", "TIMESTAMP_HIDE"): "meta t", + ("chat_panel", "SHORT_NICKNAME"): "meta n", + ("chat_panel", "SUBJECT_SWITCH"): "meta s", + ("chat_panel", "GOTO_BOTTOM"): "G", + # card game + ("card_game", "CARD_SELECT"): " ", + # focus + ("focus", "FOCUS_EXTRA"): "ctrl f", + } +) + + +action_key_map.set_close_namespaces(tuple(), ("global", "focus", "menu_global")) +action_key_map.check_namespaces() diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/notify.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/notify.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import dbus + + +class Notify(object): + """Used to send notification and detect if we have focus""" + + def __init__(self): + + # X11 stuff + self.display = None + self.X11_id = -1 + + try: + from Xlib import display as X_display + + self.display = X_display.Display() + self.X11_id = self.get_focus() + except: + pass + + # Now we try to connect to Freedesktop D-Bus API + try: + bus = dbus.SessionBus() + db_object = bus.get_object( + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + follow_name_owner_changes=True, + ) + self.freedesktop_int = dbus.Interface( + db_object, dbus_interface="org.freedesktop.Notifications" + ) + except: + self.freedesktop_int = None + + def get_focus(self): + if not self.display: + return 0 + return self.display.get_input_focus().focus.id + + def has_focus(self): + return (self.get_focus() == self.X11_id) if self.display else True + + def use_x11(self): + return bool(self.display) + + def send_notification(self, summ_mess, body_mess=""): + """Send notification to the user if possible""" + # TODO: check options before sending notifications + if self.freedesktop_int: + self.send_fd_notification(summ_mess, body_mess) + + def send_fd_notification(self, summ_mess, body_mess=""): + """Send notification with the FreeDesktop D-Bus API""" + if self.freedesktop_int: + app_name = "LiberviaTUI" + replaces_id = 0 + app_icon = "" + summary = summ_mess + body = body_mess + actions = dbus.Array(signature="s") + hints = dbus.Dictionary(signature="sv") + expire_timeout = -1 + + self.freedesktop_int.Notify( + app_name, + replaces_id, + app_icon, + summary, + body, + actions, + hints, + expire_timeout, + ) diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/profile_manager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/profile_manager.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +from libervia.backend.core import log as logging + +log = logging.getLogger(__name__) +from libervia.frontends.quick_frontend.quick_profile_manager import QuickProfileManager +from libervia.tui.constants import Const as C +from libervia.tui.keys import action_key_map as a_key +from urwid_satext import sat_widgets +import urwid + + +class ProfileManager(QuickProfileManager, urwid.WidgetWrap): + def __init__(self, host, autoconnect=None): + QuickProfileManager.__init__(self, host, autoconnect) + + # login & password box must be created before list because of on_profile_change + self.login_wid = sat_widgets.AdvancedEdit(_("Login:"), align="center") + self.pass_wid = sat_widgets.Password(_("Password:"), align="center") + + style = ["no_first_select"] + profiles = host.bridge.profiles_list_get() + profiles.sort() + self.list_profile = sat_widgets.List( + profiles, style=style, align="center", on_change=self.on_profile_change + ) + + # new & delete buttons + buttons = [ + urwid.Button(_("New"), self.on_new_profile), + urwid.Button(_("Delete"), self.on_delete_profile), + ] + buttons_flow = urwid.GridFlow( + buttons, + max([len(button.get_label()) for button in buttons]) + 4, + 1, + 1, + "center", + ) + + # second part: login information: + divider = urwid.Divider("-") + + # connect button + connect_button = sat_widgets.CustomButton( + _("Connect"), self.on_connect_profiles, align="center" + ) + + # we now build the widget + list_walker = urwid.SimpleFocusListWalker( + [ + buttons_flow, + self.list_profile, + divider, + self.login_wid, + self.pass_wid, + connect_button, + ] + ) + frame_body = urwid.ListBox(list_walker) + frame = urwid.Frame( + frame_body, + urwid.AttrMap(urwid.Text(_("Profile Manager"), align="center"), "title"), + ) + self.main_widget = urwid.LineBox(frame) + urwid.WidgetWrap.__init__(self, self.main_widget) + + self.go(autoconnect) + + def keypress(self, size, key): + if key == a_key["APP_QUIT"]: + self.host.on_exit() + raise urwid.ExitMainLoop() + elif key in (a_key["FOCUS_UP"], a_key["FOCUS_DOWN"]): + focus_diff = 1 if key == a_key["FOCUS_DOWN"] else -1 + list_box = self.main_widget.base_widget.body + current_focus = list_box.body.get_focus()[1] + if current_focus is None: + return + while True: + current_focus += focus_diff + if current_focus < 0 or current_focus >= len(list_box.body): + break + if list_box.body[current_focus].selectable(): + list_box.set_focus( + current_focus, "above" if focus_diff == 1 else "below" + ) + list_box._invalidate() + return + return super(ProfileManager, self).keypress(size, key) + + def cancel_dialog(self, button): + self.host.remove_pop_up() + + def new_profile(self, button, edit): + """Create the profile""" + name = edit.get_edit_text() + self.host.bridge.profile_create( + name, + callback=lambda: self.new_profile_created(name), + errback=self.profile_creation_failure, + ) + + def new_profile_created(self, profile): + # new profile will be selected, and a selected profile assume the session is started + self.host.bridge.profile_start_session( + "", + profile, + callback=lambda __: self.new_profile_session_started(profile), + errback=self.profile_creation_failure, + ) + + def new_profile_session_started(self, profile): + self.host.remove_pop_up() + self.refill_profiles() + self.list_profile.select_value(profile) + self.current.profile = profile + self.get_connection_params(profile) + self.host.redraw() + + def profile_creation_failure(self, reason): + self.host.remove_pop_up() + message = self._get_error_message(reason) + self.host.alert(_("Can't create profile"), message) + + def delete_profile(self, button): + self._delete_profile() + self.host.remove_pop_up() + + def on_new_profile(self, e): + pop_up_widget = sat_widgets.InputDialog( + _("New profile"), + _("Please enter a new profile name"), + cancel_cb=self.cancel_dialog, + ok_cb=self.new_profile, + ) + self.host.show_pop_up(pop_up_widget) + + def on_delete_profile(self, e): + if self.current.profile: + pop_up_widget = sat_widgets.ConfirmDialog( + _("Are you sure you want to delete the profile {} ?").format( + self.current.profile + ), + no_cb=self.cancel_dialog, + yes_cb=self.delete_profile, + ) + self.host.show_pop_up(pop_up_widget) + + def on_connect_profiles(self, button): + """Connect the profiles and start the main widget + + @param button: the connect button + """ + self._on_connect_profiles() + + def reset_fields(self): + """Set profile to None, and reset fields""" + super(ProfileManager, self).reset_fields() + self.list_profile.unselect_all(invisible=True) + + def set_profiles(self, profiles): + """Update the list of profiles""" + self.list_profile.change_values(profiles) + self.host.redraw() + + def get_profiles(self): + return self.list_profile.get_selected_values() + + def get_jid(self): + return self.login_wid.get_edit_text() + + def getPassword(self): + return self.pass_wid.get_edit_text() + + def set_jid(self, jid_): + self.login_wid.set_edit_text(jid_) + self.current.login = jid_ + self.host.redraw() # FIXME: redraw should be avoided + + def set_password(self, password): + self.pass_wid.set_edit_text(password) + self.current.password = password + self.host.redraw() + + def on_profile_change(self, list_wid, widget=None, selected=None): + """This is called when a profile is selected in the profile list. + + @param list_wid: the List widget who sent the event + """ + self.update_connection_params() + focused = list_wid.focus + selected = focused.get_state() if focused is not None else False + if not selected: # profile was just unselected + return + focused.set_state( + False, invisible=True + ) # we don't want the widget to be selected until we are sure we can access it + + def authenticate_cb(data, cb_id, profile): + if C.bool(data.pop("validated", C.BOOL_FALSE)): + self.current.profile = profile + focused.set_state(True, invisible=True) + self.get_connection_params(profile) + self.host.redraw() + self.host.action_manager(data, callback=authenticate_cb, profile=profile) + + self.host.action_launch( + C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text + ) diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/progress.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/progress.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +import urwid +from urwid_satext import sat_widgets +from libervia.frontends.quick_frontend import quick_widgets + + +class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget): + PROFILES_ALLOW_NONE = True + + def __init__(self, host, target, profiles): + assert target is None and profiles is None + quick_widgets.QuickWidget.__init__(self, host, target) + self.host = host + self.progress_list = urwid.SimpleListWalker([]) + self.progress_dict = {} + listbox = urwid.ListBox(self.progress_list) + buttons = [] + buttons.append(sat_widgets.CustomButton(_("Clear progress list"), self._on_clear)) + max_len = max([button.get_size() for button in buttons]) + buttons_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center") + main_wid = sat_widgets.FocusFrame(listbox, footer=buttons_wid) + urwid.WidgetWrap.__init__(self, main_wid) + + def add(self, progress_id, message, profile): + mess_wid = urwid.Text(message) + progr_wid = urwid.ProgressBar("progress_normal", "progress_complete") + column = urwid.Columns([mess_wid, progr_wid]) + self.progress_dict[(progress_id, profile)] = { + "full": column, + "progress": progr_wid, + "state": "init", + } + self.progress_list.append(column) + self.progress_cb(self.host.loop, (progress_id, message, profile)) + + def progress_cb(self, loop, data): + progress_id, message, profile = data + data = self.host.bridge.progress_get(progress_id, profile) + pbar = self.progress_dict[(progress_id, profile)]["progress"] + if data: + if self.progress_dict[(progress_id, profile)]["state"] == "init": + # first answer, we must construct the bar + self.progress_dict[(progress_id, profile)]["state"] = "progress" + pbar.done = float(data["size"]) + + pbar.set_completion(float(data["position"])) + self.update_not_bar() + else: + if self.progress_dict[(progress_id, profile)]["state"] == "progress": + self.progress_dict[(progress_id, profile)]["state"] = "done" + pbar.set_completion(pbar.done) + self.update_not_bar() + return + + loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile)) + + def _remove_bar(self, progress_id, profile): + wid = self.progress_dict[(progress_id, profile)]["full"] + self.progress_list.remove(wid) + del (self.progress_dict[(progress_id, profile)]) + + def _on_clear(self, button): + to_remove = [] + for progress_id, profile in self.progress_dict: + if self.progress_dict[(progress_id, profile)]["state"] == "done": + to_remove.append((progress_id, profile)) + for progress_id, profile in to_remove: + self._remove_bar(progress_id, profile) + self.update_not_bar() + + def update_not_bar(self): + if not self.progress_dict: + self.host.set_progress(None) + return + progress = 0 + nb_bars = 0 + for progress_id, profile in self.progress_dict: + pbar = self.progress_dict[(progress_id, profile)]["progress"] + progress += pbar.current / pbar.done * 100 + nb_bars += 1 + av_progress = progress / float(nb_bars) + self.host.set_progress(av_progress) diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/status.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/status.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +import urwid +from urwid_satext import sat_widgets +from libervia.frontends.quick_frontend.constants import Const as commonConst +from libervia.tui.constants import Const as C + + +class StatusBar(urwid.Columns): + def __init__(self, host): + self.host = host + self.presence = sat_widgets.ClickableText("") + status_prefix = urwid.Text("[") + status_suffix = urwid.Text("]") + self.status = sat_widgets.ClickableText("") + self.set_presence_status(C.PRESENCE_UNAVAILABLE, "") + urwid.Columns.__init__( + self, + [ + ("weight", 1, self.presence), + ("weight", 1, status_prefix), + ("weight", 9, self.status), + ("weight", 1, status_suffix), + ], + ) + urwid.connect_signal(self.presence, "click", self.on_presence_click) + urwid.connect_signal(self.status, "click", self.on_status_click) + + def on_presence_click(self, sender=None): + if not self.host.bridge.is_connected( + self.host.current_profile + ): # FIXME: manage multi-profiles + return + options = [commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE] + list_widget = sat_widgets.GenericList( + options=options, option_type=sat_widgets.ClickableText, on_click=self.on_change + ) + decorated = sat_widgets.LabelLine( + list_widget, sat_widgets.SurroundedText(_("Set your presence")) + ) + self.host.show_pop_up(decorated) + + def on_status_click(self, sender=None): + if not self.host.bridge.is_connected( + self.host.current_profile + ): # FIXME: manage multi-profiles + return + pop_up_widget = sat_widgets.InputDialog( + _("Set your status"), + _("New status"), + default_txt=self.status.get_text(), + cancel_cb=lambda _: self.host.remove_pop_up(), + ok_cb=self.on_change, + ) + self.host.show_pop_up(pop_up_widget) + + def on_change(self, sender=None, user_data=None): + new_value = user_data.get_text() + previous = ( + [key for key in C.PRESENCE if C.PRESENCE[key][0] == self.presence.get_text()][ + 0 + ], + self.status.get_text(), + ) + if isinstance(user_data, sat_widgets.ClickableText): + new = ( + [ + key + for key in commonConst.PRESENCE + if commonConst.PRESENCE[key] == new_value + ][0], + previous[1], + ) + elif isinstance(user_data, sat_widgets.AdvancedEdit): + new = (previous[0], new_value[0]) + if new != previous: + statuses = { + C.PRESENCE_STATUSES_DEFAULT: new[1] + } # FIXME: manage multilingual statuses + for ( + profile + ) in ( + self.host.profiles + ): # FIXME: for now all the profiles share the same status + self.host.bridge.presence_set( + show=new[0], statuses=statuses, profile_key=profile + ) + self.set_presence_status(new[0], new[1]) + self.host.remove_pop_up() + + def set_presence_status(self, show, status): + show_icon, show_attr = C.PRESENCE.get(show) + self.presence.set_text(("show_normal", show_icon)) + if status is not None: + self.status.set_text((show_attr, status)) + self.host.redraw() diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/widget.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core import log as logging + +log = logging.getLogger(__name__) +import urwid +from urwid_satext import sat_widgets +from libervia.tui.keys import action_key_map as a_key + + +class LiberviaTUIWidget(urwid.WidgetWrap): + """Base widget for LiberviaTUI""" + + def __init__(self, w, title=""): + self._title = title + self._title_dynamic = None + self._original_widget = w + urwid.WidgetWrap.__init__(self, self._get_decoration(w)) + + @property + def title(self): + """Text shown in title bar of the widget""" + + # profiles currently managed by frontend + try: + all_profiles = self.host.profiles + except AttributeError: + all_profiles = [] + + # profiles managed by the widget + try: + profiles = self.profiles + except AttributeError: + try: + profiles = [self.profile] + except AttributeError: + profiles = [] + + title_elts = [] + if self._title: + title_elts.append(self._title) + if self._title_dynamic: + title_elts.append(self._title_dynamic) + if len(all_profiles) > 1 and profiles: + title_elts.append("[{}]".format(", ".join(profiles))) + return sat_widgets.SurroundedText(" ".join(title_elts)) + + @title.setter + def title(self, value): + self._title = value + if self.decoration_visible: + self.show_decoration() + + @property + def title_dynamic(self): + """Dynamic part of title""" + return self._title_dynamic + + @title_dynamic.setter + def title_dynamic(self, value): + self._title_dynamic = value + if self.decoration_visible: + self.show_decoration() + + @property + def decoration_visible(self): + """True if the decoration is visible""" + return isinstance(self._w, sat_widgets.LabelLine) + + def keypress(self, size, key): + if key == a_key["DECORATION_HIDE"]: # user wants to (un)hide widget decoration + show = not self.decoration_visible + self.show_decoration(show) + else: + return super(LiberviaTUIWidget, self).keypress(size, key) + + def _get_decoration(self, widget): + return sat_widgets.LabelLine(widget, self.title) + + def show_decoration(self, show=True): + """Show/Hide the decoration around the window""" + self._w = ( + self._get_decoration(self._original_widget) if show else self._original_widget + ) + + def get_menu(self): + raise NotImplementedError diff -r 47401850dec6 -r b620a8e882e1 libervia/tui/xmlui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/tui/xmlui.py Fri Jun 02 16:25:25 2023 +0200 @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 + + +# Libervia TUI +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +import urwid +import copy +from libervia.backend.core import exceptions +from urwid_satext import sat_widgets +from urwid_satext import files_management +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from libervia.tui.constants import Const as C +from libervia.tui.widget import LiberviaTUIWidget +from libervia.frontends.tools import xmlui + + +class LiberviaTUIEvents(object): + """ Used to manage change event of LiberviaTUI widgets """ + + def _event_callback(self, ctrl, *args, **kwargs): + """" Call xmlui callback and ignore any extra argument """ + args[-1](ctrl) + + def _xmlui_on_change(self, callback): + """ Call callback with widget as only argument """ + urwid.connect_signal(self, "change", self._event_callback, callback) + + +class LiberviaTUIEmptyWidget(xmlui.EmptyWidget, urwid.Text): + def __init__(self, _xmlui_parent): + urwid.Text.__init__(self, "") + + +class LiberviaTUITextWidget(xmlui.TextWidget, urwid.Text): + def __init__(self, _xmlui_parent, value, read_only=False): + urwid.Text.__init__(self, value) + + +class LiberviaTUILabelWidget(xmlui.LabelWidget, LiberviaTUITextWidget): + def __init__(self, _xmlui_parent, value): + super(LiberviaTUILabelWidget, self).__init__(_xmlui_parent, value + ": ") + + +class LiberviaTUIJidWidget(xmlui.JidWidget, LiberviaTUITextWidget): + pass + + +class LiberviaTUIDividerWidget(xmlui.DividerWidget, urwid.Divider): + def __init__(self, _xmlui_parent, style="line"): + if style == "line": + div_char = "─" + elif style == "dot": + div_char = "·" + elif style == "dash": + div_char = "-" + elif style == "plain": + div_char = "█" + elif style == "blank": + div_char = " " + else: + log.warning(_("Unknown div_char")) + div_char = "─" + + urwid.Divider.__init__(self, div_char) + + +class LiberviaTUIStringWidget( + xmlui.StringWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents +): + def __init__(self, _xmlui_parent, value, read_only=False): + sat_widgets.AdvancedEdit.__init__(self, edit_text=value) + self.read_only = read_only + + def selectable(self): + if self.read_only: + return False + return super(LiberviaTUIStringWidget, self).selectable() + + def _xmlui_set_value(self, value): + self.set_edit_text(value) + + def _xmlui_get_value(self): + return self.get_edit_text() + + +class LiberviaTUIJidInputWidget(xmlui.JidInputWidget, LiberviaTUIStringWidget): + pass + + +class LiberviaTUIPasswordWidget( + xmlui.PasswordWidget, sat_widgets.Password, LiberviaTUIEvents +): + def __init__(self, _xmlui_parent, value, read_only=False): + sat_widgets.Password.__init__(self, edit_text=value) + self.read_only = read_only + + def selectable(self): + if self.read_only: + return False + return super(LiberviaTUIPasswordWidget, self).selectable() + + def _xmlui_set_value(self, value): + self.set_edit_text(value) + + def _xmlui_get_value(self): + return self.get_edit_text() + + +class LiberviaTUITextBoxWidget( + xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents +): + def __init__(self, _xmlui_parent, value, read_only=False): + sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True) + self.read_only = read_only + + def selectable(self): + if self.read_only: + return False + return super(LiberviaTUITextBoxWidget, self).selectable() + + def _xmlui_set_value(self, value): + self.set_edit_text(value) + + def _xmlui_get_value(self): + return self.get_edit_text() + + +class LiberviaTUIBoolWidget(xmlui.BoolWidget, urwid.CheckBox, LiberviaTUIEvents): + def __init__(self, _xmlui_parent, state, read_only=False): + urwid.CheckBox.__init__(self, "", state=state) + self.read_only = read_only + + def selectable(self): + if self.read_only: + return False + return super(LiberviaTUIBoolWidget, self).selectable() + + def _xmlui_set_value(self, value): + self.set_state(value == "true") + + def _xmlui_get_value(self): + return C.BOOL_TRUE if self.get_state() else C.BOOL_FALSE + + +class LiberviaTUIIntWidget(xmlui.IntWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents): + def __init__(self, _xmlui_parent, value, read_only=False): + sat_widgets.AdvancedEdit.__init__(self, edit_text=value) + self.read_only = read_only + + def selectable(self): + if self.read_only: + return False + return super(LiberviaTUIIntWidget, self).selectable() + + def _xmlui_set_value(self, value): + self.set_edit_text(value) + + def _xmlui_get_value(self): + return self.get_edit_text() + + +class LiberviaTUIButtonWidget( + xmlui.ButtonWidget, sat_widgets.CustomButton, LiberviaTUIEvents +): + def __init__(self, _xmlui_parent, value, click_callback): + sat_widgets.CustomButton.__init__(self, value, on_press=click_callback) + + def _xmlui_on_click(self, callback): + urwid.connect_signal(self, "click", callback) + + +class LiberviaTUIListWidget(xmlui.ListWidget, sat_widgets.List, LiberviaTUIEvents): + def __init__(self, _xmlui_parent, options, selected, flags): + sat_widgets.List.__init__(self, options=options, style=flags) + self._xmlui_select_values(selected) + + def _xmlui_select_value(self, value): + return self.select_value(value) + + def _xmlui_select_values(self, values): + return self.select_values(values) + + def _xmlui_get_selected_values(self): + return [option.value for option in self.get_selected_values()] + + def _xmlui_add_values(self, values, select=True): + current_values = self.get_all_values() + new_values = copy.deepcopy(current_values) + for value in values: + if value not in current_values: + new_values.append(value) + if select: + selected = self._xmlui_get_selected_values() + self.change_values(new_values) + if select: + for value in values: + if value not in selected: + selected.append(value) + self._xmlui_select_values(selected) + + +class LiberviaTUIJidsListWidget(xmlui.ListWidget, sat_widgets.List, LiberviaTUIEvents): + def __init__(self, _xmlui_parent, jids, styles): + sat_widgets.List.__init__( + self, + options=jids + [""], # the empty field is here to add new jids if needed + option_type=lambda txt, align: sat_widgets.AdvancedEdit( + edit_text=txt, align=align + ), + on_change=self._on_change, + ) + self.delete = 0 + + def _on_change(self, list_widget, jid_widget=None, text=None): + if jid_widget is not None: + if jid_widget != list_widget.contents[-1] and not text: + # if a field is empty, we delete the line (except for the last line) + list_widget.contents.remove(jid_widget) + elif jid_widget == list_widget.contents[-1] and text: + # we always want an empty field as last value to be able to add jids + list_widget.contents.append(sat_widgets.AdvancedEdit()) + + def _xmlui_get_selected_values(self): + # XXX: there is not selection in this list, so we return all non empty values + return [jid_ for jid_ in self.get_all_values() if jid_] + + +class LiberviaTUIAdvancedListContainer( + xmlui.AdvancedListContainer, sat_widgets.TableContainer, LiberviaTUIEvents +): + def __init__(self, _xmlui_parent, columns, selectable="no"): + options = {"ADAPT": ()} + if selectable != "no": + options["HIGHLIGHT"] = () + sat_widgets.TableContainer.__init__( + self, columns=columns, options=options, row_selectable=selectable != "no" + ) + + def _xmlui_append(self, widget): + self.add_widget(widget) + + def _xmlui_add_row(self, idx): + self.set_row_index(idx) + + def _xmlui_get_selected_widgets(self): + return self.get_selected_widgets() + + def _xmlui_get_selected_index(self): + return self.get_selected_index() + + def _xmlui_on_select(self, callback): + """ Call callback with widget as only argument """ + urwid.connect_signal(self, "click", self._event_callback, callback) + + +class LiberviaTUIPairsContainer(xmlui.PairsContainer, sat_widgets.TableContainer): + def __init__(self, _xmlui_parent): + options = {"ADAPT": (0,), "HIGHLIGHT": (0,)} + if self._xmlui_main.type == "param": + options["FOCUS_ATTR"] = "param_selected" + sat_widgets.TableContainer.__init__(self, columns=2, options=options) + + def _xmlui_append(self, widget): + if isinstance(widget, LiberviaTUIEmptyWidget): + # we don't want highlight on empty widgets + widget = urwid.AttrMap(widget, "default") + self.add_widget(widget) + + +class LiberviaTUILabelContainer(LiberviaTUIPairsContainer, xmlui.LabelContainer): + pass + + +class LiberviaTUITabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer): + def __init__(self, _xmlui_parent): + sat_widgets.TabsContainer.__init__(self) + + def _xmlui_append(self, widget): + self.body.append(widget) + + def _xmlui_add_tab(self, label, selected): + tab = LiberviaTUIVerticalContainer(None) + self.add_tab(label, tab, selected) + return tab + + +class LiberviaTUIVerticalContainer(xmlui.VerticalContainer, urwid.ListBox): + BOX_HEIGHT = 5 + + def __init__(self, _xmlui_parent): + urwid.ListBox.__init__(self, urwid.SimpleListWalker([])) + self._last_size = None + + def _xmlui_append(self, widget): + if "flow" not in widget.sizing(): + widget = urwid.BoxAdapter(widget, self.BOX_HEIGHT) + self.body.append(widget) + + def render(self, size, focus=False): + if size != self._last_size: + (maxcol, maxrow) = size + if self.body: + widget = self.body[0] + if isinstance(widget, urwid.BoxAdapter): + widget.height = maxrow + self._last_size = size + return super(LiberviaTUIVerticalContainer, self).render(size, focus) + + +### Dialogs ### + + +class LiberviaTUIDialog(object): + def __init__(self, _xmlui_parent): + self.host = _xmlui_parent.host + + def _xmlui_show(self): + self.host.show_pop_up(self) + + def _xmlui_close(self): + self.host.remove_pop_up(self) + + +class LiberviaTUIMessageDialog(LiberviaTUIDialog, xmlui.MessageDialog, sat_widgets.Alert): + def __init__(self, _xmlui_parent, title, message, level): + LiberviaTUIDialog.__init__(self, _xmlui_parent) + xmlui.MessageDialog.__init__(self, _xmlui_parent) + sat_widgets.Alert.__init__( + self, title, message, ok_cb=lambda __: self._xmlui_close() + ) + + +class LiberviaTUINoteDialog(xmlui.NoteDialog, LiberviaTUIMessageDialog): + # TODO: separate NoteDialog + pass + + +class LiberviaTUIConfirmDialog( + LiberviaTUIDialog, xmlui.ConfirmDialog, sat_widgets.ConfirmDialog +): + def __init__(self, _xmlui_parent, title, message, level, buttons_set): + LiberviaTUIDialog.__init__(self, _xmlui_parent) + xmlui.ConfirmDialog.__init__(self, _xmlui_parent) + sat_widgets.ConfirmDialog.__init__( + self, + title, + message, + no_cb=lambda __: self._xmlui_cancelled(), + yes_cb=lambda __: self._xmlui_validated(), + ) + + +class LiberviaTUIFileDialog( + LiberviaTUIDialog, xmlui.FileDialog, files_management.FileDialog +): + def __init__(self, _xmlui_parent, title, message, level, filetype): + # TODO: message is not managed yet + LiberviaTUIDialog.__init__(self, _xmlui_parent) + xmlui.FileDialog.__init__(self, _xmlui_parent) + style = [] + if filetype == C.XMLUI_DATA_FILETYPE_DIR: + style.append("dir") + files_management.FileDialog.__init__( + self, + ok_cb=lambda path: self._xmlui_validated({"path": path}), + cancel_cb=lambda __: self._xmlui_cancelled(), + message=message, + title=title, + style=style, + ) + + +class GenericFactory(object): + def __getattr__(self, attr): + if attr.startswith("create"): + cls = globals()[ + "LiberviaTUI" + attr[6:] + ] # XXX: we prefix with "LiberviaTUI" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names + return cls + + +class WidgetFactory(GenericFactory): + def __getattr__(self, attr): + if attr.startswith("create"): + cls = GenericFactory.__getattr__(self, attr) + cls._xmlui_main = self._xmlui_main + return cls + + +class XMLUIPanel(xmlui.XMLUIPanel, LiberviaTUIWidget): + widget_factory = WidgetFactory() + + def __init__( + self, + host, + parsed_xml, + title=None, + flags=None, + callback=None, + ignore=None, + whitelist=None, + profile=C.PROF_KEY_NONE, + ): + self.widget_factory._xmlui_main = self + self._dest = None + xmlui.XMLUIPanel.__init__( + self, + host, + parsed_xml, + title=title, + flags=flags, + callback=callback, + ignore=ignore, + profile=profile, + ) + LiberviaTUIWidget.__init__(self, self.main_cont, self.xmlui_title) + + + def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None): + # Small hack to always have a VerticalContainer as main container in LiberviaTUI. + # this used to be the default behaviour for all frontends, but now + # TabsContainer can also be the main container. + if _xmlui_parent is self: + node = current_node.childNodes[0] + if node.nodeName == "container" and node.getAttribute("type") == "tabs": + _xmlui_parent = self.widget_factory.createVerticalContainer(self) + self.main_cont = _xmlui_parent + return super(XMLUIPanel, self)._parse_childs(_xmlui_parent, current_node, wanted, + data) + + + def construct_ui(self, parsed_dom): + def post_treat(): + assert self.main_cont.body + + if self.type in ("form", "popup"): + buttons = [] + if self.type == "form": + buttons.append(urwid.Button(_("Submit"), self.on_form_submitted)) + if not "NO_CANCEL" in self.flags: + buttons.append(urwid.Button(_("Cancel"), self.on_form_cancelled)) + else: + buttons.append( + urwid.Button(_("OK"), on_press=lambda __: self._xmlui_close()) + ) + max_len = max([len(button.get_label()) for button in buttons]) + grid_wid = urwid.GridFlow(buttons, max_len + 4, 1, 0, "center") + self.main_cont.body.append(grid_wid) + elif self.type == "param": + tabs_cont = self.main_cont.body[0].base_widget + assert isinstance(tabs_cont, sat_widgets.TabsContainer) + buttons = [] + buttons.append(sat_widgets.CustomButton(_("Save"), self.on_save_params)) + buttons.append( + sat_widgets.CustomButton( + _("Cancel"), lambda x: self.host.remove_window() + ) + ) + max_len = max([button.get_size() for button in buttons]) + grid_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center") + tabs_cont.add_footer(grid_wid) + + xmlui.XMLUIPanel.construct_ui(self, parsed_dom, post_treat) + urwid.WidgetWrap.__init__(self, self.main_cont) + + def show(self, show_type=None, valign="middle"): + """Show the constructed UI + @param show_type: how to show the UI: + - None (follow XMLUI's recommendation) + - 'popup' + - 'window' + @param valign: vertical alignment when show_type is 'popup'. + Ignored when show_type is 'window'. + + """ + if show_type is None: + if self.type in ("window", "param"): + show_type = "window" + elif self.type in ("popup", "form"): + show_type = "popup" + + if show_type not in ("popup", "window"): + raise ValueError("Invalid show_type [%s]" % show_type) + + self._dest = show_type + if show_type == "popup": + self.host.show_pop_up(self, valign=valign) + elif show_type == "window": + self.host.new_widget(self, user_action=self.user_action) + else: + assert False + self.host.redraw() + + def _xmlui_close(self): + if self._dest == "window": + self.host.remove_window() + elif self._dest == "popup": + self.host.remove_pop_up(self) + else: + raise exceptions.InternalError( + "self._dest unknown, are you sure you have called XMLUI.show ?" + ) + + +class XMLUIDialog(xmlui.XMLUIDialog): + dialog_factory = GenericFactory() + + +xmlui.register_class(xmlui.CLASS_PANEL, XMLUIPanel) +xmlui.register_class(xmlui.CLASS_DIALOG, XMLUIDialog) +create = xmlui.create diff -r 47401850dec6 -r b620a8e882e1 setup.py --- a/setup.py Fri Jun 02 14:54:26 2023 +0200 +++ b/setup.py Fri Jun 02 16:25:25 2023 +0200 @@ -137,8 +137,8 @@ "jp = libervia.cli.base:LiberviaCli.run", # TUI + alias - "libervia-tui = libervia.frontends.primitivus.base:PrimitivusApp.run", - "primitivus = libervia.frontends.primitivus.base:PrimitivusApp.run", + "libervia-tui = libervia.tui.base:LiberviaTUIApp.run", + "primitivus = libervia.tui.base:LiberviaTUIApp.run", ], }, zip_safe=False,