root/branches/gajim_0.11/src/adhoc_commands.py

Revision 7888, 16.1 kB (checked in by asterix, 19 months ago)

merge fixes from trunk

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