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 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(),
203 self
.records_treeview
.append_column(
204 gtk
.TreeViewColumn(field
.label
, gtk
.CellRendererText(),
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'
218 self
.buttons_vbox
.set_no_show_all(True)
219 self
.buttons_vbox
.hide()
221 self
.buttons_vbox
.set_no_show_all(False)
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
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()
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)
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)
254 self
.up_button
.set_sensitive(False)
255 self
.down_button
.set_sensitive(True)
257 self
.up_button
.set_sensitive(True)
258 self
.down_button
.set_sensitive(True)
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)
266 self
.clear_button
.set_sensitive(False)
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
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
330 if field
.description
!= '':
331 if widget
.flags() & gtk
.NO_WINDOW
:
332 evbox
= gtk
.EventBox()
335 widget
.set_tooltip_text(field
.description
)
338 self
._data
_form
= dataform
343 # is the form changeable?
344 readwrite
= dataform
.type != 'result'
347 for field
in self
._data
_form
.iter_fields():
348 if field
.type == 'hidden': continue
351 commonlabelcenter
= False
355 if field
.type == 'boolean':
356 commonlabelcenter
= True
357 widget
= gtk
.CheckButton()
358 widget
.connect('toggled', self
.on_boolean_checkbutton_toggled
,
360 widget
.set_active(field
.value
)
362 elif field
.type == 'fixed':
365 if field
.label
is None:
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
382 for value
, label
in field
.iter_options():
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:
390 if field
.value
== '': # TODO: is None when done
392 if value
== field
.value
:
393 radio
.set_active(True)
394 widget
.pack_start(radio
, expand
=False)
396 # more than 5 options: show combobox
397 def on_list_single_combobox_changed(combobox
, f
):
398 iter_
= combobox
.get_active_iter()
400 model
= combobox
.get_model()
401 f
.value
= model
[iter_
][1]
404 widget
= gtkgui_helpers
.create_combobox(field
.options
,
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
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
)
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])
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
,
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':
441 widget
.connect('changed', self
.on_text_single_entry_changed
, field
)
442 widget
.set_text(field
.value
)
444 elif field
.type == 'jid-multi':
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
,
465 treeview
.append_column(gtk
.TreeViewColumn(None, renderer
,
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
)
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)
493 elif field
.type == 'text-private':
494 commonlabelcenter
= True
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
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)
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
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
,
531 widget
.set_sensitive(readwrite
)
532 if field
.value
is None:
534 widget
.set_text(field
.value
)
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,
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)
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/'):
558 img_data
= base64
.decodestring(uri
.uri_data
)
559 pixbuf_l
= gtk
.gdk
.PixbufLoader()
560 pixbuf_l
.write(img_data
)
562 media
= gtk
.image_new_from_pixbuf(pixbuf_l
.\
565 media
= gtk
.Label(_('Unable to load image'))
567 media
= gtk
.Label(_('Media type not supported: %s') % \
570 self
.attach(media
, 0, 1, linecounter
, linecounter
+1,
571 xoptions
=gtk
.FILL
, yoptions
=gtk
.FILL
)
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,
581 label
= gtk
.Label('*')
582 label
.set_tooltip_text(_('This field is required'))
583 self
.attach(label
, 2, 3, linecounter
, linecounter
+1, xoptions
=0,
587 if self
.get_property('visible'):
591 # simulate that we are one widget
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
):
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
,
621 newtext
= helpers
.parse_jid(newtext
)
622 except helpers
.InvalidFormat
, s
:
623 dialogs
.ErrorDialog(_('Invalid Jabber ID'), str(s
))
625 if newtext
in field
.values
:
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
)
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
):
639 jid
= _('new@jabber.id')
640 if jid
in field
.values
:
642 while _('new%d@jabber.id') % i
in field
.values
:
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()
659 def remove(model
, path
, iter_
, deleted
):
660 deleted
+=model
[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
):