Prevent Ctrl+f to open send file dialog if GTK has Emacs-style keybindings enabled...
[gajim.git] / src / dataforms_widget.py
blobe63ee5a51994b77dc4aba9a9f79546706dfe90c2
1 # -*- coding:utf-8 -*-
2 ## src/dataforms_widget.py
3 ##
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2006 Tomasz Melcer <liori AT exroot.org>
6 ## Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
7 ##
8 ## This file is part of Gajim.
9 ##
10 ## Gajim is free software; you can redistribute it and/or modify
11 ## it under the terms of the GNU General Public License as published
12 ## by the Free Software Foundation; version 3 only.
14 ## Gajim is distributed in the hope that it will be useful,
15 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ## GNU General Public License for more details.
19 ## You should have received a copy of the GNU General Public License
20 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
23 ''' This module contains widget that can display data form (JEP-0004).
24 Words single and multiple refers here to types of data forms:
25 single means these with one record of data (without <reported/> element),
26 multiple - these which may contain more data (with <reported/> element).'''
28 import gtk
29 import gobject
30 import base64
32 import gtkgui_helpers
33 import dialogs
35 import common.dataforms as dataforms
36 from common import helpers
38 import itertools
40 class DataFormWidget(gtk.Alignment, object):
41 # "public" interface
42 """
43 Data Form widget. Use like any other widget
44 """
46 __gsignals__ = dict(
47 validated = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, ())
50 def __init__(self, dataformnode=None):
51 ''' Create a widget. '''
52 gtk.Alignment.__init__(self, xscale=1.0, yscale=1.0)
54 self._data_form = None
56 self.xml = gtkgui_helpers.get_gtk_builder('data_form_window.ui',
57 'data_form_vbox')
58 self.xml.connect_signals(self)
59 for name in ('instructions_label', 'instructions_hseparator',
60 'single_form_viewport', 'data_form_types_notebook',
61 'single_form_scrolledwindow', 'multiple_form_hbox',
62 'records_treeview', 'buttons_vbox', 'add_button', 'remove_button',
63 'edit_button', 'up_button', 'down_button', 'clear_button'):
64 self.__dict__[name] = self.xml.get_object(name)
66 self.add(self.xml.get_object('data_form_vbox'))
68 if dataformnode is not None:
69 self.set_data_form(dataformnode)
71 selection = self.records_treeview.get_selection()
72 selection.connect('changed', self.on_records_selection_changed)
73 selection.set_mode(gtk.SELECTION_MULTIPLE)
75 def on_data_form_vbox_key_press_event(self, widget, event):
76 print 'key pressed'
78 def set_data_form(self, dataform):
79 """
80 Set the data form (xmpp.DataForm) displayed in widget
81 """
82 assert isinstance(dataform, dataforms.DataForm)
84 self.del_data_form()
85 self._data_form = dataform
86 if isinstance(dataform, dataforms.SimpleDataForm):
87 self.build_single_data_form()
88 else:
89 self.build_multiple_data_form()
91 # create appropriate description for instructions field if there isn't any
92 if dataform.instructions == '':
93 self.instructions_label.set_no_show_all(True)
94 self.instructions_label.hide()
95 else:
96 self.instructions_label.set_text(dataform.instructions)
97 gtkgui_helpers.label_set_autowrap(self.instructions_label)
99 def get_data_form(self):
101 Data form displayed in the widget or None if no form
103 return self._data_form
105 def del_data_form(self):
106 self.clean_data_form()
107 self._data_form = None
109 data_form = property(get_data_form, set_data_form, del_data_form,
110 'Data form presented in a widget')
112 def get_title(self):
114 Get the title of data form, as a unicode object. If no title or no form,
115 returns u''. Useful for setting window title
117 if self._data_form is not None:
118 if self._data_form.title is not None:
119 return self._data_form.title
120 return u''
122 title = property(get_title, None, None, 'Data form title')
124 def show(self):
125 ''' Treat 'us' as one widget. '''
126 self.show_all()
128 # "private" methods
130 # we have actually two different kinds of data forms: one is a simple form to fill,
131 # second is a table with several records;
133 def empty_method(self):
134 pass
136 def clean_data_form(self):
138 Remove data about existing form. This metod is empty, because it is
139 rewritten by build_*_data_form, according to type of form which is
140 actually displayed
142 pass
144 def build_single_data_form(self):
145 '''Invoked when new single form is to be created.'''
146 assert isinstance(self._data_form, dataforms.SimpleDataForm)
148 self.clean_data_form()
150 self.singleform = SingleForm(self._data_form)
151 def _on_validated(widget):
152 self.emit('validated')
153 self.singleform.connect('validated', _on_validated)
154 self.singleform.show()
155 self.single_form_viewport.add(self.singleform)
156 self.data_form_types_notebook.set_current_page(
157 self.data_form_types_notebook.page_num(
158 self.single_form_scrolledwindow))
160 self.clean_data_form = self.clean_single_data_form
162 def clean_single_data_form(self):
164 Called as clean_data_form, read the docs of clean_data_form(). Remove
165 form from widget
167 self.singleform.destroy()
168 self.clean_data_form = self.empty_method # we won't call it twice
169 del self.singleform
171 def build_multiple_data_form(self):
173 Invoked when new multiple form is to be created
175 assert isinstance(self._data_form, dataforms.MultipleDataForm)
177 self.clean_data_form()
179 # creating model for form...
180 fieldtypes = []
181 fieldvars = []
182 for field in self._data_form.reported.iter_fields():
183 # note: we store also text-private and hidden fields,
184 # we just do not display them.
185 # TODO: boolean fields
186 #elif field.type=='boolean': fieldtypes.append(bool)
187 fieldtypes.append(str)
188 fieldvars.append(field.var)
190 self.multiplemodel = gtk.ListStore(*fieldtypes)
192 # moving all data to model
193 for item in self._data_form.iter_records():
194 iter_ = self.multiplemodel.append()
195 for field in item.iter_fields():
196 if field.var in fieldvars:
197 self.multiplemodel.set_value(iter_,
198 fieldvars.index(field.var), field.value)
200 # constructing columns...
201 for field, counter in zip(self._data_form.reported.iter_fields(),
202 itertools.count()):
203 self.records_treeview.append_column(
204 gtk.TreeViewColumn(field.label, gtk.CellRendererText(),
205 text=counter))
207 self.records_treeview.set_model(self.multiplemodel)
208 self.records_treeview.show_all()
210 self.data_form_types_notebook.set_current_page(
211 self.data_form_types_notebook.page_num(
212 self.multiple_form_hbox))
214 self.clean_data_form = self.clean_multiple_data_form
216 readwrite = self._data_form.type != 'result'
217 if not readwrite:
218 self.buttons_vbox.set_no_show_all(True)
219 self.buttons_vbox.hide()
220 else:
221 self.buttons_vbox.set_no_show_all(False)
222 # refresh list look
223 self.refresh_multiple_buttons()
225 def clean_multiple_data_form(self):
227 Called as clean_data_form, read the docs of clean_data_form(). Remove
228 form from widget
230 self.clean_data_form = self.empty_method # we won't call it twice
231 del self.multiplemodel
233 def refresh_multiple_buttons(self):
235 Checks for treeview state and makes control buttons sensitive
237 selection = self.records_treeview.get_selection()
238 model = self.records_treeview.get_model()
239 count = selection.count_selected_rows()
240 if count == 0:
241 self.remove_button.set_sensitive(False)
242 self.edit_button.set_sensitive(False)
243 self.up_button.set_sensitive(False)
244 self.down_button.set_sensitive(False)
245 elif count == 1:
246 self.remove_button.set_sensitive(True)
247 self.edit_button.set_sensitive(True)
248 _, (path,) = selection.get_selected_rows()
249 iter_ = model.get_iter(path)
250 if model.iter_next(iter_) is None:
251 self.up_button.set_sensitive(True)
252 self.down_button.set_sensitive(False)
253 elif path == (0, ):
254 self.up_button.set_sensitive(False)
255 self.down_button.set_sensitive(True)
256 else:
257 self.up_button.set_sensitive(True)
258 self.down_button.set_sensitive(True)
259 else:
260 self.remove_button.set_sensitive(True)
261 self.edit_button.set_sensitive(True)
262 self.up_button.set_sensitive(False)
263 self.down_button.set_sensitive(False)
265 if len(model) == 0:
266 self.clear_button.set_sensitive(False)
267 else:
268 self.clear_button.set_sensitive(True)
270 def on_clear_button_clicked(self, widget):
271 self.records_treeview.get_model().clear()
273 def on_remove_button_clicked(self, widget):
274 selection = self.records_treeview.get_selection()
275 model, rowrefs = selection.get_selected_rows()
276 # rowref is a list of paths
277 for i in xrange(len(rowrefs)):
278 rowrefs[i] = gtk.TreeRowReference(model, rowrefs[i])
279 # rowref is a list of row references; need to convert because we will
280 # modify the model, paths would change
281 for rowref in rowrefs:
282 del model[rowref.get_path()]
284 def on_up_button_clicked(self, widget):
285 selection = self.records_treeview.get_selection()
286 model, (path,) = selection.get_selected_rows()
287 iter_ = model.get_iter(path)
288 # constructing path for previous iter
289 previter = model.get_iter((path[0]-1,))
290 model.swap(iter_, previter)
292 self.refresh_multiple_buttons()
294 def on_down_button_clicked(self, widget):
295 selection = self.records_treeview.get_selection()
296 model, (path,) = selection.get_selected_rows()
297 iter_ = model.get_iter(path)
298 nextiter = model.iter_next(iter_)
299 model.swap(iter_, nextiter)
301 self.refresh_multiple_buttons()
303 def on_records_selection_changed(self, widget):
304 self.refresh_multiple_buttons()
306 class SingleForm(gtk.Table, object):
308 Widget that represent DATAFORM_SINGLE mode form. Because this is used not
309 only to display single forms, but to form input windows of multiple-type
310 forms, it is in another class
313 __gsignals__ = dict(
314 validated = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, ())
317 def __init__(self, dataform):
318 assert isinstance(dataform, dataforms.SimpleDataForm)
320 gtk.Table.__init__(self)
321 self.set_col_spacings(12)
322 self.set_row_spacings(6)
324 def decorate_with_tooltip(widget, field):
326 Adds a tooltip containing field's description to a widget. Creates
327 EventBox if widget doesn't have its own gdk window. Returns decorated
328 widget
330 if field.description != '':
331 if widget.flags() & gtk.NO_WINDOW:
332 evbox = gtk.EventBox()
333 evbox.add(widget)
334 widget = evbox
335 widget.set_tooltip_text(field.description)
336 return widget
338 self._data_form = dataform
340 # building widget
341 linecounter = 0
343 # is the form changeable?
344 readwrite = dataform.type != 'result'
346 # for each field...
347 for field in self._data_form.iter_fields():
348 if field.type == 'hidden': continue
350 commonlabel = True
351 commonlabelcenter = False
352 commonwidget = True
353 widget = None
355 if field.type == 'boolean':
356 commonlabelcenter = True
357 widget = gtk.CheckButton()
358 widget.connect('toggled', self.on_boolean_checkbutton_toggled,
359 field)
360 widget.set_active(field.value)
362 elif field.type == 'fixed':
363 leftattach = 1
364 rightattach = 2
365 if field.label is None:
366 commonlabel = False
367 leftattach = 0
369 commonwidget = False
370 widget = gtk.Label(field.value)
371 widget.set_line_wrap(True)
372 self.attach(widget, leftattach, rightattach, linecounter,
373 linecounter+1, xoptions=gtk.FILL, yoptions=gtk.FILL)
375 elif field.type == 'list-single':
376 # TODO: What if we have radio buttons and non-required field?
377 # TODO: We cannot deactivate them all...
378 if len(field.options) < 6:
379 # 5 option max: show radiobutton
380 widget = gtk.VBox()
381 first_radio = None
382 for value, label in field.iter_options():
383 if not label:
384 label = value
385 radio = gtk.RadioButton(first_radio, label=label)
386 radio.connect('toggled',
387 self.on_list_single_radiobutton_toggled, field, value)
388 if first_radio is None:
389 first_radio = radio
390 if field.value == '': # TODO: is None when done
391 field.value = value
392 if value == field.value:
393 radio.set_active(True)
394 widget.pack_start(radio, expand=False)
395 else:
396 # more than 5 options: show combobox
397 def on_list_single_combobox_changed(combobox, f):
398 iter_ = combobox.get_active_iter()
399 if iter_:
400 model = combobox.get_model()
401 f.value = model[iter_][1]
402 else:
403 f.value = ''
404 widget = gtkgui_helpers.create_combobox(field.options,
405 field.value)
406 widget.connect('changed', on_list_single_combobox_changed, field)
407 widget.set_sensitive(readwrite)
409 elif field.type == 'list-multi':
410 # TODO: When more than few choices, make a list
411 if len(field.options) < 6:
412 # 5 option max: show checkbutton
413 widget = gtk.VBox()
414 for value, label in field.iter_options():
415 check = gtk.CheckButton(label, use_underline=False)
416 check.set_active(value in field.values)
417 check.connect('toggled',
418 self.on_list_multi_checkbutton_toggled, field, value)
419 widget.pack_start(check, expand=False)
420 widget.set_sensitive(readwrite)
421 else:
422 # more than 5 options: show combobox
423 def on_list_multi_treeview_changed(selection, f):
424 def for_selected(treemodel, path, iter):
425 vals.append(treemodel[iter][1])
426 vals = []
427 selection.selected_foreach(for_selected)
428 field.values = vals[:]
429 widget = gtk.ScrolledWindow()
430 widget.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
431 tv = gtkgui_helpers.create_list_multi(field.options,
432 field.values)
433 widget.add(tv)
434 widget.set_size_request(-1, 120)
435 tv.get_selection().connect('changed',
436 on_list_multi_treeview_changed, field)
437 tv.set_sensitive(readwrite)
439 elif field.type == 'jid-single':
440 widget = gtk.Entry()
441 widget.connect('changed', self.on_text_single_entry_changed, field)
442 widget.set_text(field.value)
444 elif field.type == 'jid-multi':
445 commonwidget = False
447 xml = gtkgui_helpers.get_gtk_builder('data_form_window.ui',
448 'multiple_form_hbox')
449 widget = xml.get_object('multiple_form_hbox')
450 treeview = xml.get_object('records_treeview')
452 listmodel = gtk.ListStore(str)
453 for value in field.iter_values():
454 # nobody will create several megabytes long stanza
455 listmodel.insert(999999, (value,))
457 treeview.set_model(listmodel)
459 renderer = gtk.CellRendererText()
460 renderer.set_property('editable', True)
461 renderer.connect('edited',
462 self.on_jid_multi_cellrenderertext_edited, treeview, listmodel,
463 field)
465 treeview.append_column(gtk.TreeViewColumn(None, renderer,
466 text=0))
468 decorate_with_tooltip(treeview, field)
470 add_button=xml.get_object('add_button')
471 add_button.connect('clicked',
472 self.on_jid_multi_add_button_clicked, treeview, listmodel, field)
473 edit_button=xml.get_object('edit_button')
474 edit_button.connect('clicked',
475 self.on_jid_multi_edit_button_clicked, treeview)
476 remove_button=xml.get_object('remove_button')
477 remove_button.connect('clicked',
478 self.on_jid_multi_remove_button_clicked, treeview, field)
479 clear_button=xml.get_object('clear_button')
480 clear_button.connect('clicked',
481 self.on_jid_multi_clean_button_clicked, listmodel, field)
482 if not readwrite:
483 add_button.set_no_show_all(True)
484 edit_button.set_no_show_all(True)
485 remove_button.set_no_show_all(True)
486 clear_button.set_no_show_all(True)
488 widget.set_sensitive(readwrite)
489 self.attach(widget, 1, 2, linecounter, linecounter+1)
491 del xml
493 elif field.type == 'text-private':
494 commonlabelcenter = True
495 widget = gtk.Entry()
496 widget.connect('changed', self.on_text_single_entry_changed, field)
497 widget.set_visibility(False)
498 widget.set_text(field.value)
500 elif field.type == 'text-multi':
501 # TODO: bigger text view
502 commonwidget = False
504 textwidget = gtk.TextView()
505 textwidget.set_wrap_mode(gtk.WRAP_WORD)
506 textwidget.get_buffer().connect('changed',
507 self.on_text_multi_textbuffer_changed, field)
508 textwidget.get_buffer().set_text(field.value)
509 textwidget.set_sensitive(readwrite)
511 widget = gtk.ScrolledWindow()
512 widget.add(textwidget)
514 widget=decorate_with_tooltip(widget, field)
515 self.attach(widget, 1, 2, linecounter, linecounter+1)
517 else:
518 # field.type == 'text-single' or field.type is nonstandard:
519 # JEP says that if we don't understand some type, we
520 # should handle it as text-single
521 commonlabelcenter = True
522 if readwrite:
523 widget = gtk.Entry()
524 def kpe(widget, event):
525 if event.keyval == gtk.keysyms.Return or \
526 event.keyval == gtk.keysyms.KP_Enter:
527 self.emit('validated')
528 widget.connect('key-press-event', kpe)
529 widget.connect('changed', self.on_text_single_entry_changed,
530 field)
531 widget.set_sensitive(readwrite)
532 if field.value is None:
533 field.value = u''
534 widget.set_text(field.value)
535 else:
536 commonwidget=False
537 widget = gtk.Label(field.value)
538 widget.set_sensitive(True)
539 widget.set_alignment(0.0, 0.5)
540 widget=decorate_with_tooltip(widget, field)
541 self.attach(widget, 1, 2, linecounter, linecounter+1,
542 yoptions=gtk.FILL)
544 if commonlabel and field.label is not None:
545 label = gtk.Label(field.label)
546 if commonlabelcenter:
547 label.set_alignment(0.0, 0.5)
548 else:
549 label.set_alignment(0.0, 0.0)
550 label = decorate_with_tooltip(label, field)
551 self.attach(label, 0, 1, linecounter, linecounter+1,
552 xoptions=gtk.FILL, yoptions=gtk.FILL)
554 if field.media is not None:
555 for uri in field.media.uris:
556 if uri.type_.startswith('image/'):
557 try:
558 img_data = base64.decodestring(uri.uri_data)
559 pixbuf_l = gtk.gdk.PixbufLoader()
560 pixbuf_l.write(img_data)
561 pixbuf_l.close()
562 media = gtk.image_new_from_pixbuf(pixbuf_l.\
563 get_pixbuf())
564 except Exception:
565 media = gtk.Label(_('Unable to load image'))
566 else:
567 media = gtk.Label(_('Media type not supported: %s') % \
568 uri.type_)
569 linecounter += 1
570 self.attach(media, 0, 1, linecounter, linecounter+1,
571 xoptions=gtk.FILL, yoptions=gtk.FILL)
573 if commonwidget:
574 assert widget is not None
575 widget.set_sensitive(readwrite)
576 widget = decorate_with_tooltip(widget, field)
577 self.attach(widget, 1, 2, linecounter, linecounter+1,
578 yoptions=gtk.FILL)
580 if field.required:
581 label = gtk.Label('*')
582 label.set_tooltip_text(_('This field is required'))
583 self.attach(label, 2, 3, linecounter, linecounter+1, xoptions=0,
584 yoptions=0)
586 linecounter+=1
587 if self.get_property('visible'):
588 self.show_all()
590 def show(self):
591 # simulate that we are one widget
592 self.show_all()
594 def on_boolean_checkbutton_toggled(self, widget, field):
595 field.value = widget.get_active()
597 def on_list_single_radiobutton_toggled(self, widget, field, value):
598 field.value = value
600 def on_list_multi_checkbutton_toggled(self, widget, field, value):
601 # TODO: make some methods like add_value and remove_value
602 if widget.get_active() and value not in field.values:
603 field.values += [value]
604 elif not widget.get_active() and value in field.values:
605 field.values = [v for v in field.values if v!=value]
607 def on_text_single_entry_changed(self, widget, field):
608 field.value = widget.get_text()
610 def on_text_multi_textbuffer_changed(self, widget, field):
611 field.value = widget.get_text(
612 widget.get_start_iter(),
613 widget.get_end_iter())
615 def on_jid_multi_cellrenderertext_edited(self, cell, path, newtext, treeview,
616 model, field):
617 old = model[path][0]
618 if old == newtext:
619 return
620 try:
621 newtext = helpers.parse_jid(newtext)
622 except helpers.InvalidFormat, s:
623 dialogs.ErrorDialog(_('Invalid Jabber ID'), str(s))
624 return
625 if newtext in field.values:
626 dialogs.ErrorDialog(
627 _('Jabber ID already in list'),
628 _('The Jabber ID you entered is already in the list. Choose another one.'))
629 gobject.idle_add(treeview.set_cursor, path)
630 return
631 model[path][0]=newtext
633 values = field.values
634 values[values.index(old)]=newtext
635 field.values = values
637 def on_jid_multi_add_button_clicked(self, widget, treeview, model, field):
638 #Default jid
639 jid = _('new@jabber.id')
640 if jid in field.values:
641 i = 1
642 while _('new%d@jabber.id') % i in field.values:
643 i += 1
644 jid = _('new%d@jabber.id') % i
645 iter_ = model.insert(999999, (jid,))
646 treeview.set_cursor(model.get_path(iter_), treeview.get_column(0), True)
647 field.values = field.values + [jid]
649 def on_jid_multi_edit_button_clicked(self, widget, treeview):
650 model, iter_ = treeview.get_selection().get_selected()
651 assert iter_ is not None
653 treeview.set_cursor(model.get_path(iter_), treeview.get_column(0), True)
655 def on_jid_multi_remove_button_clicked(self, widget, treeview, field):
656 selection = treeview.get_selection()
657 deleted = []
659 def remove(model, path, iter_, deleted):
660 deleted+=model[iter_]
661 model.remove(iter_)
663 selection.selected_foreach(remove, deleted)
664 field.values = (v for v in field.values if v not in deleted)
666 def on_jid_multi_clean_button_clicked(self, widget, model, field):
667 model.clear()
668 del field.values