1 # -*- coding: utf-8 -*-
2 ## src/adhoc_commands.py
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-2010 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>
10 ## This file is part of Gajim.
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.
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.
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/>.
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
31 from common
import xmpp
, gajim
, dataforms
35 import dataforms_widget
39 Class for a window for single ad-hoc commands session
41 Note, that there might be more than one for one account/jid pair in one
44 TODO: Maybe put this window into MessageWindow? consider this when it will
45 be possible to manage more than one window of one.
46 TODO: Account/jid pair in MessageWindowMgr.
47 TODO: GTK 2.10 has a special wizard-widget, consider using it...
50 def __init__(self
, account
, jid
, commandnode
=None):
56 self
.account
= gajim
.connections
[account
]
58 self
.commandnode
= commandnode
59 self
.data_form_widget
= None
61 # retrieving widgets from xml
62 self
.xml
= gtkgui_helpers
.get_gtk_builder('adhoc_commands_window.ui')
63 self
.window
= self
.xml
.get_object('adhoc_commands_window')
64 self
.window
.connect('delete-event',
65 self
.on_adhoc_commands_window_delete_event
)
66 for name
in ('restart_button', 'back_button', 'forward_button',
67 'execute_button', 'finish_button', 'close_button', 'stages_notebook',
68 'retrieving_commands_stage_vbox', 'command_list_stage_vbox',
69 'command_list_vbox', 'sending_form_stage_vbox',
70 'sending_form_progressbar', 'notes_label', 'no_commands_stage_vbox',
71 'error_stage_vbox', 'error_description_label'):
72 setattr(self
, name
, self
.xml
.get_object(name
))
78 self
.pulse_id
= None # to satisfy self.setup_pulsing()
79 self
.commandlist
= None # a list of (commandname, commanddescription)
84 self
.allow_stage3_close
= False
86 # creating data forms widget
87 if self
.data_form_widget
:
88 self
.sending_form_stage_vbox
.remove(self
.data_form_widget
)
89 self
.data_form_widget
.destroy()
90 self
.data_form_widget
= dataforms_widget
.DataFormWidget()
91 self
.data_form_widget
.show()
92 self
.sending_form_stage_vbox
.pack_start(self
.data_form_widget
)
98 # setting initial stage
101 # displaying the window
102 self
.window
.set_title('Ad-hoc Commands - Gajim')
103 self
.xml
.connect_signals(self
)
104 self
.window
.show_all()
106 self
.restart_button
.set_sensitive(False)
108 # These functions are set up by appropriate stageX methods.
109 def stage_finish(self
, *anything
):
112 def stage_back_button_clicked(self
, *anything
):
115 def stage_forward_button_clicked(self
, *anything
):
118 def stage_execute_button_clicked(self
, *anything
):
121 def stage_close_button_clicked(self
, *anything
):
124 def stage_restart_button_clicked(self
, *anything
):
127 def stage_adhoc_commands_window_delete_event(self
, *anything
):
130 def do_nothing(self
, *anything
):
133 # Widget callbacks...
134 def on_back_button_clicked(self
, *anything
):
135 return self
.stage_back_button_clicked(*anything
)
137 def on_forward_button_clicked(self
, *anything
):
138 return self
.stage_forward_button_clicked(*anything
)
140 def on_execute_button_clicked(self
, *anything
):
141 return self
.stage_execute_button_clicked(*anything
)
143 def on_finish_button_clicked(self
, *anything
):
144 return self
.stage_finish_button_clicked(*anything
)
146 def on_close_button_clicked(self
, *anything
):
147 return self
.stage_close_button_clicked(*anything
)
149 def on_restart_button_clicked(self
, *anything
):
150 return self
.stage_restart_button_clicked(*anything
)
152 def on_adhoc_commands_window_destroy(self
, *anything
):
153 # TODO: do all actions that are needed to remove this object from memory
154 self
.remove_pulsing()
156 def on_adhoc_commands_window_delete_event(self
, *anything
):
157 return self
.stage_adhoc_commands_window_delete_event(self
.window
)
160 print 'Object has been deleted.'
162 # stage 1: waiting for command list
165 Prepare the first stage. Request command list, set appropriate state of
172 self
.stages_notebook
.set_current_page(
173 self
.stages_notebook
.page_num(
174 self
.retrieving_commands_stage_vbox
))
177 self
.close_button
.set_sensitive(True)
178 self
.back_button
.set_sensitive(False)
179 self
.forward_button
.set_sensitive(False)
180 self
.execute_button
.set_sensitive(False)
181 self
.finish_button
.set_sensitive(False)
183 # request command list
184 self
.request_command_list()
186 self
.xml
.get_object('retrieving_commands_progressbar'))
188 # setup the callbacks
189 self
.stage_finish
= self
.stage1_finish
190 self
.stage_close_button_clicked
= self
.stage1_close_button_clicked
191 self
.stage_restart_button_clicked
= self
.stage1_restart_button_clicked
192 self
.stage_adhoc_commands_window_delete_event
= \
193 self
.stage1_adhoc_commands_window_delete_event
195 def stage1_finish(self
):
196 self
.remove_pulsing()
198 def stage1_close_button_clicked(self
, widget
):
199 # cancelling in this stage is not critical, so we don't
200 # show any popups to user
202 self
.window
.destroy()
204 def stage1_restart_button_clicked(self
, widget
):
208 def stage1_adhoc_commands_window_delete_event(self
, widget
):
212 # stage 2: choosing the command to execute
215 Populate the command list vbox with radiobuttons
217 FIXME: If there is more commands, maybe some kind of list, set widgets
223 assert len(self
.commandlist
)>0
225 self
.stages_notebook
.set_current_page(
226 self
.stages_notebook
.page_num(self
.command_list_stage_vbox
))
228 self
.close_button
.set_sensitive(True)
229 self
.back_button
.set_sensitive(False)
230 self
.forward_button
.set_sensitive(True)
231 self
.execute_button
.set_sensitive(False)
232 self
.finish_button
.set_sensitive(False)
234 # build the commands list radiobuttons
236 for (commandnode
, commandname
) in self
.commandlist
:
237 radio
= gtk
.RadioButton(first_radio
, label
=commandname
)
238 radio
.connect("toggled", self
.on_command_radiobutton_toggled
,
242 self
.commandnode
= commandnode
243 self
.command_list_vbox
.pack_start(radio
, expand
=False)
244 self
.command_list_vbox
.show_all()
246 self
.stage_finish
= self
.stage2_finish
247 self
.stage_close_button_clicked
= self
.stage2_close_button_clicked
248 self
.stage_restart_button_clicked
= self
.stage2_restart_button_clicked
249 self
.stage_forward_button_clicked
= self
.stage2_forward_button_clicked
250 self
.stage_adhoc_commands_window_delete_event
= self
.do_nothing
252 def stage2_finish(self
):
254 Remove widgets we created. Not needed when the window is destroyed
256 def remove_widget(widget
):
257 self
.command_list_vbox
.remove(widget
)
258 self
.command_list_vbox
.foreach(remove_widget
)
260 def stage2_close_button_clicked(self
, widget
):
262 self
.window
.destroy()
264 def stage2_restart_button_clicked(self
, widget
):
268 def stage2_forward_button_clicked(self
, widget
):
271 def on_command_radiobutton_toggled(self
, widget
, commandnode
):
272 self
.commandnode
= commandnode
274 def on_check_commands_1_button_clicked(self
, widget
):
277 # stage 3: command invocation
282 assert isinstance(self
.commandnode
, unicode)
284 self
.form_status
= None
286 self
.stages_notebook
.set_current_page(
287 self
.stages_notebook
.page_num(
288 self
.sending_form_stage_vbox
))
290 self
.restart_button
.set_sensitive(True)
291 self
.close_button
.set_sensitive(True)
292 self
.back_button
.set_sensitive(False)
293 self
.forward_button
.set_sensitive(False)
294 self
.execute_button
.set_sensitive(False)
295 self
.finish_button
.set_sensitive(False)
297 self
.stage3_submit_form()
299 self
.stage_finish
= self
.stage3_finish
300 self
.stage_back_button_clicked
= self
.stage3_back_button_clicked
301 self
.stage_forward_button_clicked
= self
.stage3_forward_button_clicked
302 self
.stage_execute_button_clicked
= self
.stage3_execute_button_clicked
303 self
.stage_finish_button_clicked
= self
.stage3_finish_button_clicked
304 self
.stage_close_button_clicked
= self
.stage3_close_button_clicked
305 self
.stage_restart_button_clicked
= self
.stage3_restart_button_clicked
306 self
.stage_adhoc_commands_window_delete_event
= \
307 self
.stage3_close_button_clicked
309 def stage3_finish(self
):
312 def stage3_can_close(self
, cb
):
313 if self
.form_status
== 'completed':
322 dialog
= dialogs
.HigDialog(self
.window
, gtk
.DIALOG_DESTROY_WITH_PARENT \
323 | gtk
.DIALOG_MODAL
, gtk
.BUTTONS_YES_NO
, _('Cancel confirmation'),
324 _('You are in process of executing command. Do you really want to '
325 'cancel it?'), on_response_yes
=on_yes
)
328 def stage3_close_button_clicked(self
, widget
):
330 We are in the middle of executing command. Ask user if he really want to
331 cancel the process, then cancel it
333 # this works also as a handler for window_delete_event, so we have to
334 # return appropriate values
335 if self
.allow_stage3_close
:
339 self
.allow_stage3_close
= True
340 self
.window
.destroy()
342 self
.stage3_can_close(on_ok
)
344 return True # Block event, don't close window
346 def stage3_restart_button_clicked(self
, widget
):
350 self
.stage3_can_close(on_ok
)
352 def stage3_back_button_clicked(self
, widget
):
353 self
.stage3_submit_form('prev')
355 def stage3_forward_button_clicked(self
, widget
):
356 self
.stage3_submit_form('next')
358 def stage3_execute_button_clicked(self
, widget
):
359 self
.stage3_submit_form('execute')
361 def stage3_finish_button_clicked(self
, widget
):
362 self
.stage3_submit_form('complete')
364 def stage3_submit_form(self
, action
='execute'):
365 self
.data_form_widget
.set_sensitive(False)
367 if self
.data_form_widget
.get_data_form():
368 df
= self
.data_form_widget
.get_data_form()
369 if not df
.is_valid():
370 dialogs
.ErrorDialog(_('Invalid Form'),
371 _('The form is not filled correctly.'))
372 self
.data_form_widget
.set_sensitive(True)
374 self
.data_form_widget
.data_form
.type = 'submit'
376 self
.data_form_widget
.hide()
378 self
.close_button
.set_sensitive(True)
379 self
.back_button
.set_sensitive(False)
380 self
.forward_button
.set_sensitive(False)
381 self
.execute_button
.set_sensitive(False)
382 self
.finish_button
.set_sensitive(False)
384 self
.sending_form_progressbar
.show()
385 self
.setup_pulsing(self
.sending_form_progressbar
)
386 self
.send_command(action
)
388 def stage3_next_form(self
, command
):
389 if not isinstance(command
, xmpp
.Node
):
390 self
.stage5(error
=_('Service sent malformed data'), senderror
=True)
393 self
.remove_pulsing()
394 self
.sending_form_progressbar
.hide()
396 if not self
.sessionid
:
397 self
.sessionid
= command
.getAttr('sessionid')
398 elif self
.sessionid
!= command
.getAttr('sessionid'):
399 self
.stage5(error
=_('Service changed the session identifier.'),
403 self
.form_status
= command
.getAttr('status')
405 self
.commandnode
= command
.getAttr('node')
406 if command
.getTag('x'):
407 self
.dataform
= dataforms
.ExtendForm(node
=command
.getTag('x'))
409 self
.data_form_widget
.set_sensitive(True)
411 self
.data_form_widget
.data_form
= self
.dataform
412 except dataforms
.Error
:
413 self
.stage5(error
=_('Service sent malformed data'),
416 self
.data_form_widget
.show()
417 if self
.data_form_widget
.title
:
418 self
.window
.set_title('%s - Ad-hoc Commands - Gajim' % \
419 self
.data_form_widget
.title
)
421 self
.data_form_widget
.hide()
423 actions
= command
.getTag('actions')
425 # actions, actions, actions...
426 self
.close_button
.set_sensitive(True)
427 self
.back_button
.set_sensitive(actions
.getTag('prev') is not None)
428 self
.forward_button
.set_sensitive(
429 actions
.getTag('next') is not None)
430 self
.execute_button
.set_sensitive(True)
431 self
.finish_button
.set_sensitive(actions
.getTag('complete') is not \
434 self
.close_button
.set_sensitive(True)
435 self
.back_button
.set_sensitive(False)
436 self
.forward_button
.set_sensitive(False)
437 self
.execute_button
.set_sensitive(True)
438 self
.finish_button
.set_sensitive(False)
440 if self
.form_status
== 'completed':
441 self
.close_button
.set_sensitive(True)
442 self
.back_button
.hide()
443 self
.forward_button
.hide()
444 self
.execute_button
.hide()
445 self
.finish_button
.hide()
446 self
.close_button
.show()
447 self
.stage_adhoc_commands_window_delete_event
= \
448 self
.stage3_close_button_clicked
450 note
= command
.getTag('note')
452 self
.notes_label
.set_text(note
.getData().decode('utf-8'))
453 self
.notes_label
.set_no_show_all(False)
454 self
.notes_label
.show()
456 self
.notes_label
.set_no_show_all(True)
457 self
.notes_label
.hide()
460 self
.commandnode
= None
463 # stage 4: no commands are exposed
466 Display the message. Wait for user to close the window
471 self
.stages_notebook
.set_current_page(
472 self
.stages_notebook
.page_num(self
.no_commands_stage_vbox
))
474 self
.close_button
.set_sensitive(True)
475 self
.back_button
.set_sensitive(False)
476 self
.forward_button
.set_sensitive(False)
477 self
.execute_button
.set_sensitive(False)
478 self
.finish_button
.set_sensitive(False)
480 self
.stage_finish
= self
.do_nothing
481 self
.stage_close_button_clicked
= self
.stage4_close_button_clicked
482 self
.stage_restart_button_clicked
= self
.stage4_restart_button_clicked
483 self
.stage_adhoc_commands_window_delete_event
= self
.do_nothing
485 def stage4_close_button_clicked(self
, widget
):
486 self
.window
.destroy()
488 def stage4_restart_button_clicked(self
, widget
):
491 def on_check_commands_2_button_clicked(self
, widget
):
494 # stage 5: an error has occured
495 def stage5(self
, error
=None, errorid
=None, senderror
=False):
497 Display the error message. Wait for user to close the window
499 # FIXME: sending error to responder
503 assert errorid
or error
506 # we've got error code, display appropriate message
508 errorname
= xmpp
.NS_STANZAS
+ ' ' + str(errorid
)
509 errordesc
= xmpp
.ERRORS
[errorname
][2]
510 error
= errordesc
.decode('utf-8')
511 del errorname
, errordesc
512 except KeyError: # when stanza doesn't have error description
513 error
= _('Service returned an error.')
515 # we've got error message
518 # we don't know what's that, bailing out
521 self
.stages_notebook
.set_current_page(
522 self
.stages_notebook
.page_num(self
.error_stage_vbox
))
524 self
.close_button
.set_sensitive(True)
525 self
.back_button
.hide()
526 self
.forward_button
.hide()
527 self
.execute_button
.hide()
528 self
.finish_button
.hide()
530 self
.error_description_label
.set_text(error
)
532 self
.stage_finish
= self
.do_nothing
533 self
.stage_close_button_clicked
= self
.stage5_close_button_clicked
534 self
.stage_restart_button_clicked
= self
.stage5_restart_button_clicked
535 self
.stage_adhoc_commands_window_delete_event
= self
.do_nothing
537 def stage5_close_button_clicked(self
, widget
):
538 self
.window
.destroy()
540 def stage5_restart_button_clicked(self
, widget
):
543 # helpers to handle pulsing in progressbar
544 def setup_pulsing(self
, progressbar
):
546 Set the progressbar to pulse. Makes a custom function to repeatedly call
547 progressbar.pulse() method
549 assert not self
.pulse_id
550 assert isinstance(progressbar
, gtk
.ProgressBar
)
554 return True # important to keep callback be called back!
556 # 12 times per second (80 miliseconds)
557 self
.pulse_id
= gobject
.timeout_add(80, callback
)
559 def remove_pulsing(self
):
561 Stop pulsing, useful when especially when removing widget
564 gobject
.source_remove(self
.pulse_id
)
567 # handling xml stanzas
568 def request_command_list(self
):
570 Request the command list. Change stage on delivery
572 query
= xmpp
.Iq(typ
='get', to
=xmpp
.JID(self
.jid
),
573 queryNS
=xmpp
.NS_DISCO_ITEMS
)
574 query
.setQuerynode(xmpp
.NS_COMMANDS
)
576 def callback(response
):
577 '''Called on response to query.'''
578 # FIXME: move to connection_handlers.py
579 # is error => error stage
580 error
= response
.getError()
582 # extracting error description from xmpp/protocol.py
583 self
.stage5(errorid
= error
)
586 # no commands => no commands stage
587 # commands => command selection stage
588 query
= response
.getTag('query')
589 if query
and query
.getAttr('node') == xmpp
.NS_COMMANDS
:
590 items
= query
.getTags('item')
594 self
.commandlist
= []
597 self
.commandlist
= [(t
.getAttr('node'), t
.getAttr('name')) \
601 self
.account
.connection
.SendAndCallForResponse(query
, callback
)
603 def send_command(self
, action
='execute'):
605 Send the command with data form. Wait for reply
608 assert isinstance(self
.commandnode
, unicode)
609 assert action
in ('execute', 'prev', 'next', 'complete')
611 stanza
= xmpp
.Iq(typ
='set', to
=self
.jid
)
612 cmdnode
= stanza
.addChild('command', namespace
=xmpp
.NS_COMMANDS
, attrs
={
613 'node':self
.commandnode
, 'action':action
})
616 cmdnode
.setAttr('sessionid', self
.sessionid
)
618 if self
.data_form_widget
.data_form
:
619 cmdnode
.addChild(node
=self
.data_form_widget
.data_form
.get_purged())
621 def callback(response
):
622 # FIXME: move to connection_handlers.py
623 err
= response
.getError()
625 self
.stage5(errorid
= err
)
627 self
.stage3_next_form(response
.getTag('command'))
629 self
.account
.connection
.SendAndCallForResponse(stanza
, callback
)
631 def send_cancel(self
):
633 Send the command with action='cancel'
635 assert self
.commandnode
636 if self
.sessionid
and self
.account
.connection
:
637 # we already have sessionid, so the service sent at least one reply.
638 stanza
= xmpp
.Iq(typ
='set', to
=self
.jid
)
639 stanza
.addChild('command', namespace
=xmpp
.NS_COMMANDS
, attrs
={
640 'node':self
.commandnode
,
641 'sessionid':self
.sessionid
,
645 self
.account
.connection
.send(stanza
)
647 # we did not received any reply from service;
648 # FIXME: we should wait and then send cancel; for now we do nothing