whiteboard plugin. added border to the main container
[gajim.git] / src / dataforms_widget.py
blobce7012563782246cdc2ea7607bc3b581880af647
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 def __init__(self, dataformnode=None):
47 ''' Create a widget. '''
48 gtk.Alignment.__init__(self, xscale=1.0, yscale=1.0)
50 self._data_form = None
52 self.xml = gtkgui_helpers.get_gtk_builder('data_form_window.ui',
53 'data_form_vbox')
54 self.xml.connect_signals(self)
55 for name in ('instructions_label', 'instructions_hseparator',
56 'single_form_viewport', 'data_form_types_notebook',
57 'single_form_scrolledwindow', 'multiple_form_hbox',
58 'records_treeview', 'buttons_vbox', 'add_button', 'remove_button',
59 'edit_button', 'up_button', 'down_button', 'clear_button'):
60 self.__dict__[name] = self.xml.get_object(name)
62 self.add(self.xml.get_object('data_form_vbox'))
64 if dataformnode is not None:
65 self.set_data_form(dataformnode)
67 selection = self.records_treeview.get_selection()
68 selection.connect('changed', self.on_records_selection_changed)
69 selection.set_mode(gtk.SELECTION_MULTIPLE)
71 def set_data_form(self, dataform):
72 """
73 Set the data form (xmpp.DataForm) displayed in widget
74 """
75 assert isinstance(dataform, dataforms.DataForm)
77 self.del_data_form()
78 self._data_form = dataform
79 if isinstance(dataform, dataforms.SimpleDataForm):
80 self.build_single_data_form()
81 else:
82 self.build_multiple_data_form()
84 # create appropriate description for instructions field if there isn't any
85 if dataform.instructions == '':
86 self.instructions_label.set_no_show_all(True)
87 self.instructions_label.hide()
88 else:
89 self.instructions_label.set_text(dataform.instructions)
90 gtkgui_helpers.label_set_autowrap(self.instructions_label)
92 def get_data_form(self):
93 """
94 Data form displayed in the widget or None if no form
95 """
96 return self._data_form
98 def del_data_form(self):
99 self.clean_data_form()
100 self._data_form = None
102 data_form = property(get_data_form, set_data_form, del_data_form,
103 'Data form presented in a widget')
105 def get_title(self):
107 Get the title of data form, as a unicode object. If no title or no form,
108 returns u''. Useful for setting window title
110 if self._data_form is not None:
111 if self._data_form.title is not None:
112 return self._data_form.title
113 return u''
115 title = property(get_title, None, None, 'Data form title')
117 def show(self):
118 ''' Treat 'us' as one widget. '''
119 self.show_all()
121 # "private" methods
123 # we have actually two different kinds of data forms: one is a simple form to fill,
124 # second is a table with several records;
126 def empty_method(self):
127 pass
129 def clean_data_form(self):
131 Remove data about existing form. This metod is empty, because it is
132 rewritten by build_*_data_form, according to type of form which is
133 actually displayed
135 pass
137 def build_single_data_form(self):
138 '''Invoked when new single form is to be created.'''
139 assert isinstance(self._data_form, dataforms.SimpleDataForm)
141 self.clean_data_form()
143 self.singleform = SingleForm(self._data_form)
144 self.singleform.show()
145 self.single_form_viewport.add(self.singleform)
146 self.data_form_types_notebook.set_current_page(
147 self.data_form_types_notebook.page_num(
148 self.single_form_scrolledwindow))
150 self.clean_data_form = self.clean_single_data_form
152 def clean_single_data_form(self):
154 Called as clean_data_form, read the docs of clean_data_form(). Remove
155 form from widget
157 self.singleform.destroy()
158 self.clean_data_form = self.empty_method # we won't call it twice
159 del self.singleform
161 def build_multiple_data_form(self):
163 Invoked when new multiple form is to be created
165 assert isinstance(self._data_form, dataforms.MultipleDataForm)
167 self.clean_data_form()
169 # creating model for form...
170 fieldtypes = []
171 fieldvars = []
172 for field in self._data_form.reported.iter_fields():
173 # note: we store also text-private and hidden fields,
174 # we just do not display them.
175 # TODO: boolean fields
176 #elif field.type=='boolean': fieldtypes.append(bool)
177 fieldtypes.append(str)
178 fieldvars.append(field.var)
180 self.multiplemodel = gtk.ListStore(*fieldtypes)
182 # moving all data to model
183 for item in self._data_form.iter_records():
184 iter_ = self.multiplemodel.append()
185 for field in item.iter_fields():
186 self.multiplemodel.set_value(iter_, fieldvars.index(field.var),
187 field.value)
189 # constructing columns...
190 for field, counter in zip(self._data_form.reported.iter_fields(),
191 itertools.count()):
192 self.records_treeview.append_column(
193 gtk.TreeViewColumn(field.label, gtk.CellRendererText(),
194 text=counter))
196 self.records_treeview.set_model(self.multiplemodel)
197 self.records_treeview.show_all()
199 self.data_form_types_notebook.set_current_page(
200 self.data_form_types_notebook.page_num(
201 self.multiple_form_hbox))
203 self.clean_data_form = self.clean_multiple_data_form
205 readwrite = self._data_form.type != 'result'
206 if not readwrite:
207 self.buttons_vbox.set_no_show_all(True)
208 self.buttons_vbox.hide()
209 else:
210 self.buttons_vbox.set_no_show_all(False)
211 # refresh list look
212 self.refresh_multiple_buttons()
214 def clean_multiple_data_form(self):
216 Called as clean_data_form, read the docs of clean_data_form(). Remove
217 form from widget
219 self.clean_data_form = self.empty_method # we won't call it twice
220 del self.multiplemodel
222 def refresh_multiple_buttons(self):
224 Checks for treeview state and makes control buttons sensitive
226 selection = self.records_treeview.get_selection()
227 model = self.records_treeview.get_model()
228 count = selection.count_selected_rows()
229 if count == 0:
230 self.remove_button.set_sensitive(False)
231 self.edit_button.set_sensitive(False)
232 self.up_button.set_sensitive(False)
233 self.down_button.set_sensitive(False)
234 elif count == 1:
235 self.remove_button.set_sensitive(True)
236 self.edit_button.set_sensitive(True)
237 _, (path,) = selection.get_selected_rows()
238 iter_ = model.get_iter(path)
239 if model.iter_next(iter_) is None:
240 self.up_button.set_sensitive(True)
241 self.down_button.set_sensitive(False)
242 elif path == (0, ):
243 self.up_button.set_sensitive(False)
244 self.down_button.set_sensitive(True)
245 else:
246 self.up_button.set_sensitive(True)
247 self.down_button.set_sensitive(True)
248 else:
249 self.remove_button.set_sensitive(True)
250 self.edit_button.set_sensitive(True)
251 self.up_button.set_sensitive(False)
252 self.down_button.set_sensitive(False)
254 if len(model) == 0:
255 self.clear_button.set_sensitive(False)
256 else:
257 self.clear_button.set_sensitive(True)
259 def on_clear_button_clicked(self, widget):
260 self.records_treeview.get_model().clear()
262 def on_remove_button_clicked(self, widget):
263 selection = self.records_treeview.get_selection()
264 model, rowrefs = selection.get_selected_rows()
265 # rowref is a list of paths
266 for i in xrange(len(rowrefs)):
267 rowrefs[i] = gtk.TreeRowReference(model, rowrefs[i])
268 # rowref is a list of row references; need to convert because we will
269 # modify the model, paths would change
270 for rowref in rowrefs:
271 del model[rowref.get_path()]
273 def on_up_button_clicked(self, widget):
274 selection = self.records_treeview.get_selection()
275 model, (path,) = selection.get_selected_rows()
276 iter_ = model.get_iter(path)
277 # constructing path for previous iter
278 previter = model.get_iter((path[0]-1,))
279 model.swap(iter_, previter)
281 self.refresh_multiple_buttons()
283 def on_down_button_clicked(self, widget):
284 selection = self.records_treeview.get_selection()
285 model, (path,) = selection.get_selected_rows()
286 iter_ = model.get_iter(path)
287 nextiter = model.iter_next(iter_)
288 model.swap(iter_, nextiter)
290 self.refresh_multiple_buttons()
292 def on_records_selection_changed(self, widget):
293 self.refresh_multiple_buttons()
295 class SingleForm(gtk.Table, object):
297 Widget that represent DATAFORM_SINGLE mode form. Because this is used not
298 only to display single forms, but to form input windows of multiple-type
299 forms, it is in another class
302 def __init__(self, dataform):
303 assert isinstance(dataform, dataforms.SimpleDataForm)
305 gtk.Table.__init__(self)
306 self.set_col_spacings(12)
307 self.set_row_spacings(6)
309 def decorate_with_tooltip(widget, field):
311 Adds a tooltip containing field's description to a widget. Creates
312 EventBox if widget doesn't have its own gdk window. Returns decorated
313 widget
315 if field.description != '':
316 if widget.flags() & gtk.NO_WINDOW:
317 evbox = gtk.EventBox()
318 evbox.add(widget)
319 widget = evbox
320 widget.set_tooltip_text(field.description)
321 return widget
323 self._data_form = dataform
325 # building widget
326 linecounter = 0
328 # is the form changeable?
329 readwrite = dataform.type != 'result'
331 # for each field...
332 for field in self._data_form.iter_fields():
333 if field.type == 'hidden': continue
335 commonlabel = True
336 commonlabelcenter = False
337 commonwidget = True
338 widget = None
340 if field.type == 'boolean':
341 commonlabelcenter = True
342 widget = gtk.CheckButton()
343 widget.connect('toggled', self.on_boolean_checkbutton_toggled,
344 field)
345 widget.set_active(field.value)
347 elif field.type == 'fixed':
348 leftattach = 1
349 rightattach = 2
350 if field.label is None:
351 commonlabel = False
352 leftattach = 0
354 commonwidget = False
355 widget = gtk.Label(field.value)
356 widget.set_line_wrap(True)
357 self.attach(widget, leftattach, rightattach, linecounter,
358 linecounter+1, xoptions=gtk.FILL, yoptions=gtk.FILL)
360 elif field.type == 'list-single':
361 # TODO: What if we have radio buttons and non-required field?
362 # TODO: We cannot deactivate them all...
363 if len(field.options) < 6:
364 # 5 option max: show radiobutton
365 widget = gtk.VBox()
366 first_radio = None
367 for value, label in field.iter_options():
368 if not label:
369 label = value
370 radio = gtk.RadioButton(first_radio, label=label)
371 radio.connect('toggled',
372 self.on_list_single_radiobutton_toggled, field, value)
373 if first_radio is None:
374 first_radio = radio
375 if field.value == '': # TODO: is None when done
376 field.value = value
377 if value == field.value:
378 radio.set_active(True)
379 widget.pack_start(radio, expand=False)
380 else:
381 # more than 5 options: show combobox
382 def on_list_single_combobox_changed(combobox, f):
383 iter_ = combobox.get_active_iter()
384 if iter_:
385 model = combobox.get_model()
386 f.value = model[iter_][1]
387 else:
388 f.value = ''
389 widget = gtkgui_helpers.create_combobox(field.options,
390 field.value)
391 widget.connect('changed', on_list_single_combobox_changed, field)
392 widget.set_sensitive(readwrite)
394 elif field.type == 'list-multi':
395 # TODO: When more than few choices, make a list
396 if len(field.options) < 6:
397 # 5 option max: show checkbutton
398 widget = gtk.VBox()
399 for value, label in field.iter_options():
400 check = gtk.CheckButton(label, use_underline=False)
401 check.set_active(value in field.values)
402 check.connect('toggled',
403 self.on_list_multi_checkbutton_toggled, field, value)
404 widget.pack_start(check, expand=False)
405 widget.set_sensitive(readwrite)
406 else:
407 # more than 5 options: show combobox
408 def on_list_multi_treeview_changed(selection, f):
409 def for_selected(treemodel, path, iter):
410 vals.append(treemodel[iter][1])
411 vals = []
412 selection.selected_foreach(for_selected)
413 field.values = vals[:]
414 widget = gtk.ScrolledWindow()
415 widget.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
416 tv = gtkgui_helpers.create_list_multi(field.options,
417 field.values)
418 widget.add(tv)
419 widget.set_size_request(-1, 120)
420 tv.get_selection().connect('changed',
421 on_list_multi_treeview_changed, field)
422 tv.set_sensitive(readwrite)
424 elif field.type == 'jid-single':
425 widget = gtk.Entry()
426 widget.connect('changed', self.on_text_single_entry_changed, field)
427 widget.set_text(field.value)
429 elif field.type == 'jid-multi':
430 commonwidget = False
432 xml = gtkgui_helpers.get_gtk_builder('data_form_window.ui',
433 'multiple_form_hbox')
434 widget = xml.get_object('multiple_form_hbox')
435 treeview = xml.get_object('records_treeview')
437 listmodel = gtk.ListStore(str)
438 for value in field.iter_values():
439 # nobody will create several megabytes long stanza
440 listmodel.insert(999999, (value,))
442 treeview.set_model(listmodel)
444 renderer = gtk.CellRendererText()
445 renderer.set_property('editable', True)
446 renderer.connect('edited',
447 self.on_jid_multi_cellrenderertext_edited, treeview, listmodel,
448 field)
450 treeview.append_column(gtk.TreeViewColumn(None, renderer,
451 text=0))
453 decorate_with_tooltip(treeview, field)
455 add_button=xml.get_object('add_button')
456 add_button.connect('clicked',
457 self.on_jid_multi_add_button_clicked, treeview, listmodel, field)
458 edit_button=xml.get_object('edit_button')
459 edit_button.connect('clicked',
460 self.on_jid_multi_edit_button_clicked, treeview)
461 remove_button=xml.get_object('remove_button')
462 remove_button.connect('clicked',
463 self.on_jid_multi_remove_button_clicked, treeview, field)
464 clear_button=xml.get_object('clear_button')
465 clear_button.connect('clicked',
466 self.on_jid_multi_clean_button_clicked, listmodel, field)
467 if not readwrite:
468 add_button.set_no_show_all(True)
469 edit_button.set_no_show_all(True)
470 remove_button.set_no_show_all(True)
471 clear_button.set_no_show_all(True)
473 widget.set_sensitive(readwrite)
474 self.attach(widget, 1, 2, linecounter, linecounter+1)
476 del xml
478 elif field.type == 'text-private':
479 commonlabelcenter = True
480 widget = gtk.Entry()
481 widget.connect('changed', self.on_text_single_entry_changed, field)
482 widget.set_visibility(False)
483 widget.set_text(field.value)
485 elif field.type == 'text-multi':
486 # TODO: bigger text view
487 commonwidget = False
489 textwidget = gtk.TextView()
490 textwidget.set_wrap_mode(gtk.WRAP_WORD)
491 textwidget.get_buffer().connect('changed',
492 self.on_text_multi_textbuffer_changed, field)
493 textwidget.get_buffer().set_text(field.value)
494 textwidget.set_sensitive(readwrite)
496 widget = gtk.ScrolledWindow()
497 widget.add(textwidget)
499 widget=decorate_with_tooltip(widget, field)
500 self.attach(widget, 1, 2, linecounter, linecounter+1)
502 else:
503 # field.type == 'text-single' or field.type is nonstandard:
504 # JEP says that if we don't understand some type, we
505 # should handle it as text-single
506 commonlabelcenter = True
507 if readwrite:
508 widget = gtk.Entry()
509 widget.connect('changed', self.on_text_single_entry_changed,
510 field)
511 widget.set_sensitive(readwrite)
512 if field.value is None:
513 field.value = u''
514 widget.set_text(field.value)
515 else:
516 commonwidget=False
517 widget = gtk.Label(field.value)
518 widget.set_sensitive(True)
519 widget.set_alignment(0.0, 0.5)
520 widget=decorate_with_tooltip(widget, field)
521 self.attach(widget, 1, 2, linecounter, linecounter+1,
522 yoptions=gtk.FILL)
524 if commonlabel and field.label is not None:
525 label = gtk.Label(field.label)
526 if commonlabelcenter:
527 label.set_alignment(0.0, 0.5)
528 else:
529 label.set_alignment(0.0, 0.0)
530 label = decorate_with_tooltip(label, field)
531 self.attach(label, 0, 1, linecounter, linecounter+1,
532 xoptions=gtk.FILL, yoptions=gtk.FILL)
534 if field.media is not None:
535 for uri in field.media.uris:
536 if uri.type_.startswith('image/'):
537 try:
538 img_data = base64.decodestring(uri.uri_data)
539 pixbuf_l = gtk.gdk.PixbufLoader()
540 pixbuf_l.write(img_data)
541 pixbuf_l.close()
542 media = gtk.image_new_from_pixbuf(pixbuf_l.\
543 get_pixbuf())
544 except Exception:
545 media = gtk.Label(_('Unable to load image'))
546 else:
547 media = gtk.Label(_('Media type not supported: %s') % \
548 uri.type_)
549 linecounter += 1
550 self.attach(media, 0, 1, linecounter, linecounter+1,
551 xoptions=gtk.FILL, yoptions=gtk.FILL)
553 if commonwidget:
554 assert widget is not None
555 widget.set_sensitive(readwrite)
556 widget = decorate_with_tooltip(widget, field)
557 self.attach(widget, 1, 2, linecounter, linecounter+1,
558 yoptions=gtk.FILL)
560 if field.required:
561 label = gtk.Label('*')
562 label.set_tooltip_text(_('This field is required'))
563 self.attach(label, 2, 3, linecounter, linecounter+1, xoptions=0,
564 yoptions=0)
566 linecounter+=1
567 if self.get_property('visible'):
568 self.show_all()
570 def show(self):
571 # simulate that we are one widget
572 self.show_all()
574 def on_boolean_checkbutton_toggled(self, widget, field):
575 field.value = widget.get_active()
577 def on_list_single_radiobutton_toggled(self, widget, field, value):
578 field.value = value
580 def on_list_multi_checkbutton_toggled(self, widget, field, value):
581 # TODO: make some methods like add_value and remove_value
582 if widget.get_active() and value not in field.values:
583 field.values += [value]
584 elif not widget.get_active() and value in field.values:
585 field.values = [v for v in field.values if v!=value]
587 def on_text_single_entry_changed(self, widget, field):
588 field.value = widget.get_text()
590 def on_text_multi_textbuffer_changed(self, widget, field):
591 field.value = widget.get_text(
592 widget.get_start_iter(),
593 widget.get_end_iter())
595 def on_jid_multi_cellrenderertext_edited(self, cell, path, newtext, treeview,
596 model, field):
597 old = model[path][0]
598 if old == newtext:
599 return
600 try:
601 newtext = helpers.parse_jid(newtext)
602 except helpers.InvalidFormat, s:
603 dialogs.ErrorDialog(_('Invalid Jabber ID'), str(s))
604 return
605 if newtext in field.values:
606 dialogs.ErrorDialog(
607 _('Jabber ID already in list'),
608 _('The Jabber ID you entered is already in the list. Choose another one.'))
609 gobject.idle_add(treeview.set_cursor, path)
610 return
611 model[path][0]=newtext
613 values = field.values
614 values[values.index(old)]=newtext
615 field.values = values
617 def on_jid_multi_add_button_clicked(self, widget, treeview, model, field):
618 #Default jid
619 jid = _('new@jabber.id')
620 if jid in field.values:
621 i = 1
622 while _('new%d@jabber.id') % i in field.values:
623 i += 1
624 jid = _('new%d@jabber.id') % i
625 iter_ = model.insert(999999, (jid,))
626 treeview.set_cursor(model.get_path(iter_), treeview.get_column(0), True)
627 field.values = field.values + [jid]
629 def on_jid_multi_edit_button_clicked(self, widget, treeview):
630 model, iter_ = treeview.get_selection().get_selected()
631 assert iter_ is not None
633 treeview.set_cursor(model.get_path(iter_), treeview.get_column(0), True)
635 def on_jid_multi_remove_button_clicked(self, widget, treeview, field):
636 selection = treeview.get_selection()
637 deleted = []
639 def remove(model, path, iter_, deleted):
640 deleted+=model[iter_]
641 model.remove(iter_)
643 selection.selected_foreach(remove, deleted)
644 field.values = (v for v in field.values if v not in deleted)
646 def on_jid_multi_clean_button_clicked(self, widget, model, field):
647 model.clear()
648 del field.values