use our roster filter as soon as we type a letter. Fixes #5221
[gajim.git] / src / adhoc_commands.py
blobb0835b47424d6c0e6b4c83e8e8224b3abd526806
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-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>
9 ##
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
28 import gobject
29 import gtk
31 from common import xmpp, gajim, dataforms
33 import gtkgui_helpers
34 import dialogs
35 import dataforms_widget
37 class CommandWindow:
38 """
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
42 moment.
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...
48 """
50 def __init__(self, account, jid, commandnode=None):
51 """
52 Create new window
53 """
55 # an account object
56 self.account = gajim.connections[account]
57 self.jid = jid
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))
74 self.initiate()
76 def initiate(self):
78 self.pulse_id = None # to satisfy self.setup_pulsing()
79 self.commandlist = None # a list of (commandname, commanddescription)
81 # command's data
82 self.sessionid = None
83 self.dataform = None
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)
94 if self.commandnode:
95 # Execute command
96 self.stage3()
97 else:
98 # setting initial stage
99 self.stage1()
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):
110 pass
112 def stage_back_button_clicked(self, *anything):
113 assert False
115 def stage_forward_button_clicked(self, *anything):
116 assert False
118 def stage_execute_button_clicked(self, *anything):
119 assert False
121 def stage_close_button_clicked(self, *anything):
122 assert False
124 def stage_restart_button_clicked(self, *anything):
125 assert False
127 def stage_adhoc_commands_window_delete_event(self, *anything):
128 assert False
130 def do_nothing(self, *anything):
131 return False
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)
159 def __del__(self):
160 print 'Object has been deleted.'
162 # stage 1: waiting for command list
163 def stage1(self):
165 Prepare the first stage. Request command list, set appropriate state of
166 widgets
168 # close old stage...
169 self.stage_finish()
171 # show the stage
172 self.stages_notebook.set_current_page(
173 self.stages_notebook.page_num(
174 self.retrieving_commands_stage_vbox))
176 # set widgets' state
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()
185 self.setup_pulsing(
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
201 self.stage_finish()
202 self.window.destroy()
204 def stage1_restart_button_clicked(self, widget):
205 self.stage_finish()
206 self.restart()
208 def stage1_adhoc_commands_window_delete_event(self, widget):
209 self.stage1_finish()
210 return True
212 # stage 2: choosing the command to execute
213 def stage2(self):
215 Populate the command list vbox with radiobuttons
217 FIXME: If there is more commands, maybe some kind of list, set widgets
218 state
220 # close old stage
221 self.stage_finish()
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
235 first_radio = None
236 for (commandnode, commandname) in self.commandlist:
237 radio = gtk.RadioButton(first_radio, label=commandname)
238 radio.connect("toggled", self.on_command_radiobutton_toggled,
239 commandnode)
240 if not first_radio:
241 first_radio = radio
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):
261 self.stage_finish()
262 self.window.destroy()
264 def stage2_restart_button_clicked(self, widget):
265 self.stage_finish()
266 self.restart()
268 def stage2_forward_button_clicked(self, widget):
269 self.stage3()
271 def on_command_radiobutton_toggled(self, widget, commandnode):
272 self.commandnode = commandnode
274 def on_check_commands_1_button_clicked(self, widget):
275 self.stage1()
277 # stage 3: command invocation
278 def stage3(self):
279 # close old stage
280 self.stage_finish()
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):
310 pass
312 def stage3_can_close(self, cb):
313 if self.form_status == 'completed':
314 cb()
315 return
317 def on_yes(button):
318 self.send_cancel()
319 dialog.destroy()
320 cb()
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)
326 dialog.popup()
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:
336 return False
338 def on_ok():
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):
347 def on_ok():
348 self.restart()
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)
373 return
374 self.data_form_widget.data_form.type = 'submit'
375 else:
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)
391 return
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.'),
400 senderror=True)
401 return
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)
410 try:
411 self.data_form_widget.data_form = self.dataform
412 except dataforms.Error:
413 self.stage5(error=_('Service sent malformed data'),
414 senderror=True)
415 return
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)
420 else:
421 self.data_form_widget.hide()
423 actions = command.getTag('actions')
424 if 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 \
432 None)
433 else:
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')
451 if 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()
455 else:
456 self.notes_label.set_no_show_all(True)
457 self.notes_label.hide()
459 def restart(self):
460 self.commandnode = None
461 self.initiate()
463 # stage 4: no commands are exposed
464 def stage4(self):
466 Display the message. Wait for user to close the window
468 # close old stage
469 self.stage_finish()
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):
489 self.restart()
491 def on_check_commands_2_button_clicked(self, widget):
492 self.stage1()
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
500 # close old stage
501 self.stage_finish()
503 assert errorid or error
505 if errorid:
506 # we've got error code, display appropriate message
507 try:
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.')
514 elif error:
515 # we've got error message
516 pass
517 else:
518 # we don't know what's that, bailing out
519 assert False
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):
541 self.restart()
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)
552 def callback():
553 progressbar.pulse()
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
563 if self.pulse_id:
564 gobject.source_remove(self.pulse_id)
565 self.pulse_id = None
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()
581 if error:
582 # extracting error description from xmpp/protocol.py
583 self.stage5(errorid = error)
584 return
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')
591 else:
592 items = []
593 if len(items)==0:
594 self.commandlist = []
595 self.stage4()
596 else:
597 self.commandlist = [(t.getAttr('node'), t.getAttr('name')) \
598 for t in items]
599 self.stage2()
601 self.account.connection.SendAndCallForResponse(query, callback)
603 def send_command(self, action='execute'):
605 Send the command with data form. Wait for reply
607 # create the stanza
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})
615 if self.sessionid:
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()
624 if err:
625 self.stage5(errorid = err)
626 else:
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,
642 'action':'cancel'
645 self.account.connection.send(stanza)
646 else:
647 # we did not received any reply from service;
648 # FIXME: we should wait and then send cancel; for now we do nothing
649 pass