summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorent Le Coz <louiz@louiz.org>2011-01-21 04:46:21 +0100
committerFlorent Le Coz <louiz@louiz.org>2011-01-21 04:46:21 +0100
commit1b995e4bf04b8eb8d5a40039596a7b7a8277fb03 (patch)
tree929238043f8502528265c77b2ac8a955b8f4bdec /src
parent33c69df12b1621129b7af1a21eb180454d01e21b (diff)
downloadpoezio-1b995e4bf04b8eb8d5a40039596a7b7a8277fb03.tar.gz
poezio-1b995e4bf04b8eb8d5a40039596a7b7a8277fb03.tar.bz2
poezio-1b995e4bf04b8eb8d5a40039596a7b7a8277fb03.tar.xz
poezio-1b995e4bf04b8eb8d5a40039596a7b7a8277fb03.zip
Data form support.
supported yet: text-single, text-private, list-single, boolean The interface is really ugly, but, well, it works
Diffstat (limited to 'src')
-rw-r--r--src/connection.py1
-rw-r--r--src/core.py21
-rw-r--r--src/data_forms.py335
-rw-r--r--src/tabs.py31
-rw-r--r--src/user.py3
-rw-r--r--src/windows.py29
6 files changed, 408 insertions, 12 deletions
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 <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/>.
+
+"""
+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 <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.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 <nickname>\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 <nickname>\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):