From 1b995e4bf04b8eb8d5a40039596a7b7a8277fb03 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Fri, 21 Jan 2011 04:46:21 +0100 Subject: Data form support. supported yet: text-single, text-private, list-single, boolean The interface is really ugly, but, well, it works --- src/connection.py | 1 + src/core.py | 21 +++- src/data_forms.py | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tabs.py | 31 ++++- src/user.py | 3 +- src/windows.py | 29 ++++- 6 files changed, 408 insertions(+), 12 deletions(-) create mode 100644 src/data_forms.py (limited to 'src') diff --git a/src/connection.py b/src/connection.py index e7e85710..3133a645 100644 --- a/src/connection.py +++ b/src/connection.py @@ -54,6 +54,7 @@ class Connection(sleekxmpp.ClientXMPP): self.auto_authorize = None self.register_plugin('xep_0030') self.register_plugin('xep_0045') + self.register_plugin('xep_0004') if config.get('send_poezio_info', 'true') == 'true': info = {'name':'poezio', 'version':'0.7'} diff --git a/src/core.py b/src/core.py index 7d837202..6d05f472 100644 --- a/src/core.py +++ b/src/core.py @@ -42,6 +42,7 @@ import multiuserchat as muc import tabs import windows +from data_forms import DataFormsTab from connection import connection from config import config from logger import logger @@ -159,6 +160,8 @@ 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.xmpp.add_event_handler("changed_subscription", self.on_changed_subscription) + self.xmpp.add_event_handler("message_xform", self.on_data_form) + self.information(_('Welcome to poezio!')) self.refresh_window() @@ -196,6 +199,21 @@ class Core(object): tab.on_info_win_size_changed() self.refresh_window() + def on_data_form(self, message): + """ + When a data form is received + """ + self.information('%s' % messsage) + + def open_new_form(self, form, on_cancel, on_send, **kwargs): + """ + Open a new tab containing the form + The callback are called with the completed form as parameter in + addition with kwargs + """ + form_tab = DataFormsTab(self, form, on_cancel, on_send, kwargs) + self.add_tab(form_tab, True) + def on_got_offline(self, presence): jid = presence['from'] contact = roster.get_contact_by_jid(jid.bare) @@ -334,7 +352,7 @@ class Core(object): if not room.joined: # user in the room BEFORE us. # ignore redondant presence message, see bug #1509 if from_nick not in [user.nick for user in room.users]: - new_user = User(from_nick, affiliation, show, status, role) + new_user = User(from_nick, affiliation, show, status, role, jid) room.users.append(new_user) if from_nick == room.own_nick: room.joined = True @@ -700,6 +718,7 @@ class Core(object): """ curses.curs_set(1) curses.noecho() + curses.nonl() theme.init_colors() stdscr.keypad(True) diff --git a/src/data_forms.py b/src/data_forms.py new file mode 100644 index 00000000..5480213d --- /dev/null +++ b/src/data_forms.py @@ -0,0 +1,335 @@ +# Copyright 2010-2011 Le Coz Florent +# +# 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 . + +""" +Defines the data-forms Tab and all the Windows for it. +""" + +import logging +log = logging.getLogger(__name__) +import curses + +from windows import g_lock +import windows +from tabs import Tab + +class DataFormsTab(Tab): + """ + A tab contaning various window type, displaying + a form that the user needs to fill. + """ + def __init__(self, core, form, on_cancel, on_send, kwargs): + Tab.__init__(self, core) + self._form = form + self._on_cancel = on_cancel + self._on_send = on_send + self._kwargs = kwargs + for field in self._form: + self.fields.append(field) + self.topic_win = windows.Topic() + self.tab_win = windows.GlobalInfoBar() + self.form_win = FormWin(form, self.height-3, self.width, 1, 0) + self.help_win = windows.HelpText("Ctrl+Y: send form, Ctrl+G: cancel") + self.key_func['KEY_UP'] = self.form_win.go_to_previous_input + self.key_func['KEY_DOWN'] = self.form_win.go_to_next_input + self.key_func['^G'] = self.on_cancel + self.key_func['^Y'] = self.on_send + self.resize() + + def on_cancel(self): + self._on_cancel(self._form) + + def on_send(self): + self._form.reply() + self.form_win.reply() + self._on_send(self._form) + + def on_input(self, key): + if key in self.key_func: + return self.key_func[key]() + self.form_win.on_input(key) + + def resize(self): + Tab.resize(self) + self.topic_win.resize(1, self.width, 0, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.form_win.resize(self.height-3, self.width, 1, 0) + self.help_win.resize(1, self.width, self.height-1, 0, None) + self.lines = [] + + def refresh(self, tabs, informations, _): + self.topic_win.refresh(self._form['title']) + self.tab_win.refresh(tabs, tabs[0]) + self.help_win.refresh() + self.form_win.refresh() + +class FieldInput(object): + """ + All input type in a data form should inherite this class, + in addition with windows.Input or any relevant class from the + 'windows' library. + """ + def __init__(self, field): + self._field = field + self.color = 14 + + def set_color(self, color): + self.color = color + self.refresh() + + def update_field_value(self, value): + raise NotImplementedError + + def resize(self, height, width, y, x): + self._resize(height, width, y, x, None) + + def is_dummy(self): + return False + + def reply(self): + """ + Set the correct response value in the field + """ + raise NotImplementedError + +class DummyInput(FieldInput, windows.Win): + def __init__(self, field): + FieldInput.__init__(self, field) + windows.Win.__init__(self) + + def do_command(self): + return + + def refresh(self): + return + + def is_dummy(self): + return True + +class BooleanWin(FieldInput, windows.Win): + def __init__(self, field): + FieldInput.__init__(self, field) + windows.Win.__init__(self) + self.last_key = 'KEY_RIGHT' + self.value = bool(field.getValue()) + + def do_command(self, key): + if key == 'KEY_LEFT' or key == 'KEY_RIGHT': + self.value = not self.value + self.last_key = key + self.refresh() + + def refresh(self): + with g_lock: + self._win.attron(curses.color_pair(self.color)) + self.addnstr(0, 0, ' '*(8), self.width) + self.addstr(0, 2, "%s"%self.value) + self.addstr(0, 8, '→') + self.addstr(0, 0, '←') + if self.last_key == 'KEY_RIGHT': + self.addstr(0, 8, '') + else: + self.addstr(0, 0, '') + self._win.attroff(curses.color_pair(self.color)) + self._refresh() + + def reply(self): + self._field['label'] = '' + self._field.setAnswer(self.value) + +class ListSingleWin(FieldInput, windows.Win): + def __init__(self, field): + FieldInput.__init__(self, field) + windows.Win.__init__(self) + # the option list never changes + self.options = field.getOptions() + # val_pos is the position of the currently selected option + self.val_pos = 0 + for i, option in enumerate(self.options): + if field.getValue() == option['value']: + self.val_pos = i + + def do_command(self, key): + if key == 'KEY_LEFT': + if self.val_pos > 0: + self.val_pos -= 1 + elif key == 'KEY_RIGHT': + if self.val_pos < len(self.options)-1: + self.val_pos += 1 + else: + return + self.refresh() + + def refresh(self): + with g_lock: + self._win.attron(curses.color_pair(self.color)) + self.addnstr(0, 0, ' '*self.width, self.width) + if self.val_pos > 0: + self.addstr(0, 0, '←') + if self.val_pos < len(self.options)-1: + self.addstr(0, self.width-1, '→') + option = self.options[self.val_pos]['label'] + self.addstr(0, self.width//2-len(option)//2, option) + self._win.attroff(curses.color_pair(self.color)) + self._refresh() + + def reply(self): + self._field['label'] = '' + self._field.delOptions() + self._field.setAnswer(self.options[self.val_pos]['value']) + +class TextSingleWin(FieldInput, windows.Input): + def __init__(self, field): + FieldInput.__init__(self, field) + windows.Input.__init__(self) + self.text = field.getValue() if isinstance(field.getValue(), str)\ + else "" + self.pos = len(self.text) + self.color = 14 + + def reply(self): + self._field['label'] = '' + self._field.setAnswer(self.get_text()) + +class TextPrivateWin(TextSingleWin): + def __init__(self, field): + TextSingleWin.__init__(self, field) + + def rewrite_text(self): + with g_lock: + self._win.erase() + if self.color: + self._win.attron(curses.color_pair(self.color)) + self.addstr('*'*len(self.text[self.line_pos:self.line_pos+self.width-1])) + if self.color: + (y, x) = self._win.getyx() + size = self.width-x + self.addnstr(' '*size, size, curses.color_pair(self.color)) + self.addstr(0, self.pos, '') + if self.color: + self._win.attroff(curses.color_pair(self.color)) + self._refresh() + +class FormWin(object): + """ + A window, with some subwins (the various inputs). + On init, create all the subwins. + On resize, move and resize all the subwin and define how the text will be written + On refresh, write all the text, and refresh all the subwins + """ + input_classes = {'boolean': BooleanWin, + 'text-single': TextSingleWin, + 'text-multi': TextSingleWin, + 'jid-single': TextSingleWin, + 'text-private': TextPrivateWin, + 'fixed': DummyInput, + 'list-single': ListSingleWin} + def __init__(self, form, height, width, y, x): + self._form = form + self._win = curses.newwin(height, width, y, x) + self.current_input = 0 + self.inputs = [] # dict list + for (name, field) in self._form.getFields(): + if field['type'] == 'hidden': + continue + try: + input_class = self.input_classes[field['type']] + except: + field.setValue(field['type']) + input_class = TextSingleWin + instructions = field['instructions'] + label = field['label'] + if field['type'] == 'fixed': + label = field.getValue() + inp = input_class(field) + self.inputs.append({'label':label, + 'instructions':instructions, + 'input':inp}) + + def resize(self, height, width, y, x): + self._win.resize(height, width) + self.height = height + self.width = width + + def reply(self): + """ + Set the response values in the form, for each field + from the corresponding input + """ + for inp in self.inputs: + if inp['input'].is_dummy(): + continue + else: + inp['input'].reply() + self._form['title'] = '' + self._form['instructions'] = '' + + def go_to_next_input(self): + if not self.inputs: + return + if self.current_input == len(self.inputs) - 1: + return + self.inputs[self.current_input]['input'].set_color(14) + self.current_input += 1 + jump = 0 + while self.current_input+jump != len(self.inputs) - 1 and self.inputs[self.current_input+jump]['input'].is_dummy(): + jump += 1 + if self.inputs[self.current_input+jump]['input'].is_dummy(): + return + self.current_input += jump + self.inputs[self.current_input]['input'].set_color(13) + + def go_to_previous_input(self): + if not self.inputs: + return + if self.current_input == 0: + return + self.inputs[self.current_input]['input'].set_color(14) + self.current_input -= 1 + jump = 0 + while self.current_input-jump > 0 and self.inputs[self.current_input+jump]['input'].is_dummy(): + jump += 1 + if self.inputs[self.current_input+jump]['input'].is_dummy(): + return + self.current_input -= jump + self.inputs[self.current_input]['input'].set_color(13) + + def on_input(self, key): + if not self.inputs: + return + self.inputs[self.current_input]['input'].do_command(key) + + def refresh(self): + with g_lock: + self._win.erase() + y = 0 + i = 0 + for name, field in self._form.getFields(): + if field['type'] == 'hidden': + continue + label = self.inputs[i]['label'] + self._win.addstr(y, 0, label) + self.inputs[i]['input'].resize(1, self.width//3, y+1, 2*self.width//3) + if field['instructions']: + y += 1 + self._win.addstr(y, 0, field['instructions']) + y += 1 + i += 1 + self._win.refresh() + for inp in self.inputs: + inp['input'].refresh() + self.inputs[self.current_input]['input'].set_color(13) + self.inputs[self.current_input]['input'].refresh() diff --git a/src/tabs.py b/src/tabs.py index 19688713..023c9cf3 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -51,6 +51,7 @@ class Tab(object): def __init__(self, core): self.core = core # a pointer to core, to access its attributes (ugly?) + self._color_state = theme.COLOR_TAB_NORMAL self.nb = Tab.number Tab.number += 1 self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() @@ -133,7 +134,7 @@ class Tab(object): """ returns the color that should be used in the GlobalInfoBar """ - return theme.COLOR_TAB_NORMAL + return self._color_state def set_color_state(self, color): """ @@ -160,13 +161,13 @@ class Tab(object): """ called when this tab loses the focus. """ - pass + self._color_state = theme.COLOR_TAB_NORMAL def on_gain_focus(self): """ called when this tab gains the focus. """ - pass + self._color_state = theme.COLOR_TAB_CURRENT def add_message(self): """ @@ -367,7 +368,8 @@ class MucTab(ChatTab): self.commands['nick'] = (self.command_nick, _("Usage: /nick \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.commands['cycle'] = (self.command_cycle, _('Usage: /cycle [message]\nCycle: Leaves the current room and rejoin it immediately'), None) - self.commands['info'] = (self.command_info, _('Usage: /info \nInfoDisplay some information about the user in the MUC: his/here role, affiliation, status and status message.'), None) + self.commands['info'] = (self.command_info, _('Usage: /info \nInfo: Display some information about the user in the MUC: his/here role, affiliation, status and status message.'), None) + self.commands['configure'] = (self.command_configure, _('Usage: /configure\nConfigure: Configure the current room, through a form.'), None) self.resize() def scroll_user_list_up(self): @@ -388,9 +390,28 @@ class MucTab(ChatTab): user = self.get_room().get_user_by_name(args[0]) if not user: return self.core.information("Unknown user: %s" % args[0]) - self.get_room().add_message("%s: show: %s, affiliation: %s, role: %s\n%s"% (args[0], user.show or 'Available', user.role or 'None', user.affiliation or 'None', user.status)) + self.get_room().add_message("%s%s: show: %s, affiliation: %s, role: %s\n%s"% (args[0], user.user.show or 'Available', user.role or 'None', user.affiliation or 'None', user.status)) self.core.refresh_window() + def command_configure(self, arg): + form = self.core.xmpp.plugin['xep_0045'].getRoomForm(self.get_name()) + self.core.information('%s' % form) + self.core.open_new_form(form, self.cancel_config, self.send_config) + + def cancel_config(self, form): + """ + The user do not want to send his/her config, send an iq cancel + """ + self.core.xmpp.plugin['xep_0045'].cancelConfig(self.get_name()) + self.core.close_tab() + + def send_config(self, form): + """ + The user sends his/her config to the server + """ + self.core.xmpp.plugin['xep_0045'].configureRoom(self.get_name(), form) + self.core.close_tab() + def command_cycle(self, arg): if self.get_room().joined: muc.leave_groupchat(self.core.xmpp, self.get_name(), self.get_room().own_nick, arg) diff --git a/src/user.py b/src/user.py index 2cbe32d2..82d74193 100644 --- a/src/user.py +++ b/src/user.py @@ -36,11 +36,12 @@ class User(object): """ keep trace of an user in a Room """ - def __init__(self, nick, affiliation, show, status, role): + def __init__(self, nick, affiliation, show, status, role, jid): self.last_talked = datetime(1, 1, 1) # The oldest possible time self.update(affiliation, show, status, role) self.change_nick(nick) self.color = choice(theme.LIST_COLOR_NICKNAMES) + self.jid = jid def update(self, affiliation, show, status, role): self.affiliation = affiliation diff --git a/src/windows.py b/src/windows.py index e425913b..0acce6db 100644 --- a/src/windows.py +++ b/src/windows.py @@ -695,19 +695,24 @@ class Input(Win): "KEY_BACKSPACE": self.key_backspace, '^?': self.key_backspace, } - Win.__init__(self) self.text = '' self.pos = 0 # cursor position self.line_pos = 0 # position (in self.text) of + self.on_input = None # callback called on any key pressed + self.color = None # use this color on addstr + + def set_color(self, color): + self.color = color + self.rewrite_text() def is_empty(self): return len(self.text) == 0 - def resize(self, height, width, y, x, stdscr): + def resize(self, height, width, y, x, stdscr=None): # TODO remove stdscr self._resize(height, width, y, x, stdscr) - self._win.erase() - self.addnstr(0, 0, self.text, self.width-1) + # self._win.erase() + # self.addnstr(0, 0, self.text, self.width-1) def jump_word_left(self): """ @@ -960,8 +965,12 @@ class Input(Win): self.key_end(False) def do_command(self, key, reset=True): + log.debug('do_command: %s\n' % key) if key in self.key_func: - return self.key_func[key]() + res = self.key_func[key]() + if self.on_input: + self.on_input(self.get_text()) + return res if not key or len(key) > 1: return False # ignore non-handled keyboard shortcuts self.reset_completion() @@ -973,6 +982,8 @@ class Input(Win): self.pos += len(key) if reset: self.rewrite_text() + if self.on_input: + self.on_input(self.get_text()) return True def get_text(self): @@ -987,8 +998,16 @@ class Input(Win): """ with g_lock: self._win.erase() + if self.color: + self._win.attron(curses.color_pair(self.color)) self.addstr(self.text[self.line_pos:self.line_pos+self.width-1]) + if self.color: + (y, x) = self._win.getyx() + size = self.width-x + self.addnstr(' '*size, size, curses.color_pair(self.color)) self.addstr(0, self.pos, '') # WTF, this works but .move() doesn't… + if self.color: + self._win.attroff(curses.color_pair(self.color)) self._refresh() def refresh(self): -- cgit v1.2.3