From ecc0403981699a6924662734fa271164f38154d2 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 25 Jun 2006 17:03:59 +0000 Subject: [PATCH] Invoking commands works. It will be a long way to finish it, though... As long as the responder uses only text-single, list-single and boolean fields, it is possible to invoke and complete a command. Other field types are not implemented now in dataforms.py. --- data/glade/data_form_window.glade | 11 +- src/adhoc_commands.py | 197 ++++++++++++++++++--- src/dataforms.py | 277 ++++++++++++++++++------------ 3 files changed, 351 insertions(+), 134 deletions(-) diff --git a/data/glade/data_form_window.glade b/data/glade/data_form_window.glade index 0081400cb..d7a2eb92b 100644 --- a/data/glade/data_form_window.glade +++ b/data/glade/data_form_window.glade @@ -251,14 +251,13 @@ - + True + 1 + 3 False - 0 - - - - + 0 + 0 0 diff --git a/src/adhoc_commands.py b/src/adhoc_commands.py index ce0a936a9..d47c06937 100644 --- a/src/adhoc_commands.py +++ b/src/adhoc_commands.py @@ -30,6 +30,7 @@ import common.xmpp as xmpp import common.gajim as gajim import gtkgui_helpers +import dataforms class CommandWindow: '''Class for a window for single ad-hoc commands session. Note, that @@ -49,6 +50,11 @@ class CommandWindow: self.pulse_id=None # to satisfy self.setup_pulsing() self.commandlist=None # a list of (commandname, commanddescription) + # command's data + self.commandnode = None + self.sessionid = None + self.dataform = None + # retrieving widgets from xml self.xml = gtkgui_helpers.get_glade('adhoc_commands_window.glade') self.window = self.xml.get_widget('adhoc_commands_window') @@ -56,10 +62,15 @@ class CommandWindow: 'execute_button','stages_notebook', 'retrieving_commands_stage_vbox', 'command_list_stage_vbox','command_list_vbox', - 'sending_form_stage_vbox','no_commands_stage_vbox', - 'error_stage_vbox', 'error_description_label'): + 'sending_form_stage_vbox','sending_form_progressbar', + 'no_commands_stage_vbox','error_stage_vbox', + 'error_description_label'): self.__dict__[name] = self.xml.get_widget(name) - + + # creating data forms widget + self.data_form_widget = dataforms.DataFormWidget() + self.sending_form_stage_vbox.pack_start(self.data_form_widget) + # setting initial stage self.stage1() @@ -74,12 +85,32 @@ class CommandWindow: # these functions are set up by appropriate stageX methods def stage_finish(self, *anything): pass - def on_cancel_button_clicked(self, *anything): pass - def on_back_button_clicked(self, *anything): pass - def on_forward_button_clicked(self, *anything): pass - def on_execute_button_clicked(self, *anything): pass - def on_adhoc_commands_window_destroy(self, *anything): pass - def do_nothing(self, *anything): pass + def stage_cancel_button_clicked(self, *anything): assert False + def stage_back_button_clicked(self, *anything): assert False + def stage_forward_button_clicked(self, *anything): assert False + def stage_execute_button_clicked(self, *anything): assert False + def stage_adhoc_commands_window_destroy(self, *anything): assert False + def stage_adhoc_commands_window_delete_event(self, *anything): assert False + def do_nothing(self, *anything): return False + +# widget callbacks + def on_cancel_button_clicked(self, *anything): + return self.stage_cancel_button_clicked(*anything) + + def on_back_button_clicked(self, *anything): + return self.stage_back_button_clicked(*anything) + + def on_forward_button_clicked(self, *anything): + return self.stage_forward_button_clicked(*anything) + + def on_execute_button_clicked(self, *anything): + return self.stage_execute_button_clicked(*anything) + + def on_adhoc_commands_window_destroy(self, *anything): + return self.stage_adhoc_commands_window_destroy(*anything) + + def on_adhoc_commands_window_delete_event(self, *anything): + return self.stage_adhoc_commands_window_delete_event(self, *anything) # stage 1: waiting for command list def stage1(self): @@ -106,17 +137,23 @@ class CommandWindow: # setup the callbacks self.stage_finish = self.stage1_finish - self.on_cancel_button_clicked = self.stage1_on_cancel_button_clicked + self.stage_cancel_button_clicked = self.stage1_cancel_button_clicked + self.stage_adhoc_commands_window_delete_event = self.stage1_adhoc_commands_window_delete_event + self.stage_adhoc_commands_window_destroy = self.do_nothing def stage1_finish(self): self.remove_pulsing() - def stage1_on_cancel_button_clicked(self, widget): + def stage1_cancel_button_clicked(self, widget): # cancelling in this stage is not critical, so we don't # show any popups to user self.stage1_finish() self.window.destroy() + def stage1_adhoc_commands_window_delete_event(self, widget): + self.stage1_finish() + return True + # stage 2: choosing the command to execute def stage2(self): '''Populate the command list vbox with radiobuttons @@ -140,30 +177,115 @@ class CommandWindow: first_radio = None for (commandnode, commandname) in self.commandlist: radio = gtk.RadioButton(first_radio, label=commandname) - if first_radio is None: first_radio = radio + radio.connect("toggled", self.on_command_radiobutton_toggled, commandnode) + if first_radio is None: + first_radio = radio + self.commandnode = commandnode self.command_list_vbox.pack_end(radio, expand=False) self.command_list_vbox.show_all() self.stage_finish = self.stage2_finish - self.on_cancel_button_clicked = self.stage2_on_cancel_button_clicked - self.on_forward_button_clicked = self.stage2_on_forward_button_clicked + self.stage_cancel_button_clicked = self.stage2_cancel_button_clicked + self.stage_forward_button_clicked = self.stage2_forward_button_clicked + self.stage_adhoc_commands_window_destroy = self.do_nothing + self.stage_adhoc_commands_window_delete_event = self.do_nothing def stage2_finish(self): - '''Remove widgets we created.''' + '''Remove widgets we created. Not needed when the window is destroyed.''' def remove_widget(widget): self.command_list_vbox.remove(widget) self.command_list_vbox.foreach(remove_widget) - def stage2_on_cancel_button_clicked(self): + def stage2_cancel_button_clicked(self, widget): self.stage_finish() self.window.destroy() - def stage2_on_forward_button_clicked(self): - pass + def stage2_forward_button_clicked(self, widget): + self.stage3() + + def on_command_radiobutton_toggled(self, widget, commandnode): + self.commandnode = commandnode def on_check_commands_1_button_clicked(self, widget): self.stage1() +# stage 3: command invocation + def stage3(self): + # close old stage + self.stage_finish() + + assert isinstance(self.commandnode, unicode) + + self.stages_notebook.set_current_page( + self.stages_notebook.page_num( + self.sending_form_stage_vbox)) + + self.cancel_button.set_sensitive(True) + self.back_button.set_sensitive(False) + self.forward_button.set_sensitive(False) + self.execute_button.set_sensitive(False) + + self.stage3_submit_form() + + self.stage_finish = self.stage3_finish + self.stage_cancel_button_clicked = self.stage3_cancel_button_clicked + self.stage_back_button_clicked = self.stage3_back_button_clicked + self.stage_forward_button_clicked = self.stage3_forward_button_clicked + self.stage_execute_button_clicked = self.stage3_execute_button_clicked + self.stage_adhoc_commands_window_destroy = self.do_nothing + self.stage_adhoc_commands_window_delete_event = self.do_nothing + + def stage3_finish(self): + pass + + def stage3_cancel_button_clicked(self, widget): + pass + + def stage3_back_button_clicked(self, widget): + self.stage3_submit_form('prev') + + def stage3_forward_button_clicked(self, widget): + self.stage3_submit_form('next') + + def stage3_execute_button_clicked(self, widget): + self.stage3_submit_form('execute') + + def stage3_submit_form(self, action='execute'): + self.data_form_widget.set_sensitive(False) + if self.data_form_widget.get_data_form() is None: + self.data_form_widget.hide() + self.sending_form_progressbar.show() + self.setup_pulsing(self.sending_form_progressbar) + self.send_command(action) + + def stage3_next_form(self, command): + assert isinstance(command, xmpp.Node) + + self.remove_pulsing() + self.sending_form_progressbar.hide() + + self.sessionid = command.getAttr('sessionid') + + self.dataform = xmpp.DataForm(node=command.getTag('x')) + + self.data_form_widget.show() + self.data_form_widget.set_sensitive(True) + self.data_form_widget.set_data_form(self.dataform) + + action = command.getTag('action') + if action is None: + # no action tag? that's last stage... + self.cancel_button.set_sensitive(False) + self.back_button.set_sensitive(False) + self.forward_button.set_sensitive(False) + self.execute_button.set_sensitive(True) + else: + # actions, actions, actions... + self.cancel_button.set_sensitive(False) + self.back_button.set_sensitive(action.getTag('prev') is not None) + self.forward_button.set_sensitive(action.getTag('next') is not None) + self.execute_button.set_sensitive(True) + # stage 4: no commands are exposed def stage4(self): '''Display the message. Wait for user to close the window''' @@ -180,9 +302,11 @@ class CommandWindow: self.execute_button.set_sensitive(False) self.stage_finish = self.do_nothing - self.on_cancel_button_clicked = self.stage4_on_cancel_button_clicked + self.stage_cancel_button_clicked = self.stage4_cancel_button_clicked + self.stage_adhoc_commands_window_destroy = self.do_nothing + self.stage_adhoc_commands_window_delete_event = self.do_nothing - def stage4_on_cancel_button_clicked(self): + def stage4_cancel_button_clicked(self, widget): self.window.destroy() def on_check_commands_2_button_clicked(self, widget): @@ -208,9 +332,11 @@ class CommandWindow: self.error_description_label.set_text(error) self.stage_finish = self.do_nothing - self.on_cancel_button_clicked = self.stage5_on_cancel_button_clicked + self.stage_cancel_button_clicked = self.stage5_cancel_button_clicked + self.stage_adhoc_commands_window_destroy = self.do_nothing + self.stage_adhoc_commands_window_delete_event = self.do_nothing - def stage5_on_cancel_button_clicked(self): + def stage5_cancel_button_clicked(self, widget): self.window.destroy() # helpers to handle pulsing in progressbar @@ -261,3 +387,30 @@ class CommandWindow: self.stage2() self.account.connection.SendAndCallForResponse(query, callback) + + def send_command(self, action='execute'): + '''Send the command with data form. Wait for reply.''' + # create the stanza + assert isinstance(self.commandnode, unicode) + assert action in ('execute', 'prev', 'next', 'complete') + + stanza = xmpp.Iq(typ='set', to=self.jid) + cmdnode = stanza.addChild('command', attrs={ + 'xmlns':xmpp.NS_COMMANDS, + 'node':self.commandnode, + 'action':action + }) + + if self.sessionid is not None: + cmdnode.setAttr('sessionid', self.sessionid) + + if self.data_form_widget.data_form is not None: + cmdnode.addChild(node=self.data_form_widget.filled_data_form()) + + def callback(response): + # TODO: error handling + self.stage3_next_form(response.getTag('command')) + + print stanza + + self.account.connection.SendAndCallForResponse(stanza, callback) diff --git a/src/dataforms.py b/src/dataforms.py index 47f9a9669..e10d87dd3 100644 --- a/src/dataforms.py +++ b/src/dataforms.py @@ -17,6 +17,9 @@ """ This module contains widget that can display data form (JEP-0004). """ import gtk +import gtkgui_helpers + +import common.xmpp as xmpp class DataFormWidget(gtk.Alignment): # "public" interface @@ -25,17 +28,21 @@ class DataFormWidget(gtk.Alignment): """ Create a widget. """ gtk.Alignment.__init__(self) + self._data_form = None + self.xml=gtkgui_helpers.get_glade('data_form_window.glade', 'data_form_scrolledwindow') self.instructions = self.xml.get_widget('form_instructions_label') - self.form = self.xml.get_widget('form_vbox') + self.form = self.xml.get_widget('form_table') - self.add(self.xml.get_widget('data_form_scrolledwindow') + self.add(self.xml.get_widget('data_form_scrolledwindow')) - self.set_data_form(dataform) + self.set_data_form(dataformnode) def set_data_form(self, dataform=None): """ Set the data form (xmpp.DataForm) displayed in widget. Set to None to erase the form. """ + assert (isinstance(dataform, xmpp.Node) or (dataform is None)) + if self._data_form is not None: self._cleanWidgets() self._data_form = dataform if self._data_form is not None: self._buildWidgets() @@ -56,6 +63,39 @@ class DataFormWidget(gtk.Alignment): """ Treat 'us' as one widget. """ self.show_all() + def filled_data_form(self): + """ Generates form that contains values filled by user. This + won't be DataForm object, as the DataFields seem to be uncapable + of some things. """ + assert isinstance(self._data_form, xmpp.DataForm) + + form = xmpp.Node('x', {'xmlns':xmpp.NS_DATA, 'type':'submit'}) + for field in self._data_form.kids: + if not isinstance(field, xmpp.DataField): continue + + ftype = field.getType() + if ftype not in ('boolean', 'fixed', 'hidden', 'jid-multi', + 'jid-single', 'list-multi', 'list-single', + 'text-multi', 'text-private', 'text-single'): + ftype = 'text-single' + + if ftype in ('fixed',): + continue + + newfield = xmpp.Node('field', {'var': field.getVar()}) + + if ftype in ('jid-multi', 'list-multi', 'text-multi'): + for value in field.getValues(): + newvalue = xmpp.Node('value', {}, [value]) + newfield.addChild(node=newvalue) + else: + newvalue = xmpp.Node('value', {}, [field.getValue()]) + newfield.addChild(node=newvalue) + + form.addChild(node=newfield) + + return form + data_form = property(get_data_form, set_data_form, None, "Data form presented in a widget") title = property(get_title, None, None, "Data form title") @@ -64,99 +104,131 @@ class DataFormWidget(gtk.Alignment): """ Create all sub-widgets according to self._data_form and JEP-0004. """ assert self._data_form is not None - assert length(self.form.get_children())==0 + assert len(self.form.get_children())==0 # it is *very* often used here df = self._data_form - if df.has_key('instructions'): - self.instructions.set_text(df['instructions']) + instructions = df.getInstructions() + if instructions is not None: + self.instructions.set_text(instructions) - i = -1 - while df.has_key(i+1): - i += 1 - if not df[i].has_key['type']: - continue - - ctype = df[i]['type'] - if ctype = 'hidden': - continue + linecounter = 0 - hbox = gtk.HBox(spacing = 5) - label = gtk.Label('') - label.set_line_wrap(True) - label.set_alignment(0.0, 0.5) - label.set_property('width_request', 150) - hbox.pack_start(label, False) - if df[i].has_key('label'): - label.set_text(df[i]['label']) - if ctype == 'boolean': - desc = None - if df[i].has_key('desc'): - desc = df[i]['desc'] - widget = gtk.CheckButton(desc, False) - activ = False - if df[i].has_key('values'): - activ = df[i]['values'][0] - widget.set_active(activ) - widget.connect('toggled', self.on_checkbutton_toggled, i) - elif ctype == 'fixed': - widget = gtk.Label('\n'.join(df[i]['values'])) + for field in df.kids: + if not isinstance(field, xmpp.DataField): continue + + # TODO: rewrite that when xmpp.DataField will be rewritten + ftype = field.getType() + if ftype not in ('boolean', 'fixed', 'hidden', 'jid-multi', + 'jid-single', 'list-multi', 'list-single', + 'text-multi', 'text-private', 'text-single'): + ftype = 'text-single' + + if ftype == 'hidden': continue + + # field label + flabel = field.getAttr('label') + if flabel is None: + flabel = field.getVar() + + # field description + fdesc = field.getDesc() + + # field value (if one) + fvalue = field.getValue() + + # field values (if one or more) + fvalues = field.getValues() + + # field options + foptions = field.getOptions() + + commonlabel = True + commondesc = True + commonwidget = True + + if ftype == 'boolean': + widget = gtk.CheckButton() + widget.connect('toggled', self.on_boolean_checkbutton_toggled, field) + if fvalue in ('1', 'true'): + widget.set_active(True) + else: + field.setValue('0') + + elif ftype == 'fixed': + leftattach = 1 + rightattach = 2 + if flabel is None: + commonlabel = False + leftattach = 0 + if fdesc is None: + commondesc = False + rightattach = 3 + commonwidget = False + widget = gtk.Label(fvalue) widget.set_line_wrap(True) - widget.set_alignment(0.0, 0.5) - elif ctype == 'jid-multi': - #FIXME - widget = gtk.Label('') - elif ctype == 'jid-single': - #FIXME - widget = gtk.Label('') - elif ctype == 'list-multi': - j = 0 - widget = gtk.Table(1, 1) - while df[i]['options'].has_key(j): - widget.resize(j + 1, 1) - child = gtk.CheckButton(df[i]['options'][j]['label'], - False) - if df[i]['options'][j]['values'][0] in \ - df[i]['values']: - child.set_active(True) - child.connect('toggled', self.on_checkbutton_toggled2, i, j) - widget.attach(child, 0, 1, j, j+1) - j += 1 - elif ctype == 'list-single': - widget = gtk.combo_box_new_text() - widget.connect('changed', self.on_combobox_changed, i) - index = 0 - j = 0 - while df[i]['options'].has_key(j): - if df[i]['options'][j]['values'][0] == \ - df[i]['values'][0]: - index = j - widget.append_text(df[i]['options'][j]['label']) - j += 1 - widget.set_active(index) - elif ctype == 'text-multi': - widget = gtk.TextView() - widget.set_size_request(100, -1) - widget.get_buffer().connect('changed', self.on_textbuffer_changed, \ - i) - widget.get_buffer().set_text('\n'.join(df[i]['values'])) - elif ctype == 'text-private': + self.form.attach(widget, leftattach, rightattach, linecounter, linecounter+1) + + elif ftype == 'jid-multi': + widget = gtk.Label('jid-multi field') + + elif ftype == 'jid-single': + widget = gtk.Label('jid-single field') + + elif ftype == 'list-multi': + widget = gtk.Label('list-multi field') + + elif ftype == 'list-single': + # TODO: When more than few choices, make a list + widget = gtk.VBox() + first_radio = None + right_value = False + for label, value in foptions: + radio = gtk.RadioButton(first_radio, label=label) + radio.connect('toggled', self.on_list_single_radiobutton_toggled, + field, value) + if first_radio is None: + first_radio = radio + first_value = value + if value == fvalue: + right_value = True + widget.pack_end(radio, expand=False) + if not right_value: + field.setValue(first_value) + + elif ftype == 'text-multi': + widget = gtk.Label('text-multi field') + + elif ftype == 'text-private': + widget = gtk.Label('text-private field') + + elif ftype == 'text-single': widget = gtk.Entry() - widget.connect('changed', self.on_entry_changed, i) - if not df[i].has_key('values'): - df[i]['values'] = [''] - widget.set_text(df[i]['values'][0]) - widget.set_visibility(False) - elif ctype == 'text-single': - widget = gtk.Entry() - widget.connect('changed', self.on_entry_changed, i) - if not df[i].has_key('values'): - df[i]['values'] = [''] - widget.set_text(df[i]['values'][0]) - hbox.pack_start(widget, False) - hbox.pack_start(gtk.Label('')) # So that widhet doesn't take all space - self.form.pack_start(hbox, False) + widget.connect('changed', self.on_text_single_entry_changed, field) + if fvalue is None: + field.setValue('') + fvalue = '' + widget.set_text(fvalue) + + else: + widget = gtk.Label('Unhandled widget type!') + + if commonlabel and flabel is not None: + label = gtk.Label(flabel) + label.set_justify(gtk.JUSTIFY_RIGHT) + self.form.attach(label, 0, 1, linecounter, linecounter+1) + + if commonwidget: + self.form.attach(widget, 1, 2, linecounter, linecounter+1) + + if commondesc and fdesc is not None: + label = gtk.Label(fdesc) + label.set_line_wrap(True) + self.form.attach(label, 2, 3, linecounter, linecounter+1) + + linecounter += 1 + self.form.show_all() def _cleanWidgets(self): @@ -165,24 +237,17 @@ class DataFormWidget(gtk.Alignment): self.form.remove(widget) self.form.foreach(remove) + self.instructions.set_text(u"") - def on_checkbutton_toggled(self, widget, index): - self.config[index]['values'][0] = widget.get_active() + def on_boolean_checkbutton_toggled(self, widget, field): + if widget.get_active(): + field.setValue('true') + else: + field.setValue('false') - def on_checkbutton_toggled2(self, widget, index1, index2): - val = self._data_form[index1]['options'][index2]['values'][0] - if widget.get_active() and val not in self._data_form[index1]['values']: - self._data_form[index1]['values'].append(val) - elif not widget.get_active() and val in self._data_form[index1]['values']: - self._data_form[index1]['values'].remove(val) + def on_list_single_radiobutton_toggled(self, widget, field, value): + field.setValue(value) - def on_combobox_changed(self, widget, index): - self._data_form[index]['values'][0] = self.config[index]['options'][ \ - widget.get_active()]['values'][0] - - def on_entry_changed(self, widget, index): - self._data_form[index]['values'][0] = widget.get_text().decode('utf-8') - - def on_textbuffer_changed(self, widget, index): - begin, end = widget.get_bounds() - self._data_form[index]['values'][0] = widget.get_text(begin, end) + def on_text_single_entry_changed(self, widget, field): + # TODO: check for encoding? + field.setValue(widget.get_text())