root/trunk/src/adhoc_commands.py

Revision 10430, 16.7 kB (checked in by asterix, 2 months ago)

fix command requesting XML

Line 
1# -*- coding: utf-8 -*-
2## src/adhoc_commands.py
3##
4## Copyright (C) 2006 Nikos Kouremenos <kourem AT gmail.com>
5## Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
6## Copyright (C) 2006-2008 Yann Leboulanger <asterix AT lagaule.org>
7## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
8##                    Stephan Erb <steve-e AT h3c.de>
9##
10## This file is part of Gajim.
11##
12## Gajim is free software; you can redistribute it and/or modify
13## it under the terms of the GNU General Public License as published
14## by the Free Software Foundation; version 3 only.
15##
16## Gajim is distributed in the hope that it will be useful,
17## but WITHOUT ANY WARRANTY; without even the implied warranty of
18## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19## GNU General Public License for more details.
20##
21## You should have received a copy of the GNU General Public License
22## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
23##
24
25# FIXME: think if we need caching command list. it may be wrong if there will
26# be entities that often change the list, it may be slow to fetch it every time
27
28import gobject
29import gtk
30
31from common import xmpp, gajim, dataforms
32
33import gtkgui_helpers
34import dialogs
35import dataforms_widget
36
37class CommandWindow:
38        '''Class for a window for single ad-hoc commands session. Note, that
39        there might be more than one for one account/jid pair in one moment.
40
41        TODO: maybe put this window into MessageWindow? consider this when
42        TODO: it will be possible to manage more than one window of one
43        TODO: account/jid pair in MessageWindowMgr.
44
45        TODO: gtk 2.10 has a special wizard-widget, consider using it...'''
46
47        def __init__(self, account, jid):
48                '''Create new window.'''
49
50                # an account object
51                self.account = gajim.connections[account]
52                self.jid = jid
53
54                self.pulse_id=None      # to satisfy self.setup_pulsing()
55                self.commandlist=None   # a list of (commandname, commanddescription)
56
57                # command's data
58                self.commandnode = None
59                self.sessionid = None
60                self.dataform = None
61                self.allow_stage3_close = False
62
63                # retrieving widgets from xml
64                self.xml = gtkgui_helpers.get_glade('adhoc_commands_window.glade')
65                self.window = self.xml.get_widget('adhoc_commands_window')
66                self.window.connect('delete-event', self.on_adhoc_commands_window_delete_event)
67                for name in ('back_button', 'forward_button',
68                        'execute_button','close_button','stages_notebook',
69                        'retrieving_commands_stage_vbox',
70                        'command_list_stage_vbox','command_list_vbox',
71                        'sending_form_stage_vbox','sending_form_progressbar',
72                        'notes_label','no_commands_stage_vbox','error_stage_vbox',
73                        'error_description_label'):
74                        self.__dict__[name] = self.xml.get_widget(name)
75
76                # creating data forms widget
77                self.data_form_widget = dataforms_widget.DataFormWidget()
78                self.data_form_widget.show()
79                self.sending_form_stage_vbox.pack_start(self.data_form_widget)
80
81                # setting initial stage
82                self.stage1()
83
84                # displaying the window
85                self.xml.signal_autoconnect(self)
86                self.window.show_all()
87
88# these functions are set up by appropriate stageX methods
89        def stage_finish(self, *anything): pass
90        def stage_back_button_clicked(self, *anything): assert False
91        def stage_forward_button_clicked(self, *anything): assert False
92        def stage_execute_button_clicked(self, *anything): assert False
93        def stage_close_button_clicked(self, *anything): assert False
94        def stage_adhoc_commands_window_delete_event(self, *anything): assert False
95        def do_nothing(self, *anything): return False
96
97# widget callbacks
98        def on_back_button_clicked(self, *anything):
99                return self.stage_back_button_clicked(*anything)
100
101        def on_forward_button_clicked(self, *anything):
102                return self.stage_forward_button_clicked(*anything)
103
104        def on_execute_button_clicked(self, *anything):
105                return self.stage_execute_button_clicked(*anything)
106
107        def on_close_button_clicked(self, *anything):
108                return self.stage_close_button_clicked(*anything)
109
110        def on_adhoc_commands_window_destroy(self, *anything):
111                # TODO: do all actions that are needed to remove this object from memory...
112                self.remove_pulsing()
113
114        def on_adhoc_commands_window_delete_event(self, *anything):
115                return self.stage_adhoc_commands_window_delete_event(self.window)
116
117        def __del__(self):
118                print 'Object has been deleted.'
119
120# stage 1: waiting for command list
121        def stage1(self):
122                '''Prepare the first stage. Request command list,
123                set appropriate state of widgets.'''
124                # close old stage...
125                self.stage_finish()
126
127                # show the stage
128                self.stages_notebook.set_current_page(
129                        self.stages_notebook.page_num(
130                                self.retrieving_commands_stage_vbox))
131
132                # set widgets' state
133                self.close_button.set_sensitive(True)
134                self.back_button.set_sensitive(False)
135                self.forward_button.set_sensitive(False)
136                self.execute_button.set_sensitive(False)
137
138                # request command list
139                self.request_command_list()
140                self.setup_pulsing(
141                        self.xml.get_widget('retrieving_commands_progressbar'))
142
143                # setup the callbacks
144                self.stage_finish = self.stage1_finish
145                self.stage_close_button_clicked = self.stage1_close_button_clicked
146                self.stage_adhoc_commands_window_delete_event = self.stage1_adhoc_commands_window_delete_event
147
148        def stage1_finish(self):
149                self.remove_pulsing()
150
151        def stage1_close_button_clicked(self, widget):
152                # cancelling in this stage is not critical, so we don't
153                # show any popups to user
154                self.stage1_finish()
155                self.window.destroy()
156
157        def stage1_adhoc_commands_window_delete_event(self, widget):
158                self.stage1_finish()
159                return True
160
161# stage 2: choosing the command to execute
162        def stage2(self):
163                '''Populate the command list vbox with radiobuttons
164                (FIXME: if there is more commands, maybe some kind of list?),
165                set widgets' state.'''
166                # close old stage
167                self.stage_finish()
168
169                assert len(self.commandlist)>0
170
171                self.stages_notebook.set_current_page(
172                        self.stages_notebook.page_num(
173                                self.command_list_stage_vbox))
174
175                self.close_button.set_sensitive(True)
176                self.back_button.set_sensitive(False)
177                self.forward_button.set_sensitive(True)
178                self.execute_button.set_sensitive(False)
179
180                # build the commands list radiobuttons
181                first_radio = None
182                for (commandnode, commandname) in self.commandlist:
183                        radio = gtk.RadioButton(first_radio, label=commandname)
184                        radio.connect("toggled", self.on_command_radiobutton_toggled, commandnode)
185                        if not first_radio:
186                                first_radio = radio
187                                self.commandnode = commandnode
188                        self.command_list_vbox.pack_start(radio, expand=False)
189                self.command_list_vbox.show_all()
190
191                self.stage_finish = self.stage2_finish
192                self.stage_close_button_clicked = self.stage2_close_button_clicked
193                self.stage_forward_button_clicked = self.stage2_forward_button_clicked
194                self.stage_adhoc_commands_window_delete_event = self.do_nothing
195
196        def stage2_finish(self):
197                '''Remove widgets we created. Not needed when the window is destroyed.'''
198                def remove_widget(widget):
199                        self.command_list_vbox.remove(widget)
200                self.command_list_vbox.foreach(remove_widget)
201
202        def stage2_close_button_clicked(self, widget):
203                self.stage_finish()
204                self.window.destroy()
205
206        def stage2_forward_button_clicked(self, widget):
207                self.stage3()
208
209        def on_command_radiobutton_toggled(self, widget, commandnode):
210                self.commandnode = commandnode
211
212        def on_check_commands_1_button_clicked(self, widget):
213                self.stage1()
214
215# stage 3: command invocation
216        def stage3(self):
217                # close old stage
218                self.stage_finish()
219
220                assert isinstance(self.commandnode, unicode)
221
222                self.form_status = None
223
224                self.stages_notebook.set_current_page(
225                        self.stages_notebook.page_num(
226                                self.sending_form_stage_vbox))
227
228                self.close_button.set_sensitive(True)
229                self.back_button.set_sensitive(False)
230                self.forward_button.set_sensitive(False)
231                self.execute_button.set_sensitive(False)
232
233                self.stage3_submit_form()
234
235                self.stage_finish = self.stage3_finish
236                self.stage_back_button_clicked = self.stage3_back_button_clicked
237                self.stage_forward_button_clicked = self.stage3_forward_button_clicked
238                self.stage_execute_button_clicked = self.stage3_execute_button_clicked
239                self.stage_close_button_clicked = self.stage3_close_button_clicked
240                self.stage_adhoc_commands_window_delete_event = self.stage3_close_button_clicked
241
242        def stage3_finish(self):
243                pass
244
245        def stage3_close_button_clicked(self, widget):
246                ''' We are in the middle of executing command. Ask user if he really want to cancel
247                the process, then... cancel it. '''
248                # this works also as a handler for window_delete_event, so we have to return appropriate
249                # values
250                if self.form_status == 'completed':
251                        if widget!=self.window:
252                                self.window.destroy()
253                        return False
254
255                if self.allow_stage3_close:
256                        return False
257
258                def on_yes(button):
259                        self.send_cancel()
260                        self.allow_stage3_close = True
261                        self.window.destroy()
262
263                dialog = dialogs.HigDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT | \
264                        gtk.DIALOG_MODAL, gtk.BUTTONS_YES_NO, _('Cancel confirmation'),
265                        _('You are in process of executing command. Do you really want to '
266                        'cancel it?'), on_response_yes=on_yes)
267                dialog.popup()
268                return True # Block event, don't close window
269
270        def stage3_back_button_clicked(self, widget):
271                self.stage3_submit_form('prev')
272
273        def stage3_forward_button_clicked(self, widget):
274                self.stage3_submit_form('next')
275
276        def stage3_execute_button_clicked(self, widget):
277                self.stage3_submit_form('execute')
278
279        def stage3_submit_form(self, action='execute'):
280                self.data_form_widget.set_sensitive(False)
281                if self.data_form_widget.get_data_form():
282                        self.data_form_widget.data_form.type='submit'
283                else:
284                        self.data_form_widget.hide()
285
286                self.close_button.set_sensitive(True)
287                self.back_button.set_sensitive(False)
288                self.forward_button.set_sensitive(False)
289                self.execute_button.set_sensitive(False)
290
291                self.sending_form_progressbar.show()
292                self.setup_pulsing(self.sending_form_progressbar)
293                self.send_command(action)
294
295        def stage3_next_form(self, command):
296                assert isinstance(command, xmpp.Node)
297
298                self.remove_pulsing()
299                self.sending_form_progressbar.hide()
300
301                if not self.sessionid:
302                        self.sessionid = command.getAttr('sessionid')
303                elif self.sessionid != command.getAttr('sessionid'):
304                        self.stage5(error=_('Service changed the session identifier.'),
305                                senderror=True)
306
307                self.form_status = command.getAttr('status')
308
309                self.commandnode = command.getAttr('node')
310                if command.getTag('x'):
311                        self.dataform = dataforms.ExtendForm(node=command.getTag('x'))
312
313                        self.data_form_widget.set_sensitive(True)
314                        try:
315                                self.data_form_widget.data_form=self.dataform
316                        except dataforms.Error:
317                                self.stage5(error=_('Service sent malformed data'), senderror=True)
318                                return
319                        self.data_form_widget.show()
320                        if self.data_form_widget.title:
321                                self.window.set_title('%s - Ad-hoc Commands - Gajim' % \
322                                        self.data_form_widget.title)
323                else:
324                        self.data_form_widget.hide()
325
326                actions = command.getTag('actions')
327                if actions:
328                        # actions, actions, actions...
329                        self.close_button.set_sensitive(True)
330                        self.back_button.set_sensitive(actions.getTag('prev') is not None)
331                        self.forward_button.set_sensitive(actions.getTag('next') is not None)
332                        self.execute_button.set_sensitive(True)
333                else:
334                        self.close_button.set_sensitive(True)
335                        self.back_button.set_sensitive(False)
336                        self.forward_button.set_sensitive(False)
337                        self.execute_button.set_sensitive(True)
338
339                if self.form_status == 'completed':
340                        self.close_button.set_sensitive(True)
341                        self.back_button.hide()
342                        self.forward_button.hide()
343                        self.execute_button.hide()
344                        self.close_button.show()
345                        self.stage_adhoc_commands_window_delete_event = self.stage3_close_button_clicked
346
347                note = command.getTag('note')
348                if note:
349                        self.notes_label.set_text(note.getData().decode('utf-8'))
350                        self.notes_label.set_no_show_all(False)
351                        self.notes_label.show()
352                else:
353                        self.notes_label.set_no_show_all(True)
354                        self.notes_label.hide()
355
356# stage 4: no commands are exposed
357        def stage4(self):
358                '''Display the message. Wait for user to close the window'''
359                # close old stage
360                self.stage_finish()
361
362                self.stages_notebook.set_current_page(
363                        self.stages_notebook.page_num(
364                                self.no_commands_stage_vbox))
365
366                self.close_button.set_sensitive(True)
367                self.back_button.set_sensitive(False)
368                self.forward_button.set_sensitive(False)
369                self.execute_button.set_sensitive(False)
370
371                self.stage_finish = self.do_nothing
372                self.stage_close_button_clicked = self.stage4_close_button_clicked
373                self.stage_adhoc_commands_window_delete_event = self.do_nothing
374
375        def stage4_close_button_clicked(self, widget):
376                self.window.destroy()
377
378        def on_check_commands_2_button_clicked(self, widget):
379                self.stage1()
380
381# stage 5: an error has occured
382        def stage5(self, error=None, errorid=None, senderror=False):
383                '''Display the error message. Wait for user to close the window'''
384                # FIXME: sending error to responder
385                # close old stage
386                self.stage_finish()
387
388                assert errorid or error
389
390                if errorid:
391                        # we've got error code, display appropriate message
392                        try:
393                                errorname = xmpp.NS_STANZAS + ' ' + str(errorid)
394                                errordesc = xmpp.ERRORS[errorname][2]
395                                error = errordesc.decode('utf-8')
396                                del errorname, errordesc
397                        except KeyError:        # when stanza doesn't have error description
398                                error = _('Service returned an error.')
399                elif error:
400                        # we've got error message
401                        pass
402                else:
403                        # we don't know what's that, bailing out
404                        assert False
405
406                self.stages_notebook.set_current_page(
407                        self.stages_notebook.page_num(
408                                self.error_stage_vbox))
409
410                self.close_button.set_sensitive(True)
411                self.back_button.hide()
412                self.forward_button.hide()
413                self.execute_button.hide()
414
415                self.error_description_label.set_text(error)
416
417                self.stage_finish = self.do_nothing
418                self.stage_close_button_clicked = self.stage5_close_button_clicked
419                self.stage_adhoc_commands_window_delete_event = self.do_nothing
420
421        def stage5_close_button_clicked(self, widget):
422                self.window.destroy()
423
424# helpers to handle pulsing in progressbar
425        def setup_pulsing(self, progressbar):
426                '''Set the progressbar to pulse. Makes a custom
427                function to repeatedly call progressbar.pulse() method.'''
428                assert not self.pulse_id
429                assert isinstance(progressbar, gtk.ProgressBar)
430
431                def callback():
432                        progressbar.pulse()
433                        return True     # important to keep callback be called back!
434
435                # 12 times per second (80 miliseconds)
436                self.pulse_id = gobject.timeout_add(80, callback)
437
438        def remove_pulsing(self):
439                '''Stop pulsing, useful when especially when removing widget.'''
440                if self.pulse_id:
441                        gobject.source_remove(self.pulse_id)
442                self.pulse_id=None
443
444# handling xml stanzas
445        def request_command_list(self):
446                '''Request the command list. Change stage on delivery.'''
447                query = xmpp.Iq(typ='get', to=xmpp.JID(self.jid), queryNS=xmpp.NS_DISCO_ITEMS)
448                query.setQuerynode(xmpp.NS_COMMANDS)
449
450                def callback(response):
451                        '''Called on response to query.'''
452                        # FIXME: move to connection_handlers.py
453                        # is error => error stage
454                        error = response.getError()
455                        if error:
456                                # extracting error description from xmpp/protocol.py
457                                self.stage5(errorid = error)
458                                return
459
460                        # no commands => no commands stage
461                        # commands => command selection stage
462                        query = response.getTag('query')
463                        if query and query.getAttr('node') == xmpp.NS_COMMANDS:
464                                items = query.getTags('item')
465                        else:
466                                items = []
467                        if len(items)==0:
468                                self.commandlist = []
469                                self.stage4()
470                        else:
471                                self.commandlist = [(t.getAttr('node'), t.getAttr('name')) for t in items]
472                                self.stage2()
473
474                self.account.connection.SendAndCallForResponse(query, callback)
475
476        def send_command(self, action='execute'):
477                '''Send the command with data form. Wait for reply.'''
478                # create the stanza
479                assert isinstance(self.commandnode, unicode)
480                assert action in ('execute', 'prev', 'next', 'complete')
481
482                stanza = xmpp.Iq(typ='set', to=self.jid)
483                cmdnode = stanza.addChild('command', attrs={
484                                'xmlns':xmpp.NS_COMMANDS,
485                                'node':self.commandnode,
486                                'action':action
487                        })
488
489                if self.sessionid:
490                        cmdnode.setAttr('sessionid', self.sessionid)
491
492                if self.data_form_widget.data_form:
493#                       cmdnode.addChild(node=dataforms.DataForm(tofill=self.data_form_widget.data_form))
494                        # FIXME: simplified form to send
495                       
496                        cmdnode.addChild(node=self.data_form_widget.data_form)
497
498                def callback(response):
499                        # FIXME: move to connection_handlers.py
500                        err = response.getError()
501                        if err:
502                                self.stage5(errorid = err)
503                        else:
504                                self.stage3_next_form(response.getTag('command'))
505
506                self.account.connection.SendAndCallForResponse(stanza, callback)
507
508        def send_cancel(self):
509                '''Send the command with action='cancel'. '''
510                assert self.commandnode
511                if self.sessionid and self.account.connection:
512                        # we already have sessionid, so the service sent at least one reply.
513                        stanza = xmpp.Iq(typ='set', to=self.jid)
514                        stanza.addChild('command', attrs={
515                                        'xmlns':xmpp.NS_COMMANDS,
516                                        'node':self.commandnode,
517                                        'sessionid':self.sessionid,
518                                        'action':'cancel'
519                                })
520
521                        self.account.connection.send(stanza)
522                else:
523                        # we did not received any reply from service; FIXME: we should wait and
524                        # then send cancel; for now we do nothing
525                        pass
526
527# vim: se ts=3:
Note: See TracBrowser for help on using the browser.