From de7c007a22fc9f8b6aa9ddd94e55ec4f2b13c71d Mon Sep 17 00:00:00 2001 From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13" Date: Thu, 11 Nov 2010 20:44:14 +0000 Subject: [PATCH] Inputs are more modulable (they also have a common history and clipboard). Search is now fully functional, and some other stuff --- src/core.py | 42 +------ src/tab.py | 119 +++++++++++++------- src/window.py | 300 +++++++++++++++++++++++++++++--------------------- 3 files changed, 257 insertions(+), 204 deletions(-) diff --git a/src/core.py b/src/core.py index e41df02b..40d727b4 100644 --- a/src/core.py +++ b/src/core.py @@ -155,7 +155,6 @@ class Core(object): self.xmpp.add_event_handler("roster_update", self.on_roster_update) self.xmpp.add_event_handler("changed_status", self.on_presence) - # self.__debug_fill_roster() def grow_information_win(self): """ @@ -486,13 +485,10 @@ class Core(object): """ """ jid = presence['from'] - log.debug('Presence Received: %s\n' % presence) contact = roster.get_contact_by_jid(jid.bare) - log.debug('Contact: %s\n' % contact) if not contact: return resource = contact.get_resource_by_fulljid(jid.full) - log.debug('Resource: %s\n' % resource) if not resource: return status = presence['type'] @@ -504,34 +500,6 @@ class Core(object): if isinstance(self.current_tab(), RosterInfoTab): self.refresh_window() - def __debug_fill_roster(self): - for i in range(10): - jid = 'contact%s@fion%s.org'%(i,i) - contact = Contact(jid) - contact.set_ask('wat') - contact.set_subscription('both') - roster.add_contact(contact, jid) - contact.set_name('%s %s fion'%(i,i)) - roster.edit_groups_of_contact(contact, ['hello']) - for i in range(10): - jid = 'test%s@bernard%s.org'%(i,i) - contact = Contact(jid) - contact.set_ask('wat') - contact.set_subscription('both') - roster.add_contact(contact, jid) - contact.set_name('%s test'%(i)) - roster.edit_groups_of_contact(contact, ['hello']) - for i in range(10): - jid = 'pouet@top%s.org'%(i) - contact = Contact(jid) - contact.set_ask('wat') - contact.set_subscription('both') - roster.add_contact(contact, jid) - contact.set_name('%s oula'%(i)) - roster.edit_groups_of_contact(contact, ['hello']) - if isinstance(self.current_tab(), RosterInfoTab): - self.refresh_window() - def on_roster_update(self, iq): """ A subscription changed, or we received a roster item @@ -1374,20 +1342,13 @@ class Core(object): if not key: return res = self.current_tab().on_input(key) - if not res: - return - if key in ('^J', '\n') and isinstance(res, str): - self.execute(res) - else : - # we did "enter" with an empty input in the roster - self.on_roster_enter_key(res) + self.refresh_window() def on_roster_enter_key(self, roster_row): """ when enter is pressed on the roster window """ if isinstance(roster_row, Contact): - # roster_row.toggle_folded() if not self.get_conversation_by_jid(roster_row.get_bare_jid()): self.open_conversation_window(roster_row.get_bare_jid()) else: @@ -1427,7 +1388,6 @@ class Core(object): muc.send_private_message(self.xmpp, self.current_tab().get_name(), line) if isinstance(self.current_tab(), PrivateTab) or\ isinstance(self.current_tab(), ConversationTab): - log.debug('ALLO ICI\n\n') self.add_message_to_text_buffer(self.current_tab().get_room(), line, None, self.own_nick) elif isinstance(self.current_tab(), MucTab): muc.send_groupchat_message(self.xmpp, self.current_tab().get_name(), line) diff --git a/src/tab.py b/src/tab.py index 01ffeb42..5086735b 100644 --- a/src/tab.py +++ b/src/tab.py @@ -216,7 +216,7 @@ class MucTab(Tab): self.info_header = window.MucInfoWin(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) self.info_win = window.TextWin(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible) + self.input = window.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) def resize(self, stdscr): """ @@ -244,14 +244,17 @@ class MucTab(Tab): self.input.refresh() def on_input(self, key): - self.key_func = { + key_func = { "\t": self.completion, "^I": self.completion, "M-i": self.completion, "KEY_BTAB": self.last_words_completion, + "^J": self.on_enter, + "^M": self.on_enter, + "\n": self.on_enter } - if key in self.key_func: - return self.key_func[key]() + if key in key_func: + return key_func[key]() return self.input.do_command(key) def completion(self): @@ -271,7 +274,6 @@ class MucTab(Tab): for msg in self._room.messages[:-40:-1]: if not msg: continue - log.debug('line: %s\n'%msg) for char in char_we_dont_want: msg.txt = msg.txt.replace(char, ' ') for word in msg.txt.split(): @@ -279,6 +281,12 @@ class MucTab(Tab): words.append(word) self.input.auto_completion(words, False) + def on_enter(self): + """ + When enter is pressed, send the message to the Muc + """ + self.core.execute(self.input.key_enter()) + def get_color_state(self): return self._room.color_state @@ -313,7 +321,7 @@ class MucTab(Tab): self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) def just_before_refresh(self): - self.input.move_cursor_to_pos() + return def on_close(self): return @@ -329,7 +337,7 @@ class PrivateTab(Tab): self.info_header = window.PrivateInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) self.info_win = window.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible) + self.input = window.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) def resize(self, stdscr): Tab.resize(self, stdscr) @@ -359,8 +367,21 @@ class PrivateTab(Tab): return self._room.name def on_input(self, key): + key_func = { + "^J": self.on_enter, + "^M": self.on_enter, + "\n": self.on_enter + } + if key in key_func: + return key_func[key]() return self.input.do_command(key) + def on_enter(self): + """ + When enter is pressed, send the message to the Muc + """ + self.core.execute(self.input.key_enter()) + def on_lose_focus(self): self._room.set_color_state(theme.COLOR_TAB_NORMAL) self._room.remove_line_separator() @@ -395,17 +416,6 @@ class RosterInfoTab(Tab): A tab, splitted in two, containing the roster and infos """ def __init__(self, stdscr, core): - self.single_key_commands = { - "^J": self.on_enter, - "^M": self.on_enter, - "\n": self.on_enter, - ' ': self.on_space, - "/": self.on_slash, - "KEY_UP": self.move_cursor_up, - "KEY_DOWN": self.move_cursor_down, - "o": self.toggle_offline_show, - "^F": self.start_search, - } Tab.__init__(self, stdscr, core) self.name = "Roster" roster_width = self.width//2 @@ -415,7 +425,8 @@ class RosterInfoTab(Tab): self.info_win = window.TextWin(self.height-2, info_width, 0, roster_width+1, stdscr, self.visible) self.roster_win = window.RosterWin(self.height-2-3, roster_width, 0, 0, stdscr, self.visible) self.contact_info_win = window.ContactInfoWin(3, roster_width, self.height-2-3, 0, stdscr, self.visible) - self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible, False, "Enter commands with “/”. “o”: toggle offline show") + self.default_help_message = window.HelpText(1, self.width, self.height-1, 0, stdscr, self.visible, "Enter commands with “/”. “o”: toggle offline show") + self.input = self.default_help_message self.set_color_state(theme.COLOR_TAB_NORMAL) def resize(self, stdscr): @@ -447,19 +458,22 @@ class RosterInfoTab(Tab): self._color_state = color def on_input(self, key): - if self.input.input_mode: - ret = self.input.do_command(key) - roster._contact_filter = (jid_and_name_match, self.input.text) - # if the input is empty, go back to command mode - if self.input.is_empty() and not self.input._instructions: - self.input.input_mode = False - curses.curs_set(0) - self.input.rewrite_text() - if self.input._instructions: - return True - return ret - if key in self.single_key_commands: - return self.single_key_commands[key]() + key_commands = { + "^J": self.on_enter, + "^M": self.on_enter, + "\n": self.on_enter, + ' ': self.on_space, + "/": self.on_slash, + "KEY_UP": self.move_cursor_up, + "KEY_DOWN": self.move_cursor_down, + "o": self.toggle_offline_show, + "^F": self.start_search, + } + res = self.input.do_command(key) + if res: + return res + if key in key_commands: + return key_commands[key]() def toggle_offline_show(self): """ @@ -476,9 +490,19 @@ class RosterInfoTab(Tab): """ '/' is pressed, we enter "input mode" """ - self.input.input_mode = True curses.curs_set(1) - self.on_input("/") # we add the slash + self.input = window.CommandInput(1, self.width, self.height-1, 0, self.default_help_message, self.visible, "", self.reset_help_message, self.execute_slash_command) + 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 @@ -518,6 +542,7 @@ class RosterInfoTab(Tab): 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): @@ -526,15 +551,17 @@ class RosterInfoTab(Tab): in it. """ curses.curs_set(1) - roster._contact_filter = (jid_and_name_match, self.input.text) - self.input.input_mode = True - self.input.start_command(self.on_search_terminate, self.on_search_terminate, '[search]') + self.input = window.CommandInput(1, self.width, self.height-1, 0, self.default_help_message, self.visible, "[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) return True + def set_roster_filter(self, txt): + roster._contact_filter = (jid_and_name_match, txt) + def on_search_terminate(self, txt): curses.curs_set(0) roster._contact_filter = None - return True + self.reset_help_message() + return False def just_before_refresh(self): return @@ -556,7 +583,7 @@ class ConversationTab(Tab): self.info_header = window.ConversationInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) self.info_win = window.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible) + self.input = window.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) def resize(self, stdscr): Tab.resize(self, stdscr) @@ -588,8 +615,22 @@ class ConversationTab(Tab): return self._name def on_input(self, key): + key_func = { + "^J": self.on_enter, + "^M": self.on_enter, + "\n": self.on_enter + } + if key in key_func: + return key_func[key]() return self.input.do_command(key) + + def on_enter(self): + """ + When enter is pressed, send the message to the Muc + """ + self.core.execute(self.input.key_enter()) + def on_lose_focus(self): self.set_color_state(theme.COLOR_TAB_NORMAL) self._text_buffer.remove_line_separator() diff --git a/src/window.py b/src/window.py index 88683853..db8d2822 100644 --- a/src/window.py +++ b/src/window.py @@ -598,25 +598,54 @@ class TextWin(Win): self.visible = visible self._resize(height, width, y, x, stdscr, visible) +class HelpText(Win): + """ + A buffer just displaying a read-only message. + Usually used to replace an Input when the tab is in + command mode. + """ + def __init__(self, height, width, y, x, parent_win, visible, text=''): + self.visible = visible + Win.__init__(self, height, width, y, x, parent_win) + self.txt = text + + def resize(self, height, width, y, x, stdscr, visible): + self._resize(height, width, y, x, stdscr, visible) + + def refresh(self): + if not self.visible: + return + with g_lock: + self._win.erase() + self.addstr(0, 0, self.txt[:self.width-1], curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self.finish_line(theme.COLOR_INFORMATION_BAR) + self._refresh() + + def do_command(self, key): + return False + class Input(Win): """ - The line where text is entered - It can be in input mode or in commmand mode. - Command mode means that single_key_commands can be entered, handled - by the Tab object, while this input just displays an help text. + The simplest Input possible, provides just a way to edit a single line + of text. It also has a clipboard, common to all Inputs. + Doesn't have any history. + It doesn't do anything when enter is pressed either. + This should be herited for all kinds of Inputs, for example MessageInput + or the little inputs in dataforms, etc, adding specific features (completion etc) + It features two kinds of completion, but they have to be called from outside (the Tab), + passing the list of items that can be used to complete. The completion can be used + in a very flexible way. """ - def __init__(self, height, width, y, x, stdscr, visible, input_mode=True, help_text=''): + clipboard = '' # A common clipboard for all the inputs, this makes + # it easy cut and paste text between various input + def __init__(self, height, width, y, x, stdscr, visible): self.key_func = { "KEY_LEFT": self.key_left, "M-D": self.key_left, "KEY_RIGHT": self.key_right, "M-C": self.key_right, - "KEY_UP": self.key_up, - "M-A": self.key_up, "KEY_END": self.key_end, "KEY_HOME": self.key_home, - "KEY_DOWN": self.key_down, - "M-B": self.key_down, "KEY_DC": self.key_dc, '^D': self.key_dc, 'M-b': self.jump_word_left, @@ -629,69 +658,13 @@ class Input(Win): 'M-f': self.jump_word_right, "KEY_BACKSPACE": self.key_backspace, '^?': self.key_backspace, - '^J': self.on_enter, - '\n': self.on_enter, } Win.__init__(self, height, width, y, x, stdscr) - self.input_mode = input_mode - self.help_text = help_text # the text displayed in command_mode self.visible = visible - self.history = [] self.text = '' - self.clipboard = None self.pos = 0 # cursor position self.line_pos = 0 # position (in self.text) of - # the first char to display on the screen - self.histo_pos = 0 - self.hit_list = [] # current possible completion (normal) - self.last_completion = None # Contains the last nickname completed, - # if last key was a tab - # These are used when the user is entering a comand - self._on_cancel = None - self._on_terminate = None - self._instructions = "" # a string displayed before the input, read-only - - def on_enter(self): - """ - Called when Enter is pressed - """ - if not self._instructions: - return self.get_text() - self.on_terminate() - return True - - def start_command(self, on_cancel, on_terminate, instructions): - """ - Start a command, with an instruction, and two callbacks. - on_terminate is called when the command is successfull - on_cancel is called when the command is canceled - """ - assert isinstance(instructions, str) - self._on_cancel = on_cancel - self._on_terminate = on_terminate - self._instructions = instructions - - def cancel_command(self): - """ - Call it to cancel the current command - """ - self._on_cancel() - self._on_cancel = None - self._on_terminate = None - self._instructions = '' - return self.get_text() - - def on_terminate(self): - """ - Call it to terminate the command. Returns the content of the input - """ - txt = self.get_text() - self._on_terminate(txt) - self._on_terminate = None - self._on_cancel = None - self._instructions = '' - return txt def is_empty(self): return len(self.text) == 0 @@ -716,6 +689,7 @@ class Input(Win): diff = self.pos+self.line_pos-previous_space for i in range(diff): self.key_left() + return True def jump_word_right(self): """ @@ -729,6 +703,7 @@ class Input(Win): diff = next_space - (self.pos+self.line_pos) for i in range(diff): self.key_right() + return True def delete_word(self): """ @@ -743,6 +718,7 @@ class Input(Win): for i in range(diff): self.key_backspace(False) self.rewrite_text() + return True def delete_end_of_line(self): """ @@ -750,9 +726,10 @@ class Input(Win): """ if len(self.text) == self.pos+self.line_pos: return # nothing to cut - self.clipboard = self.text[self.pos+self.line_pos:] + Input.clipboard = self.text[self.pos+self.line_pos:] self.text = self.text[:self.pos+self.line_pos] self.key_end() + return True def delete_begining_of_line(self): """ @@ -760,18 +737,20 @@ class Input(Win): """ if self.pos+self.line_pos == 0: return - self.clipboard = self.text[:self.pos+self.line_pos] + Input.clipboard = self.text[:self.pos+self.line_pos] self.text = self.text[self.pos+self.line_pos:] self.key_home() + return True def paste_clipboard(self): """ Insert what is in the clipboard at the cursor position """ - if not self.clipboard or len(self.clipboard) == 0: + if not Input.clipboard or len(Input.clipboard) == 0: return - for letter in self.clipboard: + for letter in Input.clipboard: self.do_command(letter) + return True def key_dc(self): """ @@ -782,36 +761,7 @@ class Input(Win): return # end of line, nothing to delete self.text = self.text[:self.pos+self.line_pos]+self.text[self.pos+self.line_pos+1:] self.rewrite_text() - - def key_up(self): - """ - Get the previous line in the history - """ - if not len(self.history): - return - self._win.erase() - if self.histo_pos >= 0: - self.histo_pos -= 1 - self.text = self.history[self.histo_pos+1] - self.key_end() - - def key_down(self): - """ - Get the next line in the history - """ - if not len(self.history): - return - self.reset_completion() - if self.histo_pos < len(self.history)-1: - self.histo_pos += 1 - self.text = self.history[self.histo_pos] - self.key_end() - else: - self.histo_pos = len(self.history)-1 - self.text = '' - self.pos = 0 - self.line_pos = 0 - self.rewrite_text() + return True def key_home(self): """ @@ -821,6 +771,7 @@ class Input(Win): self.pos = 0 self.line_pos = 0 self.rewrite_text() + return True def key_end(self, reset=False): """ @@ -835,6 +786,7 @@ class Input(Win): self.pos = len(self.text) self.line_pos = 0 self.rewrite_text() + return True def key_left(self): """ @@ -847,6 +799,7 @@ class Input(Win): elif self.pos >= 1: self.pos -= 1 self.rewrite_text() + return True def key_right(self): """ @@ -860,6 +813,7 @@ class Input(Win): elif self.pos < len(self.text): self.pos += 1 self.rewrite_text() + return True def key_backspace(self, reset=True): """ @@ -873,6 +827,7 @@ class Input(Win): self.key_left() if reset: self.rewrite_text() + return True def auto_completion(self, user_list, add_after=True): """ @@ -885,6 +840,7 @@ class Input(Win): self.shell_completion(user_list, add_after) else: self.normal_completion(user_list, add_after) + return True def reset_completion(self): """ @@ -977,7 +933,7 @@ class Input(Win): if key in self.key_func: return self.key_func[key]() if not key or len(key) > 1: - return # ignore non-handled keyboard shortcuts + return False # ignore non-handled keyboard shortcuts self.reset_completion() self.text = self.text[:self.pos+self.line_pos]+key+self.text[self.pos+self.line_pos:] (y, x) = self._win.getyx() @@ -987,38 +943,22 @@ class Input(Win): self.pos += 1 if reset: self.rewrite_text() + return True def get_text(self): """ Clear the input and return the text entered so far """ - txt = self.text - self.text = '' - self.pos = 0 - self.line_pos = 0 - if len(txt) != 0: - self.history.append(txt) - self.histo_pos = len(self.history)-1 - self.rewrite_text() - return txt + return self.text def rewrite_text(self): """ Refresh the line onscreen, from the pos and pos_line """ with g_lock: - self.clear_text() - if self.input_mode: - self.addstr(self._instructions, curses.color_pair(theme.COLOR_INFORMATION_BAR)) - if self._instructions: - self.addstr(' ') - self.addstr(self.text[self.line_pos:self.line_pos+self.width-1]) - else: - self.addstr(self.help_text, curses.color_pair(theme.COLOR_INFORMATION_BAR)) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self._win.erase() + self.addstr(self.text[self.line_pos:self.line_pos+self.width-1]) cursor_pos = self.pos - if self._instructions: - cursor_pos += 1 + len(self._instructions) self.addstr(0, cursor_pos, '') # WTF, this works but .move() doesn't… self._refresh() @@ -1028,13 +968,125 @@ class Input(Win): self.rewrite_text() def clear_text(self): - self._win.erase() + self.text = '' + self.pos = 0 + self.line_pos = 0 + self.rewrite_text() - def move_cursor_to_pos(self): +class MessageInput(Input): + """ + The input featuring history and that is being used in + Conversation, Muc and Private tabs + """ + history = list() # The history is common to all MessageInput + + def __init__(self, height, width, y, x, stdscr, visible): + Input.__init__(self, height, width, y, x, stdscr, visible) + self.histo_pos = 0 + self.key_func["KEY_UP"] = self.key_up + self.key_func["M-A"] = self.key_up + self.key_func["KEY_DOWN"] = self.key_down + self.key_func["M-B"] = self.key_down + + def key_up(self): """ - move the cursor at the current pos + Get the previous line in the history """ - return + if not len(MessageInput.history): + return + self.reset_completion() + self._win.erase() + if self.histo_pos >= 0: + self.histo_pos -= 1 + self.text = MessageInput.history[self.histo_pos+1] + self.key_end() + + def key_down(self): + """ + Get the next line in the history + """ + if not len(MessageInput.history): + return + self.reset_completion() + if self.histo_pos < len(MessageInput.history)-1: + self.histo_pos += 1 + self.text = self.history[self.histo_pos] + self.key_end() + else: + self.histo_pos = len(MessageInput.history)-1 + self.text = '' + self.pos = 0 + self.line_pos = 0 + self.rewrite_text() + + def key_enter(self): + txt = self.get_text() + if len(txt) != 0: + self.history.append(txt) + self.histo_pos = len(self.history)-1 + self.clear_text() + return txt + +class CommandInput(Input): + """ + An input with an help message in the left, with three given callbacks: + one when when successfully 'execute' the command and when we abort it. + The last callback is optional and is called on any input key + This input is used, for example, in the RosterTab when, to replace the + HelpMessage when a command is started + The on_input callback + """ + def __init__(self, height, width, y, x, stdscr, visible, + help_message, on_abort, on_success, on_input=None): + Input.__init__(self, height, width, y, x, stdscr, visible) + self.on_abort = on_abort + self.on_success = on_success + self.on_input = on_input + self.help_message = help_message + self.key_func['^J'] = self.success + self.key_func['^M'] = self.success + self.key_func['\n'] = self.success + self.key_func['^G'] = self.abort + + def do_command(self, key): + res = Input.do_command(self, key) + if self.on_input: + self.on_input(self.get_text()) + log.debug('do_command returns : %s\n' % res) + return res + + def success(self): + """ + call the success callback, passing the text as argument + """ + self.on_input = None + log.debug('before on_success') + res = self.on_success(self.get_text()) + log.debug('after on_success, res: %s'%res) + return res + + def abort(self): + """ + Call the abort callback, passing the text as argument + """ + self.on_input = None + return self.on_abort(self.get_text()) + + def rewrite_text(self): + """ + Rewrite the text just like a normal input, but with the instruction + on the left + """ + with g_lock: + self._win.erase() + self.addstr(self.help_message, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + cursor_pos = self.pos + len(self.help_message) + if len(self.help_message): + self.addstr(' ') + cursor_pos += 1 + self.addstr(self.text[self.line_pos:self.line_pos+self.width-1]) + self.addstr(0, cursor_pos, '') # WTF, this works but .move() doesn't… + self._refresh() class VerticalSeparator(Win): """