# Copyright (C) 2003-2014 Yann Leboulanger # Copyright (C) 2006 Tomasz Melcer # Copyright (C) 2006-2007 Jean-Marie Traissard # # This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . ''' This module contains widget that can display data form (XEP-0004). Words single and multiple refers here to types of data forms: single means these with one record of data (without element), multiple - these which may contain more data (with element).''' import itertools import base64 from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import GObject from gi.repository import GLib from gajim import gtkgui_helpers from gajim.common.modules import dataforms from gajim.common import helpers from gajim.common import app class DataFormWidget(Gtk.Alignment): # "public" interface """ Data Form widget. Use like any other widget """ __gsignals__ = dict(validated=( GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, ())) def __init__(self, dataformnode=None): ''' Create a widget. ''' GObject.GObject.__init__(self, xscale=1.0, yscale=1.0) self._data_form = None self.selectable = False self.clean_cb = None self.xml = gtkgui_helpers.get_gtk_builder('data_form_window.ui', 'data_form_vbox') self.xml.connect_signals(self) for name in ('instructions_label', 'instructions_hseparator', 'single_form_viewport', 'data_form_types_notebook', 'single_form_scrolledwindow', 'multiple_form_hbox', 'records_treeview', 'buttons_vbox', 'add_button', 'remove_button', 'edit_button', 'up_button', 'down_button', 'clear_button'): self.__dict__[name] = self.xml.get_object(name) self.add(self.xml.get_object('data_form_vbox')) if dataformnode is not None: self.set_data_form(dataformnode) selection = self.records_treeview.get_selection() selection.connect('changed', self.on_records_selection_changed) selection.set_mode(Gtk.SelectionMode.MULTIPLE) def on_data_form_vbox_key_press_event(self, widget, event): print('key pressed') def set_data_form(self, dataform): """ Set the data form (nbxmpp.DataForm) displayed in widget """ assert isinstance(dataform, dataforms.DataForm) self.del_data_form() self._data_form = dataform if isinstance(dataform, dataforms.SimpleDataForm): self.build_single_data_form() else: self.build_multiple_data_form() # create appropriate description for instructions field if there isn't any if dataform.instructions == '': self.instructions_label.set_no_show_all(True) self.instructions_label.hide() else: self.instructions_label.set_text(dataform.instructions) gtkgui_helpers.label_set_autowrap(self.instructions_label) def get_data_form(self): """ Data form displayed in the widget or None if no form """ return self._data_form def del_data_form(self): self.clean_data_form() self._data_form = None data_form = property(get_data_form, set_data_form, del_data_form, 'Data form presented in a widget') def get_title(self): """ Get the title of data form. If no title or no form, returns ''. Useful for setting window title """ if self._data_form is not None: if self._data_form.title is not None: return self._data_form.title return '' title = property(get_title, None, None, 'Data form title') def show(self): ''' Treat 'us' as one widget. ''' self.show_all() # "private" methods # we have actually two different kinds of data forms: one is a simple form to fill, # second is a table with several records; def empty_method(self): pass def clean_data_form(self): """ Remove data about existing form. This metod is empty, because it is rewritten by build_*_data_form, according to type of form which is actually displayed """ if self.clean_cb: self.clean_cb() def build_single_data_form(self): '''Invoked when new single form is to be created.''' assert isinstance(self._data_form, dataforms.SimpleDataForm) self.clean_data_form() self.singleform = SingleForm(self._data_form, selectable=self.selectable) def _on_validated(widget): self.emit('validated') self.singleform.connect('validated', _on_validated) self.singleform.show() self.single_form_viewport.add(self.singleform) self.data_form_types_notebook.set_current_page( self.data_form_types_notebook.page_num( self.single_form_scrolledwindow)) self.clean_cb = self.clean_single_data_form def clean_single_data_form(self): """ Called as clean_data_form, read the docs of clean_data_form(). Remove form from widget """ self.singleform.destroy() self.clean_cb = None # we won't call it twice del self.singleform def build_multiple_data_form(self): """ Invoked when new multiple form is to be created """ assert isinstance(self._data_form, dataforms.MultipleDataForm) self.clean_data_form() # creating model for form... fieldtypes = [] fieldvars = [] for field in self._data_form.reported.iter_fields(): # note: we store also text-private and hidden fields, # we just do not display them. # TODO: boolean fields #elif field.type_=='boolean': fieldtypes.append(bool) fieldtypes.append(str) fieldvars.append(field.var) self.multiplemodel = Gtk.ListStore(*fieldtypes) # moving all data to model for item in self._data_form.iter_records(): iter_ = self.multiplemodel.append() for field in item.iter_fields(): if field.var in fieldvars: self.multiplemodel.set_value(iter_, fieldvars.index(field.var), field.value) # constructing columns... for field, counter in zip(self._data_form.reported.iter_fields(), itertools.count()): self.records_treeview.append_column( Gtk.TreeViewColumn(field.label, Gtk.CellRendererText(), text=counter)) self.records_treeview.set_model(self.multiplemodel) self.records_treeview.show_all() self.data_form_types_notebook.set_current_page( self.data_form_types_notebook.page_num( self.multiple_form_hbox)) self.clean_cb = self.clean_multiple_data_form readwrite = self._data_form.type_ != 'result' if not readwrite: self.buttons_vbox.set_no_show_all(True) self.buttons_vbox.hide() else: self.buttons_vbox.set_no_show_all(False) # refresh list look self.refresh_multiple_buttons() def clean_multiple_data_form(self): """ Called as clean_data_form, read the docs of clean_data_form(). Remove form from widget """ self.clean_cb = None # we won't call it twice del self.multiplemodel def refresh_multiple_buttons(self): """ Checks for treeview state and makes control buttons sensitive """ selection = self.records_treeview.get_selection() model = self.records_treeview.get_model() count = selection.count_selected_rows() if count == 0: self.remove_button.set_sensitive(False) self.edit_button.set_sensitive(False) self.up_button.set_sensitive(False) self.down_button.set_sensitive(False) elif count == 1: self.remove_button.set_sensitive(True) self.edit_button.set_sensitive(True) _, (path,) = selection.get_selected_rows() iter_ = model.get_iter(path) if model.iter_next(iter_) is None: self.up_button.set_sensitive(True) self.down_button.set_sensitive(False) elif path == (0, ): self.up_button.set_sensitive(False) self.down_button.set_sensitive(True) else: self.up_button.set_sensitive(True) self.down_button.set_sensitive(True) else: self.remove_button.set_sensitive(True) self.edit_button.set_sensitive(True) self.up_button.set_sensitive(False) self.down_button.set_sensitive(False) if not model: self.clear_button.set_sensitive(False) else: self.clear_button.set_sensitive(True) def on_clear_button_clicked(self, widget): self.records_treeview.get_model().clear() def on_remove_button_clicked(self, widget): selection = self.records_treeview.get_selection() model, rowrefs = selection.get_selected_rows() # rowref is a list of paths for i in range(len(rowrefs)): rowrefs[i] = Gtk.TreeRowReference.new(model, rowrefs[i]) # rowref is a list of row references; need to convert because we will # modify the model, paths would change for rowref in rowrefs: del model[rowref.get_path()] def on_up_button_clicked(self, widget): selection = self.records_treeview.get_selection() model, (path,) = selection.get_selected_rows() iter_ = model.get_iter(path) # constructing path for previous iter previter = model.get_iter((path[0]-1,)) model.swap(iter_, previter) self.refresh_multiple_buttons() def on_down_button_clicked(self, widget): selection = self.records_treeview.get_selection() model, (path,) = selection.get_selected_rows() iter_ = model.get_iter(path) nextiter = model.iter_next(iter_) model.swap(iter_, nextiter) self.refresh_multiple_buttons() def on_records_selection_changed(self, widget): self.refresh_multiple_buttons() class SingleForm(Gtk.Table): """ Widget that represent DATAFORM_SINGLE mode form. Because this is used not only to display single forms, but to form input windows of multiple-type forms, it is in another class """ __gsignals__ = dict(validated=( GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, ()) ) def __init__(self, dataform, selectable=False): assert isinstance(dataform, dataforms.SimpleDataForm) GObject.GObject.__init__(self) self.set_col_spacings(12) self.set_row_spacings(6) def decorate_with_tooltip(widget, field): """ Adds a tooltip containing field's description to a widget. Creates EventBox if widget doesn't have its own gdk window. Returns decorated widget """ if field.description != '': if not widget.get_window(): #if widget.flags() & Gtk.NO_WINDOW: evbox = Gtk.EventBox() evbox.add(widget) widget = evbox widget.set_tooltip_text(field.description) return widget self._data_form = dataform # building widget linecounter = 0 # is the form changeable? readwrite = dataform.type_ != 'result' # for each field... for field in self._data_form.iter_fields(): if field.type_ == 'hidden': continue commonlabel = True commonlabelcenter = False commonwidget = True widget = None if field.type_ == 'boolean': commonlabelcenter = True widget = Gtk.CheckButton() widget.connect('toggled', self.on_boolean_checkbutton_toggled, field) widget.set_active(field.value) elif field.type_ == 'fixed': leftattach = 1 rightattach = 2 if field.label is None: commonlabel = False leftattach = 0 commonwidget = False widget = Gtk.Label(label=field.value) widget.set_property('selectable', selectable) widget.set_line_wrap(True) self.attach(widget, leftattach, rightattach, linecounter, linecounter + 1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL) elif field.type_ == 'list-single': # TODO: What if we have radio buttons and non-required field? # TODO: We cannot deactivate them all... if len(field.options) < 6: # 5 option max: show radiobutton widget = Gtk.VBox() first_radio = None for value, label in field.iter_options(): if not label: label = value radio = Gtk.RadioButton.new_with_label_from_widget( first_radio, label) radio.connect('toggled', self.on_list_single_radiobutton_toggled, field, value) if first_radio is None: first_radio = radio if field.value == '': # TODO: is None when done field.value = value if value == field.value: radio.set_active(True) widget.pack_start(radio, False, True, 0) else: # more than 5 options: show combobox def on_list_single_combobox_changed(combobox, f): iter_ = combobox.get_active_iter() if iter_: model = combobox.get_model() f.value = model[iter_][1] else: f.value = '' widget = gtkgui_helpers.create_combobox(field.options, field.value) widget.connect('changed', on_list_single_combobox_changed, field) widget.set_sensitive(readwrite) elif field.type_ == 'list-multi': # TODO: When more than few choices, make a list if len(field.options) < 6: # 5 option max: show checkbutton widget = Gtk.VBox() for value, label in field.iter_options(): check = Gtk.CheckButton(label=label, use_underline=False) check.set_active(value in field.values) check.connect('toggled', self.on_list_multi_checkbutton_toggled, field, value) widget.pack_start(check, False, True, 0) widget.set_sensitive(readwrite) else: # more than 5 options: show combobox def on_list_multi_treeview_changed(selection, f): def for_selected(treemodel, path, iter): vals.append(treemodel[iter][1]) vals = [] selection.selected_foreach(for_selected) field.values = vals[:] widget = Gtk.ScrolledWindow() widget.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) tv = gtkgui_helpers.create_list_multi(field.options, field.values) widget.add(tv) widget.set_size_request(-1, 120) tv.get_selection().connect('changed', on_list_multi_treeview_changed, field) tv.set_sensitive(readwrite) elif field.type_ == 'jid-single': widget = Gtk.Entry() widget.connect('changed', self.on_text_single_entry_changed, field) widget.set_text(field.value) elif field.type_ == 'jid-multi': commonwidget = False xml = gtkgui_helpers.get_gtk_builder('data_form_window.ui', 'multiple_form_hbox') widget = xml.get_object('multiple_form_hbox') treeview = xml.get_object('records_treeview') listmodel = Gtk.ListStore(str) for value in field.iter_values(): # nobody will create several megabytes long stanza listmodel.insert(999999, (value,)) treeview.set_model(listmodel) renderer = Gtk.CellRendererText() renderer.set_property('editable', True) renderer.connect('edited', self.on_jid_multi_cellrenderertext_edited, treeview, listmodel, field) treeview.append_column(Gtk.TreeViewColumn(None, renderer, text=0)) decorate_with_tooltip(treeview, field) add_button = xml.get_object('add_button') add_button.connect('clicked', self.on_jid_multi_add_button_clicked, treeview, listmodel, field) edit_button = xml.get_object('edit_button') edit_button.connect('clicked', self.on_jid_multi_edit_button_clicked, treeview) remove_button = xml.get_object('remove_button') remove_button.connect('clicked', self.on_jid_multi_remove_button_clicked, treeview, field) clear_button = xml.get_object('clear_button') clear_button.connect('clicked', self.on_jid_multi_clean_button_clicked, listmodel, field) if not readwrite: add_button.set_no_show_all(True) edit_button.set_no_show_all(True) remove_button.set_no_show_all(True) clear_button.set_no_show_all(True) widget.set_sensitive(readwrite) self.attach(widget, 1, 2, linecounter, linecounter+1) del xml elif field.type_ == 'text-private': commonlabelcenter = True widget = Gtk.Entry() widget.connect('changed', self.on_text_single_entry_changed, field) widget.set_visibility(False) widget.set_text(field.value) elif field.type_ == 'text-multi': # TODO: bigger text view commonwidget = False textwidget = Gtk.TextView() textwidget.set_wrap_mode(Gtk.WrapMode.WORD) textwidget.get_buffer().connect('changed', self.on_text_multi_textbuffer_changed, field) textwidget.get_buffer().set_text(field.value) if readwrite: textwidget.set_sensitive(True) else: if selectable: textwidget.set_editable(True) else: textwidget.set_sensitive(False) widget = Gtk.ScrolledWindow() widget.add(textwidget) widget = decorate_with_tooltip(widget, field) self.attach(widget, 1, 2, linecounter, linecounter+1) else: # field.type_ == 'text-single' or field.type_ is nonstandard: # JEP says that if we don't understand some type, we # should handle it as text-single commonlabelcenter = True if readwrite: widget = Gtk.Entry() def kpe(widget, event): if event.keyval == Gdk.KEY_Return or \ event.keyval == Gdk.KEY_KP_Enter: self.emit('validated') widget.connect('key-press-event', kpe) widget.connect('changed', self.on_text_single_entry_changed, field) widget.set_sensitive(readwrite) if field.value is None: field.value = '' widget.set_text(field.value) else: commonwidget = False widget = Gtk.Label(label=field.value) widget.set_property('selectable', selectable) widget.set_sensitive(True) widget.set_halign(Gtk.Align.START) widget.set_valign(Gtk.Align.CENTER) widget = decorate_with_tooltip(widget, field) self.attach(widget, 1, 2, linecounter, linecounter+1, yoptions=Gtk.AttachOptions.FILL) if commonlabel and field.label is not None: label = Gtk.Label(label=field.label) if commonlabelcenter: label.set_halign(Gtk.Align.START) label.set_valign(Gtk.Align.CENTER) else: label.set_halign(Gtk.Align.START) label.set_valign(Gtk.Align.START) label = decorate_with_tooltip(label, field) self.attach(label, 0, 1, linecounter, linecounter+1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL) if field.media is not None: for uri in field.media.uris: if uri.type_.startswith('image/'): try: img_data = base64.b64decode(uri.uri_data) pixbuf_l = GdkPixbuf.PixbufLoader() pixbuf_l.write(img_data) pixbuf_l.close() media = Gtk.Image.new_from_pixbuf(pixbuf_l.\ get_pixbuf()) except Exception: media = Gtk.Label(label=_('Unable to load image')) else: media = Gtk.Label(label=_('Media type not supported: %s') % \ uri.type_) linecounter += 1 self.attach(media, 0, 1, linecounter, linecounter+1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL) if commonwidget: assert widget is not None widget.set_sensitive(readwrite) widget = decorate_with_tooltip(widget, field) self.attach(widget, 1, 2, linecounter, linecounter+1, yoptions=Gtk.AttachOptions.FILL) if field.required: label = Gtk.Label(label='*') label.set_tooltip_text(_('This field is required')) self.attach(label, 2, 3, linecounter, linecounter+1, xoptions=0, yoptions=0) linecounter += 1 if self.get_property('visible'): self.show_all() def show(self): # simulate that we are one widget self.show_all() def on_boolean_checkbutton_toggled(self, widget, field): field.value = widget.get_active() def on_list_single_radiobutton_toggled(self, widget, field, value): field.value = value def on_list_multi_checkbutton_toggled(self, widget, field, value): # TODO: make some methods like add_value and remove_value if widget.get_active() and value not in field.values: field.values += [value] elif not widget.get_active() and value in field.values: field.values = [v for v in field.values if v != value] def on_text_single_entry_changed(self, widget, field): field.value = widget.get_text() def on_text_multi_textbuffer_changed(self, widget, field): field.value = widget.get_text(widget.get_start_iter(), widget.get_end_iter(), True) def on_jid_multi_cellrenderertext_edited(self, cell, path, newtext, treeview, model, field): old = model[path][0] if old == newtext: return try: newtext = helpers.parse_jid(newtext) except helpers.InvalidFormat as s: app.interface.raise_dialog('invalid-jid-with-error', str(s)) return if newtext in field.values: app.interface.raise_dialog('jid-in-list') GLib.idle_add(treeview.set_cursor, path) return model[path][0] = newtext values = field.values values[values.index(old)] = newtext field.values = values def on_jid_multi_add_button_clicked(self, widget, treeview, model, field): #Default jid jid = _('new@jabber.id') if jid in field.values: i = 1 while _('new%d@jabber.id') % i in field.values: i += 1 jid = _('new%d@jabber.id') % i iter_ = model.insert(999999, (jid,)) treeview.set_cursor(model.get_path(iter_), treeview.get_column(0), True) field.values = field.values + [jid] def on_jid_multi_edit_button_clicked(self, widget, treeview): model, iter_ = treeview.get_selection().get_selected() assert iter_ is not None treeview.set_cursor(model.get_path(iter_), treeview.get_column(0), True) def on_jid_multi_remove_button_clicked(self, widget, treeview, field): selection = treeview.get_selection() deleted = [] def remove(model, path, iter_, deleted): deleted += model[iter_] model.remove(iter_) selection.selected_foreach(remove, deleted) field.values = (v for v in field.values if v not in deleted) def on_jid_multi_clean_button_clicked(self, widget, model, field): model.clear() del field.values