diff options
Diffstat (limited to 'src/tabs.py')
-rw-r--r-- | src/tabs.py | 1058 |
1 files changed, 1058 insertions, 0 deletions
diff --git a/src/tabs.py b/src/tabs.py new file mode 100644 index 00000000..2dc6a017 --- /dev/null +++ b/src/tabs.py @@ -0,0 +1,1058 @@ +# Copyright 2010 Le Coz Florent <louiz@louiz.org> +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see <http://www.gnu.org/licenses/>. + +""" +a Tab object is a way to organize various Windows (see windows.py) +around the screen at once. +A tab is then composed of multiple Buffer. +Each Tab object has different refresh() and resize() methods, defining how its +Buffer are displayed, resized, etc +""" + +MIN_WIDTH = 50 +MIN_HEIGHT = 16 + +from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset, + gettext as _) + +import logging +log = logging.getLogger(__name__) + +import windows +import theme +import curses +import difflib +import shlex + +from sleekxmpp.xmlstream.stanzabase import JID +from config import config +from roster import RosterGroup, roster +from contact import Contact, Resource +import multiuserchat as muc + +class Tab(object): + number = 0 + + def __init__(self, core): + self.core = core # a pointer to core, to access its attributes (ugly?) + self.nb = Tab.number + Tab.number += 1 + self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() + if self.height < MIN_HEIGHT or self.width < MIN_WIDTH: + self.visible = False + else: + self.visible = True + self.key_func = {} # each tab should add their keys in there + # and use them in on_input + self.commands = {} # and their own commands + + def complete_commands(self, the_input): + """ + Does command completion on the specified input for both global and tab-specific + commands. + This should be called from the completion method (on tab, for example), passing + the input where completion is to be made. + It can completion the command name itself or an argument of the command. + Returns True if a completion was made, False else. + """ + txt = the_input.get_text() + # check if this is a command + if txt.startswith('/') and not txt.startswith('//'): + # check if we are in the middle of the command name + if len(txt.split()) > 1 or\ + (txt.endswith(' ') and not the_input.last_completion): + command_name = txt.split()[0][1:] + if command_name in self.core.commands: + command = self.core.commands[command_name] + elif command_name in self.commands: + command = self.commands[command_name] + else: # Unknown command, cannot complete + return False + if command[2] is None: + return False # There's no completion functio + else: + return command[2](the_input) + else: + # complete the command's name + words = ['/%s'%(name) for name in list(self.core.commands.keys())] +\ + ['/%s'% (name) for name in list(self.commands.keys())] + the_input.auto_completion(words, '') + return True + return False + + def resize(self): + self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() + if self.height < MIN_HEIGHT or self.width < MIN_WIDTH: + self.visible = False + else: + self.visible = True + + def refresh(self, tabs, informations, roster): + """ + Called on each screen refresh (when something has changed) + """ + raise NotImplementedError + + def get_color_state(self): + """ + returns the color that should be used in the GlobalInfoBar + """ + raise NotImplementedError + + def set_color_state(self, color): + """ + set the color state + """ + raise NotImplementedError + + def get_name(self): + """ + get the name of the tab + """ + raise NotImplementedError + + def on_input(self, key): + pass + + def on_lose_focus(self): + """ + called when this tab loses the focus. + """ + pass + + def on_gain_focus(self): + """ + called when this tab gains the focus. + """ + pass + + def add_message(self): + """ + Adds a message in the tab. + If the tab cannot add a message in itself (for example + FormTab, where text is not intented to be appened), it returns False. + If the tab can, it returns True + """ + return False + + def on_scroll_down(self): + """ + Defines what happens when we scrol down + """ + pass + + def on_scroll_up(self): + """ + Defines what happens when we scrol down + """ + pass + + def on_info_win_size_changed(self): + """ + Called when the window with the informations is resized + """ + pass + + def just_before_refresh(self): + """ + Method called just before the screen refresh. + Particularly useful to move the cursor at the + correct position. + """ + pass + + def on_close(self): + """ + Called when the tab is to be closed + """ + pass + +class InfoTab(Tab): + """ + The information tab, used to display global informations + when using a anonymous account + """ + def __init__(self, core, name): + Tab.__init__(self, core) + self.tab_win = windows.GlobalInfoBar() + self.text_win = windows.TextWin() + self.input = windows.Input() + self.name = name + self.color_state = theme.COLOR_TAB_NORMAL + self.resize() + + def resize(self): + Tab.resize(self) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.text_win.resize(self.height-2, self.width, 0, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, _): + if not self.visible: + return + self.text_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def get_name(self): + return self.name + + def get_color_state(self): + return self.color_state + + def set_color_state(self, color): + return + + def on_input(self, key): + return self.input.do_command(key) + + def on_lose_focus(self): + self.color_state = theme.COLOR_TAB_NORMAL + + def on_gain_focus(self): + self.color_state = theme.COLOR_TAB_CURRENT + curses.curs_set(0) + + def on_scroll_up(self): + pass + + def on_scroll_down(self): + pass + + def on_info_win_size_changed(self): + return + + def just_before_refresh(self): + return + + def on_close(self): + return + +class ChatTab(Tab): + """ + A tab containing a chat of any type. + Just use this class instead of Tab if the tab needs a recent-words completion + Also, \n, ^J and ^M are already bound to on_enter + And also, add the /say command + """ + def __init__(self, core, room): + Tab.__init__(self, core) + self._room = room + self.key_func['M-/'] = self.last_words_completion + self.key_func['^J'] = self.on_enter + self.key_func['^M'] = self.on_enter + self.key_func['\n'] = self.on_enter + self.commands['say'] = (self.command_say, + _("""Usage: /say <message>\nSay: Just send the message. + Useful if you want your message to begin with a '/'"""), None) + + def last_words_completion(self): + """ + Complete the input with words recently said + """ + # build the list of the recent words + char_we_dont_want = [',', '(', ')', '.', '"', '\'', ' ', # The last one is nbsp + '’', '“', '”', ':', ';', '[', ']', '{', '}'] + words = list() + for msg in self._room.messages[:-40:-1]: + if not msg: + continue + txt = msg.txt + for char in char_we_dont_want: + txt = txt.replace(char, ' ') + for word in txt.split(): + if len(word) >= 4 and word not in words: + words.append(word) + self.input.auto_completion(words, ' ') + + def on_enter(self): + txt = self.input.key_enter() + if txt.startswith('/') and not txt.startswith('//') and\ + not txt.startswith('/me '): + command = txt.strip().split()[0][1:] + arg = txt[2+len(command):] # jump the '/' and the ' ' + if command in self.core.commands: # check global commands + self.core.commands[command][0](arg) + elif command in self.commands: # check tab-specific commands + self.commands[command][0](arg) + else: + self.core.information(_("Unknown command (%s)") % (command), _('Error')) + else: + if txt.startswith('//'): + txt = txt[1:] + self.command_say(txt) + + def command_say(self, line): + raise NotImplementedError + +class MucTab(ChatTab): + """ + The tab containing a multi-user-chat room. + It contains an userlist, an input, a topic, an information and a chat zone + """ + def __init__(self, core, room): + ChatTab.__init__(self, core, room) + self.topic_win = windows.Topic() + self.text_win = windows.TextWin() + self.v_separator = windows.VerticalSeparator() + self.user_win = windows.UserList() + self.info_header = windows.MucInfoWin() + self.info_win = windows.TextWin() + self.tab_win = windows.GlobalInfoBar() + self.input = windows.MessageInput() + self.ignores = [] # set of Users + # keys + self.key_func['^I'] = self.completion + self.key_func['M-i'] = self.completion + # commands + self.commands['ignore'] = (self.command_ignore, _("Usage: /ignore <nickname> \nIgnore: Ignore a specified nickname."), None) + self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list."), None) + self.commands['kick'] = (self.command_kick, _("Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason."), None) + self.commands['topic'] = (self.command_topic, _("Usage: /topic <subject>\nTopic: Change the subject of the room"), None) + self.commands['query'] = (self.command_query, _('Usage: /query <nick> [message]\nQuery: Open a private conversation with <nick>. This nick has to be present in the room you\'re currently in. If you specified a message after the nickname, it will immediately be sent to this user'), None) + self.commands['part'] = (self.command_part, _("Usage: /part [message]\n Part: disconnect from a room. You can specify an optional message."), None) + self.commands['nick'] = (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room"), None) + self.commands['recolor'] = (self.command_recolor, _('Usage: /recolor\nRecolor: Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.'), None) + self.resize() + + def command_recolor(self, arg): + """ + Re-assign color to the participants of the room + """ + room = self.get_room() + i = 0 + compare_users = lambda x: x.last_talked + users = list(room.users) + # search our own user, to remove it from the room + for user in users: + if user.nick == room.own_nick: + users.remove(user) + nb_color = len(theme.LIST_COLOR_NICKNAMES) + for user in sorted(users, key=compare_users, reverse=True): + user.color = theme.LIST_COLOR_NICKNAMES[i % nb_color] + i+= 1 + self.core.refresh_window() + + def command_nick(self, arg): + """ + /nick <nickname> + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) != 1: + return + nick = args[0] + room = self.get_room() + if not room.joined: + return + muc.change_nick(self.core.xmpp, room.name, nick) + + def command_part(self, arg): + """ + /part [msg] + """ + args = arg.split() + reason = None + room = self.get_room() + if len(args): + msg = ' '.join(args) + else: + msg = None + if self.get_room().joined: + muc.leave_groupchat(self.core.xmpp, room.name, room.own_nick, arg) + self.core.close_tab() + + def command_query(self, arg): + """ + /query <nick> [message] + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) < 1: + return + nick = args[0] + room = self.get_room() + r = None + for user in room.users: + if user.nick == nick: + r = self.core.open_private_window(room.name, user.nick) + if r and len(args) > 1: + msg = arg[len(nick)+1:] + muc.send_private_message(self.core.xmpp, r.name, msg) + self.core.add_message_to_text_buffer(r, msg, None, r.own_nick) + + def command_topic(self, arg): + """ + /topic [new topic] + """ + if not arg.strip(): + self.core.add_message_to_text_buffer(self.get_room(), + _("The subject of the room is: %s") % self.get_room().topic) + return + subject = arg + muc.change_subject(self.core.xmpp, self.get_room().name, subject) + + def command_kick(self, arg): + """ + /kick <nick> [reason] + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) < 1: + self.core.command_help('kick') + return + nick = args[0] + if len(args) >= 2: + reason = ' '.join(args[1:]) + else: + reason = '' + if not self.get_room().joined: + return + res = muc.eject_user(self.core.xmpp, self.get_name(), nick, reason) + if res['type'] == 'error': + self.core.room_error(res, self.get_name()) + + def command_say(self, line): + muc.send_groupchat_message(self.core.xmpp, self.get_name(), line) + + def command_ignore(self, arg): + """ + /ignore <nick> + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) != 1: + self.core.command_help('ignore') + return + nick = args[0] + user = self._room.get_user_by_name(nick) + if not user: + self.core.information(_('%s is not in the room') % nick) + elif user in self.ignores: + self.core.information(_('%s is already ignored') % nick) + else: + self.ignores.append(user) + self.core.information(_("%s is now ignored") % nick, 'info') + + def command_unignore(self, arg): + """ + /unignore <nick> + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) != 1: + self.core.command_help('unignore') + return + nick = args[0] + user = self._room.get_user_by_name(nick) + if not user: + self.core.information(_('%s is not in the room') % nick) + elif user not in self.ignores: + self.core.information(_('%s is not ignored') % nick) + else: + self.ignores.remove(user) + self.core.information(_('%s is now unignored') % nick) + + def resize(self): + """ + Resize the whole window. i.e. all its sub-windows + """ + Tab.resize(self) + text_width = (self.width//10)*9 + self.topic_win.resize(1, self.width, 0, 0, self.core.stdscr) + self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) + self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10), self.core.stdscr) + self.user_win.resize(self.height-3, self.width-text_width-1, 1, text_width+1, self.core.stdscr) + self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, _): + if not self.visible: + return + self.topic_win.refresh(self._room.topic) + self.text_win.refresh(self._room) + self.v_separator.refresh() + self.user_win.refresh(self._room.users) + self.info_header.refresh(self._room) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def on_input(self, key): + if key in self.key_func: + self.key_func[key]() + return False + self.input.do_command(key) + return False + + def completion(self): + """ + Called when Tab is pressed, complete the nickname in the input + """ + if self.complete_commands(self.input): + return + compare_users = lambda x: x.last_talked + word_list = [user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)] + after = config.get('after_completion', ',')+" " + if ' ' not in self.input.get_text() or (self.input.last_completion and\ + self.input.get_text()[:-len(after)] == self.input.last_completion): + add_after = after + else: + add_after = ' ' + self.input.auto_completion(word_list, add_after) + + def get_color_state(self): + return self._room.color_state + + def set_color_state(self, color): + self._room.set_color_state(color) + + def get_name(self): + return self._room.name + + def get_room(self): + return self._room + + def on_lose_focus(self): + self._room.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.remove_line_separator() + self._room.add_line_separator() + + def on_gain_focus(self): + self._room.set_color_state(theme.COLOR_TAB_CURRENT) + curses.curs_set(1) + + def on_scroll_up(self): + self._room.scroll_up(self.text_win.height-1) + + def on_scroll_down(self): + self._room.scroll_down(self.text_win.height-1) + + def on_info_win_size_changed(self): + text_width = (self.width//10)*9 + self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) + self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + + def just_before_refresh(self): + return + + def on_close(self): + return + +class PrivateTab(ChatTab): + """ + The tab containg a private conversation (someone from a MUC) + """ + def __init__(self, core, room): + ChatTab.__init__(self, core, room) + self.text_win = windows.TextWin() + self.info_header = windows.PrivateInfoWin() + self.info_win = windows.TextWin() + self.tab_win = windows.GlobalInfoBar() + self.input = windows.MessageInput() + # keys + self.key_func['^I'] = self.completion + self.key_func['M-i'] = self.completion + # commands + self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None) + self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None) + self.resize() + + def completion(self): + self.complete_commands(self.input) + + def command_say(self, line): + muc.send_private_message(self.core.xmpp, self.get_name(), line) + self.core.add_message_to_text_buffer(self.get_room(), line, None, self.get_room().own_nick) + + def command_unquery(self, arg): + """ + /unquery + """ + self.core.close_tab() + + def resize(self): + Tab.resize(self) + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, _): + if not self.visible: + return + self.text_win.refresh(self._room) + self.info_header.refresh(self._room) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def get_color_state(self): + if self._room.color_state == theme.COLOR_TAB_NORMAL or\ + self._room.color_state == theme.COLOR_TAB_CURRENT: + return self._room.color_state + return theme.COLOR_TAB_PRIVATE + + def set_color_state(self, color): + self._room.color_state = color + + def get_name(self): + return self._room.name + + def on_input(self, key): + if key in self.key_func: + self.key_func[key]() + return False + self.input.do_command(key) + return False + + def on_lose_focus(self): + self._room.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.remove_line_separator() + self._room.add_line_separator() + + def on_gain_focus(self): + self._room.set_color_state(theme.COLOR_TAB_CURRENT) + curses.curs_set(1) + + def on_scroll_up(self): + self._room.scroll_up(self.text_win.height-1) + + def on_scroll_down(self): + self._room.scroll_down(self.text_win.height-1) + + def on_info_win_size_changed(self): + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + + def get_room(self): + return self._room + + def just_before_refresh(self): + return + + def on_close(self): + return + +class RosterInfoTab(Tab): + """ + A tab, splitted in two, containing the roster and infos + """ + def __init__(self, core): + Tab.__init__(self, core) + self.name = "Roster" + self.v_separator = windows.VerticalSeparator() + self.tab_win = windows.GlobalInfoBar() + self.info_win = windows.TextWin() + self.roster_win = windows.RosterWin() + self.contact_info_win = windows.ContactInfoWin() + self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show") + self.input = self.default_help_message + self.set_color_state(theme.COLOR_TAB_NORMAL) + self.key_func['^I'] = self.completion + self.key_func['M-i'] = self.completion + self.key_func["^J"] = self.on_enter + self.key_func["^M"] = self.on_enter + self.key_func[' '] = self.on_space + self.key_func["/"] = self.on_slash + self.key_func["KEY_UP"] = self.move_cursor_up + self.key_func["KEY_DOWN"] = self.move_cursor_down + self.key_func["o"] = self.toggle_offline_show + self.key_func["^F"] = self.start_search + self.resize() + + def resize(self): + Tab.resize(self) + roster_width = self.width//2 + info_width = self.width-roster_width-1 + self.v_separator.resize(self.height-2, 1, 0, roster_width, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.info_win.resize(self.height-2, info_width, 0, roster_width+1, self.core.stdscr) + self.roster_win.resize(self.height-2-3, roster_width, 0, 0, self.core.stdscr) + self.contact_info_win.resize(3, roster_width, self.height-2-3, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def completion(self): + # Check if we are entering a command (with the '/' key) + if isinstance(self.input, windows.CommandInput) and\ + not self.input.help_message: + self.complete_commands(self.input) + + def refresh(self, tabs, informations, roster): + if not self.visible: + return + self.v_separator.refresh() + self.roster_win.refresh(roster) + self.contact_info_win.refresh(self.roster_win.get_selected_row()) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def get_name(self): + return self.name + + def get_color_state(self): + return self._color_state + + def set_color_state(self, color): + self._color_state = color + + def on_input(self, key): + res = self.input.do_command(key) + if res: + return True + if key in self.key_func: + return self.key_func[key]() + + def toggle_offline_show(self): + """ + Show or hide offline contacts + """ + option = 'roster_show_offline' + if config.get(option, 'false') == 'false': + config.set_and_save(option, 'true') + else: + config.set_and_save(option, 'false') + return True + + def on_slash(self): + """ + '/' is pressed, we enter "input mode" + """ + curses.curs_set(1) + self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + self.input.do_command("/") # we add the slash + + def reset_help_message(self, _=None): + curses.curs_set(0) + self.input = self.default_help_message + return True + + def execute_slash_command(self, txt): + if txt.startswith('/'): + self.core.execute(txt) + return self.reset_help_message() + + def on_lose_focus(self): + self._color_state = theme.COLOR_TAB_NORMAL + + def on_gain_focus(self): + self._color_state = theme.COLOR_TAB_CURRENT + curses.curs_set(0) + + def add_message(self): + return False + + def move_cursor_down(self): + self.roster_win.move_cursor_down() + return True + + def move_cursor_up(self): + self.roster_win.move_cursor_up() + return True + + def on_scroll_down(self): + # Scroll info win + pass + + def on_scroll_up(self): + # Scroll info down + pass + + def on_info_win_size_changed(self): + pass + + def on_space(self): + selected_row = self.roster_win.get_selected_row() + if isinstance(selected_row, RosterGroup) or\ + isinstance(selected_row, Contact): + selected_row.toggle_folded() + return True + + def on_enter(self): + selected_row = self.roster_win.get_selected_row() + self.core.on_roster_enter_key(selected_row) + return selected_row + + def start_search(self): + """ + Start the search. The input should appear with a short instruction + in it. + """ + curses.curs_set(1) + self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + return True + + def set_roster_filter(self, txt): + roster._contact_filter = (jid_and_name_match, txt) + self.roster_win.refresh(roster) + return False + + def on_search_terminate(self, txt): + curses.curs_set(0) + roster._contact_filter = None + self.reset_help_message() + return True + + def just_before_refresh(self): + return + + def on_close(self): + return + +class ConversationTab(ChatTab): + """ + The tab containg a normal conversation (someone from our roster) + """ + def __init__(self, core, text_buffer, jid): + ChatTab.__init__(self, core, text_buffer) + self.color_state = theme.COLOR_TAB_NORMAL + self._name = jid # a conversation tab is linked to one specific full jid OR bare jid + self.text_win = windows.TextWin() + self.upper_bar = windows.ConversationStatusMessageWin() + self.info_header = windows.ConversationInfoWin() + self.info_win = windows.TextWin() + self.tab_win = windows.GlobalInfoBar() + self.input = windows.MessageInput() + # keys + self.key_func['^I'] = self.completion + self.key_func['M-i'] = self.completion + # commands + self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None) + self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None) + self.resize() + + def completion(self): + self.complete_commands(self.input) + + def command_say(self, line): + muc.send_private_message(self.core.xmpp, self.get_name(), line) + self.core.add_message_to_text_buffer(self.get_room(), line, None, self.core.own_nick) + + def command_unquery(self, arg): + """ + /unquery + """ + self.core.close_tab() + + def resize(self): + Tab.resize(self) + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 1, 0, self.core.stdscr) + self.upper_bar.resize(1, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, roster): + if not self.visible: + return + self.text_win.refresh(self._room) + self.upper_bar.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name())) + self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self._room) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def get_color_state(self): + if self.color_state == theme.COLOR_TAB_NORMAL or\ + self.color_state == theme.COLOR_TAB_CURRENT: + return self.color_state + return theme.COLOR_TAB_PRIVATE + + def set_color_state(self, color): + self.color_state = color + + def get_name(self): + return self._name + + def on_input(self, key): + if key in self.key_func: + self.key_func[key]() + return False + self.input.do_command(key) + return False + + def on_lose_focus(self): + self.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.remove_line_separator() + self._room.add_line_separator() + + def on_gain_focus(self): + self.set_color_state(theme.COLOR_TAB_CURRENT) + curses.curs_set(1) + + def on_scroll_up(self): + self._room.scroll_up(self.text_win.height-1) + + def on_scroll_down(self): + self._room.scroll_down(self.text_win.height-1) + + def on_info_win_size_changed(self): + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + + def get_room(self): + return self._room + + def just_before_refresh(self): + return + + def on_close(self): + return + +class MucListTab(Tab): + """ + A tab listing rooms from a specific server, displaying various information, + scrollable, and letting the user join them, etc + """ + def __init__(self, core, server): + Tab.__init__(self, core) + self._color_state = theme.COLOR_TAB_NORMAL + self.name = server + self.upper_message = windows.Topic() + columns = ('node-part','name', 'users') + self.list_header = windows.ColumnHeaderWin(columns) + self.listview = windows.ListWin(columns) + self.tab_win = windows.GlobalInfoBar() + self.default_help_message = windows.HelpText("“j”: join room. “i”: information") + self.input = self.default_help_message + self.key_func["KEY_DOWN"] = self.listview.move_cursor_down + self.key_func["KEY_UP"] = self.listview.move_cursor_up + self.key_func["/"] = self.on_slash + self.key_func['j'] = self.join_selected + self.key_func['J'] = self.join_selected_no_focus + self.resize() + + def refresh(self, tabs, informations, roster): + self.upper_message.refresh('Chatroom list on server %s' % self.name) + self.list_header.refresh() + self.listview.refresh() + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def resize(self): + Tab.resize(self) + self.upper_message.resize(1, self.width, 0, 0, self.core.stdscr) + column_size = {'node-part': (self.width-5)//4, + 'name': (self.width-5)//4*3, + 'users': 5} + self.list_header.resize_columns(column_size) + self.list_header.resize(1, self.width, 1, 0, self.core.stdscr) + self.listview.resize_columns(column_size) + self.listview.resize(self.height-4, self.width, 2, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def on_slash(self): + """ + '/' is pressed, activate the input + """ + curses.curs_set(1) + self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + self.input.do_command("/") # we add the slash + + def join_selected_no_focus(self): + return + + def join_selected(self): + jid = self.listview.get_selected_row()['jid'] + self.core.command_join(jid) + + def reset_help_message(self, _=None): + curses.curs_set(0) + self.input = self.default_help_message + return True + + def execute_slash_command(self, txt): + if txt.startswith('/'): + self.core.execute(txt) + return self.reset_help_message() + + def get_color_state(self): + return theme.COLOR_TAB_NORMAL + + def set_color_state(self, color): + pass + + def get_name(self): + return self.name + + def on_input(self, key): + res = self.input.do_command(key) + if res: + return True + if key in self.key_func: + return self.key_func[key]() + + def on_lose_focus(self): + self._color_state = theme.COLOR_TAB_NORMAL + + def on_gain_focus(self): + self._color_state = theme.COLOR_TAB_CURRENT + curses.curs_set(0) + + def get_color_state(self): + return self._color_state + + +def diffmatch(search, string): + """ + Use difflib and a loop to check if search_pattern can + be 'almost' found INSIDE a string. + 'almost' being defined by difflib + """ + l = len(search) + ratio = 0.7 + for i in range(len(string) - l + 1): + if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio: + return True + return False + +def jid_and_name_match(contact, txt): + """ + A function used to know if a contact in the roster should + be shown in the roster + """ + ratio = 0.7 + if not txt: + return True # Everything matches when search is empty + user = JID(contact.get_bare_jid()).user + if diffmatch(txt, user): + return True + if contact.get_name() and diffmatch(txt, contact.get_name()): + return True + return False |