2 ## src/dataforms_widget.py
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>
8 ## This file is part of Gajim.
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).'''
35 import common
.dataforms
as dataforms
36 from common
import helpers
40 class DataFormWidget(gtk
.Alignment
, object):
43 Data Form widget. Use like any other widget
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',
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
):
78 def set_data_form(self
, dataform
):
80 Set the data form (xmpp.DataForm) displayed in widget
82 assert isinstance(dataform
, dataforms
.DataForm
)
85 self
._data
_form
= dataform
86 if isinstance(dataform
, dataforms
.SimpleDataForm
):
87 self
.build_single_data_form()
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()
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')
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
122 title
= property(get_title
, None, None, 'Data form title')
125 ''' Treat 'us' as one widget. '''
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
):
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
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
167 self
.singleform
.destroy()
168 self
.clean_data_form
= self
.empty_method
# we won't call it twice
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...
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 self
.multiplemodel
.set_value(iter_
, fieldvars
.index(field
.var
),
199 # constructing columns...
200 for field
, counter
in zip(self
._data
_form
.reported
.iter_fields(),
202 self
.records_treeview
.append_column(
203 gtk
.TreeViewColumn(field
.label
, gtk
.CellRendererText(),
206 self
.records_treeview
.set_model(self
.multiplemodel
)
207 self
.records_treeview
.show_all()
209 self
.data_form_types_notebook
.set_current_page(
210 self
.data_form_types_notebook
.page_num(
211 self
.multiple_form_hbox
))
213 self
.clean_data_form
= self
.clean_multiple_data_form
215 readwrite
= self
._data
_form
.type != 'result'
217 self
.buttons_vbox
.set_no_show_all(True)
218 self
.buttons_vbox
.hide()
220 self
.buttons_vbox
.set_no_show_all(False)
222 self
.refresh_multiple_buttons()
224 def clean_multiple_data_form(self
):
226 Called as clean_data_form, read the docs of clean_data_form(). Remove
229 self
.clean_data_form
= self
.empty_method
# we won't call it twice
230 del self
.multiplemodel
232 def refresh_multiple_buttons(self
):
234 Checks for treeview state and makes control buttons sensitive
236 selection
= self
.records_treeview
.get_selection()
237 model
= self
.records_treeview
.get_model()
238 count
= selection
.count_selected_rows()
240 self
.remove_button
.set_sensitive(False)
241 self
.edit_button
.set_sensitive(False)
242 self
.up_button
.set_sensitive(False)
243 self
.down_button
.set_sensitive(False)
245 self
.remove_button
.set_sensitive(True)
246 self
.edit_button
.set_sensitive(True)
247 _
, (path
,) = selection
.get_selected_rows()
248 iter_
= model
.get_iter(path
)
249 if model
.iter_next(iter_
) is None:
250 self
.up_button
.set_sensitive(True)
251 self
.down_button
.set_sensitive(False)
253 self
.up_button
.set_sensitive(False)
254 self
.down_button
.set_sensitive(True)
256 self
.up_button
.set_sensitive(True)
257 self
.down_button
.set_sensitive(True)
259 self
.remove_button
.set_sensitive(True)
260 self
.edit_button
.set_sensitive(True)
261 self
.up_button
.set_sensitive(False)
262 self
.down_button
.set_sensitive(False)
265 self
.clear_button
.set_sensitive(False)
267 self
.clear_button
.set_sensitive(True)
269 def on_clear_button_clicked(self
, widget
):
270 self
.records_treeview
.get_model().clear()
272 def on_remove_button_clicked(self
, widget
):
273 selection
= self
.records_treeview
.get_selection()
274 model
, rowrefs
= selection
.get_selected_rows()
275 # rowref is a list of paths
276 for i
in xrange(len(rowrefs
)):
277 rowrefs
[i
] = gtk
.TreeRowReference(model
, rowrefs
[i
])
278 # rowref is a list of row references; need to convert because we will
279 # modify the model, paths would change
280 for rowref
in rowrefs
:
281 del model
[rowref
.get_path()]
283 def on_up_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 # constructing path for previous iter
288 previter
= model
.get_iter((path
[0]-1,))
289 model
.swap(iter_
, previter
)
291 self
.refresh_multiple_buttons()
293 def on_down_button_clicked(self
, widget
):
294 selection
= self
.records_treeview
.get_selection()
295 model
, (path
,) = selection
.get_selected_rows()
296 iter_
= model
.get_iter(path
)
297 nextiter
= model
.iter_next(iter_
)
298 model
.swap(iter_
, nextiter
)
300 self
.refresh_multiple_buttons()
302 def on_records_selection_changed(self
, widget
):
303 self
.refresh_multiple_buttons()
305 class SingleForm(gtk
.Table
, object):
307 Widget that represent DATAFORM_SINGLE mode form. Because this is used not
308 only to display single forms, but to form input windows of multiple-type
309 forms, it is in another class
313 validated
= (gobject
.SIGNAL_RUN_LAST | gobject
.SIGNAL_ACTION
, None, ())
316 def __init__(self
, dataform
):
317 assert isinstance(dataform
, dataforms
.SimpleDataForm
)
319 gtk
.Table
.__init
__(self
)
320 self
.set_col_spacings(12)
321 self
.set_row_spacings(6)
323 def decorate_with_tooltip(widget
, field
):
325 Adds a tooltip containing field's description to a widget. Creates
326 EventBox if widget doesn't have its own gdk window. Returns decorated
329 if field
.description
!= '':
330 if widget
.flags() & gtk
.NO_WINDOW
:
331 evbox
= gtk
.EventBox()
334 widget
.set_tooltip_text(field
.description
)
337 self
._data
_form
= dataform
342 # is the form changeable?
343 readwrite
= dataform
.type != 'result'
346 for field
in self
._data
_form
.iter_fields():
347 if field
.type == 'hidden': continue
350 commonlabelcenter
= False
354 if field
.type == 'boolean':
355 commonlabelcenter
= True
356 widget
= gtk
.CheckButton()
357 widget
.connect('toggled', self
.on_boolean_checkbutton_toggled
,
359 widget
.set_active(field
.value
)
361 elif field
.type == 'fixed':
364 if field
.label
is None:
369 widget
= gtk
.Label(field
.value
)
370 widget
.set_line_wrap(True)
371 self
.attach(widget
, leftattach
, rightattach
, linecounter
,
372 linecounter
+1, xoptions
=gtk
.FILL
, yoptions
=gtk
.FILL
)
374 elif field
.type == 'list-single':
375 # TODO: What if we have radio buttons and non-required field?
376 # TODO: We cannot deactivate them all...
377 if len(field
.options
) < 6:
378 # 5 option max: show radiobutton
381 for value
, label
in field
.iter_options():
384 radio
= gtk
.RadioButton(first_radio
, label
=label
)
385 radio
.connect('toggled',
386 self
.on_list_single_radiobutton_toggled
, field
, value
)
387 if first_radio
is None:
389 if field
.value
== '': # TODO: is None when done
391 if value
== field
.value
:
392 radio
.set_active(True)
393 widget
.pack_start(radio
, expand
=False)
395 # more than 5 options: show combobox
396 def on_list_single_combobox_changed(combobox
, f
):
397 iter_
= combobox
.get_active_iter()
399 model
= combobox
.get_model()
400 f
.value
= model
[iter_
][1]
403 widget
= gtkgui_helpers
.create_combobox(field
.options
,
405 widget
.connect('changed', on_list_single_combobox_changed
, field
)
406 widget
.set_sensitive(readwrite
)
408 elif field
.type == 'list-multi':
409 # TODO: When more than few choices, make a list
410 if len(field
.options
) < 6:
411 # 5 option max: show checkbutton
413 for value
, label
in field
.iter_options():
414 check
= gtk
.CheckButton(label
, use_underline
=False)
415 check
.set_active(value
in field
.values
)
416 check
.connect('toggled',
417 self
.on_list_multi_checkbutton_toggled
, field
, value
)
418 widget
.pack_start(check
, expand
=False)
419 widget
.set_sensitive(readwrite
)
421 # more than 5 options: show combobox
422 def on_list_multi_treeview_changed(selection
, f
):
423 def for_selected(treemodel
, path
, iter):
424 vals
.append(treemodel
[iter][1])
426 selection
.selected_foreach(for_selected
)
427 field
.values
= vals
[:]
428 widget
= gtk
.ScrolledWindow()
429 widget
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
430 tv
= gtkgui_helpers
.create_list_multi(field
.options
,
433 widget
.set_size_request(-1, 120)
434 tv
.get_selection().connect('changed',
435 on_list_multi_treeview_changed
, field
)
436 tv
.set_sensitive(readwrite
)
438 elif field
.type == 'jid-single':
440 widget
.connect('changed', self
.on_text_single_entry_changed
, field
)
441 widget
.set_text(field
.value
)
443 elif field
.type == 'jid-multi':
446 xml
= gtkgui_helpers
.get_gtk_builder('data_form_window.ui',
447 'multiple_form_hbox')
448 widget
= xml
.get_object('multiple_form_hbox')
449 treeview
= xml
.get_object('records_treeview')
451 listmodel
= gtk
.ListStore(str)
452 for value
in field
.iter_values():
453 # nobody will create several megabytes long stanza
454 listmodel
.insert(999999, (value
,))
456 treeview
.set_model(listmodel
)
458 renderer
= gtk
.CellRendererText()
459 renderer
.set_property('editable', True)
460 renderer
.connect('edited',
461 self
.on_jid_multi_cellrenderertext_edited
, treeview
, listmodel
,
464 treeview
.append_column(gtk
.TreeViewColumn(None, renderer
,
467 decorate_with_tooltip(treeview
, field
)
469 add_button
=xml
.get_object('add_button')
470 add_button
.connect('clicked',
471 self
.on_jid_multi_add_button_clicked
, treeview
, listmodel
, field
)
472 edit_button
=xml
.get_object('edit_button')
473 edit_button
.connect('clicked',
474 self
.on_jid_multi_edit_button_clicked
, treeview
)
475 remove_button
=xml
.get_object('remove_button')
476 remove_button
.connect('clicked',
477 self
.on_jid_multi_remove_button_clicked
, treeview
, field
)
478 clear_button
=xml
.get_object('clear_button')
479 clear_button
.connect('clicked',
480 self
.on_jid_multi_clean_button_clicked
, listmodel
, field
)
482 add_button
.set_no_show_all(True)
483 edit_button
.set_no_show_all(True)
484 remove_button
.set_no_show_all(True)
485 clear_button
.set_no_show_all(True)
487 widget
.set_sensitive(readwrite
)
488 self
.attach(widget
, 1, 2, linecounter
, linecounter
+1)
492 elif field
.type == 'text-private':
493 commonlabelcenter
= True
495 widget
.connect('changed', self
.on_text_single_entry_changed
, field
)
496 widget
.set_visibility(False)
497 widget
.set_text(field
.value
)
499 elif field
.type == 'text-multi':
500 # TODO: bigger text view
503 textwidget
= gtk
.TextView()
504 textwidget
.set_wrap_mode(gtk
.WRAP_WORD
)
505 textwidget
.get_buffer().connect('changed',
506 self
.on_text_multi_textbuffer_changed
, field
)
507 textwidget
.get_buffer().set_text(field
.value
)
508 textwidget
.set_sensitive(readwrite
)
510 widget
= gtk
.ScrolledWindow()
511 widget
.add(textwidget
)
513 widget
=decorate_with_tooltip(widget
, field
)
514 self
.attach(widget
, 1, 2, linecounter
, linecounter
+1)
517 # field.type == 'text-single' or field.type is nonstandard:
518 # JEP says that if we don't understand some type, we
519 # should handle it as text-single
520 commonlabelcenter
= True
523 def kpe(widget
, event
):
524 if event
.keyval
== gtk
.keysyms
.Return
or \
525 event
.keyval
== gtk
.keysyms
.KP_Enter
:
526 self
.emit('validated')
527 widget
.connect('key-press-event', kpe
)
528 widget
.connect('changed', self
.on_text_single_entry_changed
,
530 widget
.set_sensitive(readwrite
)
531 if field
.value
is None:
533 widget
.set_text(field
.value
)
536 widget
= gtk
.Label(field
.value
)
537 widget
.set_sensitive(True)
538 widget
.set_alignment(0.0, 0.5)
539 widget
=decorate_with_tooltip(widget
, field
)
540 self
.attach(widget
, 1, 2, linecounter
, linecounter
+1,
543 if commonlabel
and field
.label
is not None:
544 label
= gtk
.Label(field
.label
)
545 if commonlabelcenter
:
546 label
.set_alignment(0.0, 0.5)
548 label
.set_alignment(0.0, 0.0)
549 label
= decorate_with_tooltip(label
, field
)
550 self
.attach(label
, 0, 1, linecounter
, linecounter
+1,
551 xoptions
=gtk
.FILL
, yoptions
=gtk
.FILL
)
553 if field
.media
is not None:
554 for uri
in field
.media
.uris
:
555 if uri
.type_
.startswith('image/'):
557 img_data
= base64
.decodestring(uri
.uri_data
)
558 pixbuf_l
= gtk
.gdk
.PixbufLoader()
559 pixbuf_l
.write(img_data
)
561 media
= gtk
.image_new_from_pixbuf(pixbuf_l
.\
564 media
= gtk
.Label(_('Unable to load image'))
566 media
= gtk
.Label(_('Media type not supported: %s') % \
569 self
.attach(media
, 0, 1, linecounter
, linecounter
+1,
570 xoptions
=gtk
.FILL
, yoptions
=gtk
.FILL
)
573 assert widget
is not None
574 widget
.set_sensitive(readwrite
)
575 widget
= decorate_with_tooltip(widget
, field
)
576 self
.attach(widget
, 1, 2, linecounter
, linecounter
+1,
580 label
= gtk
.Label('*')
581 label
.set_tooltip_text(_('This field is required'))
582 self
.attach(label
, 2, 3, linecounter
, linecounter
+1, xoptions
=0,
586 if self
.get_property('visible'):
590 # simulate that we are one widget
593 def on_boolean_checkbutton_toggled(self
, widget
, field
):
594 field
.value
= widget
.get_active()
596 def on_list_single_radiobutton_toggled(self
, widget
, field
, value
):
599 def on_list_multi_checkbutton_toggled(self
, widget
, field
, value
):
600 # TODO: make some methods like add_value and remove_value
601 if widget
.get_active() and value
not in field
.values
:
602 field
.values
+= [value
]
603 elif not widget
.get_active() and value
in field
.values
:
604 field
.values
= [v
for v
in field
.values
if v
!=value
]
606 def on_text_single_entry_changed(self
, widget
, field
):
607 field
.value
= widget
.get_text()
609 def on_text_multi_textbuffer_changed(self
, widget
, field
):
610 field
.value
= widget
.get_text(
611 widget
.get_start_iter(),
612 widget
.get_end_iter())
614 def on_jid_multi_cellrenderertext_edited(self
, cell
, path
, newtext
, treeview
,
620 newtext
= helpers
.parse_jid(newtext
)
621 except helpers
.InvalidFormat
, s
:
622 dialogs
.ErrorDialog(_('Invalid Jabber ID'), str(s
))
624 if newtext
in field
.values
:
626 _('Jabber ID already in list'),
627 _('The Jabber ID you entered is already in the list. Choose another one.'))
628 gobject
.idle_add(treeview
.set_cursor
, path
)
630 model
[path
][0]=newtext
632 values
= field
.values
633 values
[values
.index(old
)]=newtext
634 field
.values
= values
636 def on_jid_multi_add_button_clicked(self
, widget
, treeview
, model
, field
):
638 jid
= _('new@jabber.id')
639 if jid
in field
.values
:
641 while _('new%d@jabber.id') % i
in field
.values
:
643 jid
= _('new%d@jabber.id') % i
644 iter_
= model
.insert(999999, (jid
,))
645 treeview
.set_cursor(model
.get_path(iter_
), treeview
.get_column(0), True)
646 field
.values
= field
.values
+ [jid
]
648 def on_jid_multi_edit_button_clicked(self
, widget
, treeview
):
649 model
, iter_
= treeview
.get_selection().get_selected()
650 assert iter_
is not None
652 treeview
.set_cursor(model
.get_path(iter_
), treeview
.get_column(0), True)
654 def on_jid_multi_remove_button_clicked(self
, widget
, treeview
, field
):
655 selection
= treeview
.get_selection()
658 def remove(model
, path
, iter_
, deleted
):
659 deleted
+=model
[iter_
]
662 selection
.selected_foreach(remove
, deleted
)
663 field
.values
= (v
for v
in field
.values
if v
not in deleted
)
665 def on_jid_multi_clean_button_clicked(self
, widget
, model
, field
):