Inputs are more modulable (they also have a common history and clipboard)....

Inputs are more modulable (they also have a common history and clipboard). Search is now fully functional, and some other stuff
parent 8bdab491
......@@ -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)
......
......@@ -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()
......
......@@ -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.text = ''
self.pos = 0
self.line_pos = 0
self.rewrite_text()
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):
"""
Get the previous line in the history
"""
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
def move_cursor_to_pos(self):
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):