diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index 0d8689de1..510d4f751 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -161,3 +161,9 @@ button.flat.link { padding: 0; border: 0; } .success-color { color: @success_color; } .error-color { color: @error_color; } .warning-color { color: @warning_color; } + +/*Dataforms*/ +.field-fixed { font-size: 16px; font-weight: bold; padding-top:5px;} +.data-form-title { font-size: 16px; font-weight: bold; } +.data-form-widget scrolledwindow { border: 1px solid; border-color:@unfocused_borders; } +.data-form-widget treeview { padding: 5px; } diff --git a/gajim/gtk/dataform.py b/gajim/gtk/dataform.py new file mode 100644 index 000000000..b9ad2215b --- /dev/null +++ b/gajim/gtk/dataform.py @@ -0,0 +1,377 @@ +# 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 . + +from gi.repository import Gtk +from gi.repository import Pango + + +class DataFormWidget(Gtk.ScrolledWindow): + def __init__(self, form_node): + Gtk.ScrolledWindow.__init__(self) + self.set_hexpand(True) + self.set_vexpand(True) + self.get_style_context().add_class('data-form-widget') + + self._form_node = form_node + + self._form_grid = FormGrid(form_node) + + self.add(self._form_grid) + + @property + def title(self): + return self._form_grid.title + + @property + def instructions(self): + return self._form_grid.instructions + + def get_submit_form(self): + self._form_node.type_ = 'submit' + return self._form_node + + +class FormGrid(Gtk.Grid): + def __init__(self, form_node): + Gtk.Grid.__init__(self) + self.set_column_spacing(12) + self.set_row_spacing(12) + self.set_halign(Gtk.Align.CENTER) + self.row_count = 0 + self.rows = [] + + self._data_form = form_node + + self.title = None + self.instructions = None + + self._fields = { + 'boolean': BooleanField, + 'fixed': FixedField, + 'list-single': ListSingleField, + 'list-multi': ListMultiField, + 'jid-single': JidSingleField, + 'jid-multi': JidMultiField, + 'text-single': TextSingleField, + 'text-private': TextPrivateField, + 'text-multi': TextMultiField + } + + if form_node.title is not None: + self.title = form_node.title + self.add_row(Title(form_node.title)) + if form_node.instructions is not None: + self.instructions = form_node.instructions + self.add_row(Instructions(form_node.instructions)) + + self.parse_form(form_node) + + def add_row(self, field): + field.add(self, self.row_count) + self.row_count += 1 + self.rows.append(field) + + def parse_form(self, form_node): + for field in form_node.iter_fields(): + if field.type_ == 'hidden': + continue + + widget = self._fields[field.type_] + self.add_row(widget(field)) + + def is_valid(self): + return self._data_form.is_valid() + + +class Title: + def __init__(self, title): + self._label = Gtk.Label(label=title) + self._label.get_style_context().add_class('data-form-title') + + def add(self, form_grid, row_number): + form_grid.attach(self._label, 0, row_number, 2, 1) + + +class Instructions: + def __init__(self, instructions): + self._label = Gtk.Label(label=instructions) + self._label.set_line_wrap(True) + self._label.set_line_wrap_mode(Pango.WrapMode.WORD) + + def add(self, form_grid, row_number): + form_grid.attach(self._label, 0, row_number, 2, 1) + + +class Field: + def __init__(self, field): + self._field = field + + self._label = Gtk.Label(label=field.label) + self._label.set_single_line_mode(False) + self._label.set_line_wrap(True) + self._label.set_line_wrap_mode(Pango.WrapMode.WORD) + self._label.set_width_chars(15) + self._label.set_size_request(100, -1) + self._label.set_xalign(1) + self._label.set_tooltip_text(field.description) + + def add(self, form_grid, row_number): + form_grid.attach(self._label, 0, row_number, 1, 1) + form_grid.attach_next_to( + self._widget, self._label, Gtk.PositionType.RIGHT, 1, 1) + + +class BooleanField(Field): + def __init__(self, field): + Field.__init__(self, field) + + self._widget = Gtk.CheckButton() + self._widget.set_active(field.value) + self._widget.connect('toggled', self._toggled) + + def _toggled(self, widget): + self._field.value = self._widget.get_active() + + +class FixedField(Field): + def __init__(self, field): + Field.__init__(self, field) + + self._label.set_text(field.value) + + # If the value is more than 30 chars it proabably isnt + # meant as a section header + if len(field.value) < 30: + self._label.get_style_context().add_class('field-fixed') + else: + self._label.set_xalign(0.5) + + def add(self, form_grid, row_number): + if len(self._field.value) < 30: + form_grid.attach(self._label, 0, row_number, 1, 1) + else: + form_grid.attach(self._label, 0, row_number, 2, 1) + + +class ListSingleField(Field): + def __init__(self, field): + Field.__init__(self, field) + + self._widget = Gtk.ComboBoxText() + for value, label in field.iter_options(): + if not label: + label = value + self._widget.append(value, label) + + self._widget.set_active_id(field.value) + self._widget.connect('changed', self._changed) + + def _changed(self, widget): + self._field.value = widget.get_active_id() + + +class ListMultiField(Field): + def __init__(self, field): + Field.__init__(self, field) + self._label.set_valign(Gtk.Align.START) + + self._treeview = ListMutliTreeView(field) + + self._widget = Gtk.ScrolledWindow() + self._widget.set_propagate_natural_height(True) + self._widget.set_min_content_height(100) + self._widget.set_max_content_height(300) + self._widget.add(self._treeview) + + +class ListMutliTreeView(Gtk.TreeView): + def __init__(self, field): + Gtk.TreeView.__init__(self) + + self._field = field + + self._store = Gtk.ListStore(str, str, bool) + + col = Gtk.TreeViewColumn() + cell = Gtk.CellRendererText() + col.pack_start(cell, True) + col.set_attributes(cell, text=0) + self.append_column(col) + + col = Gtk.TreeViewColumn() + cell = Gtk.CellRendererToggle() + cell.set_activatable(True) + cell.connect('toggled', self._toggled) + col.pack_start(cell, True) + col.set_attributes(cell, active=2) + self.append_column(col) + + self.set_headers_visible(False) + + for option in field.options: + # option = (label, value) + self._store.append( + [*option, option[1] in field.values]) + + self.set_model(self._store) + + def _toggled(self, renderer, path): + iter_ = self._store.get_iter(path) + current_value = self._store[iter_][2] + self._store.set_value(iter_, 2, not current_value) + self._set_values() + + def _set_values(self): + values = [] + for row in self.get_model(): + if not row[2]: + continue + values.append(row[1]) + self._field.values = values + + +class JidMultiField(Field): + def __init__(self, field): + Field.__init__(self, field) + self._label.set_valign(Gtk.Align.START) + + self._treeview = JidMutliTreeView(field) + + self._add_button = Gtk.Button.new_from_icon_name( + 'list-add-symbolic', Gtk.IconSize.MENU) + self._add_button.connect('clicked', self._add_clicked) + self._add_button.set_halign(Gtk.Align.START) + + self._remove_button = Gtk.Button.new_from_icon_name( + 'list-remove-symbolic', Gtk.IconSize.MENU) + self._remove_button.connect('clicked', self._remove_clicked) + self._remove_button.set_halign(Gtk.Align.START) + + self._button_box = Gtk.ButtonBox( + orientation=Gtk.Orientation.HORIZONTAL) + self._button_box.set_layout(Gtk.ButtonBoxStyle.START) + self._button_box.add(self._add_button) + self._button_box.add(self._remove_button) + self._button_box.set_child_non_homogeneous(self._add_button, True) + self._button_box.set_child_non_homogeneous(self._remove_button, True) + + self._widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self._widget.set_spacing(6) + + self._scrolled_window = Gtk.ScrolledWindow() + self._scrolled_window.set_propagate_natural_height(True) + self._scrolled_window.set_min_content_height(100) + self._scrolled_window.set_max_content_height(300) + self._scrolled_window.add(self._treeview) + + self._widget.pack_start(self._scrolled_window, True, True, 0) + self._widget.pack_end(self._button_box, False, False, 0) + + def _add_clicked(self, widget): + self._treeview.get_model().append(['']) + + def _remove_clicked(self, widget): + mod, paths = self._treeview.get_selection().get_selected_rows() + for path in paths: + iter_ = mod.get_iter(path) + self._treeview.get_model().remove(iter_) + + jids = [] + for row in self._treeview.get_model(): + if not row[0]: + continue + jids.append(row[0]) + self._field.values = jids + + +class JidMutliTreeView(Gtk.TreeView): + def __init__(self, field): + Gtk.TreeView.__init__(self) + + self._field = field + + self._store = Gtk.ListStore(str) + + col = Gtk.TreeViewColumn() + cell = Gtk.CellRendererText() + cell.set_property('editable', True) + cell.set_property('placeholder-text', 'user@example.org') + cell.connect('edited', self._jid_edited) + col.pack_start(cell, True) + col.set_attributes(cell, text=0) + self.append_column(col) + + self.set_headers_visible(False) + + for value in field.values: + self._store.append([value]) + + self.set_model(self._store) + + def _jid_edited(self, renderer, path, new_text): + iter_ = self._store.get_iter(path) + self._store.set_value(iter_, 0, new_text) + self._set_values() + + def _set_values(self): + jids = [] + for row in self._store: + if not row[0]: + continue + jids.append(row[0]) + self._field.values = jids + + +class TextSingleField(Field): + def __init__(self, field): + Field.__init__(self, field) + + self._widget = Gtk.Entry() + self._widget.set_text(field.value) + self._widget.connect('changed', self._changed) + + def _changed(self, widget): + self._field.value = self._widget.get_text() + + +class TextPrivateField(TextSingleField): + def __init__(self, field): + TextSingleField.__init__(self, field) + self._widget.set_input_purpose(Gtk.InputPurpose.PASSWORD) + self._widget.set_visibility(False) + + +class JidSingleField(TextSingleField): + def __init__(self, field): + TextSingleField.__init__(self, field) + + +class TextMultiField(Field): + def __init__(self, field): + Field.__init__(self, field) + self._label.set_valign(Gtk.Align.START) + + self._widget = Gtk.ScrolledWindow() + self._widget.set_propagate_natural_height(True) + self._widget.set_min_content_height(100) + self._widget.set_max_content_height(300) + self._textview = Gtk.TextView() + + self._textview.get_buffer().set_text(field.value) + self._textview.get_buffer().connect('changed', self._changed) + + self._widget.add(self._textview) + + def _changed(self, widget): + self._field.value = widget.get_text(*widget.get_bounds(), False)