DataForms: Make validation possible
- Show orange warning icon if a field is required - Show red error icon if validation of the field failed - Set validation error text as tooltip of the icon - DataFormWidget emits a signal 'is-valid' on every change
This commit is contained in:
parent
44d56ac2fa
commit
6b65a23371
|
@ -239,7 +239,7 @@ class DataField(ExtendedNode):
|
|||
|
||||
@staticmethod
|
||||
def is_valid():
|
||||
return True
|
||||
return True, ''
|
||||
|
||||
|
||||
class Uri(nbxmpp.Node):
|
||||
|
@ -353,6 +353,13 @@ class StringField(DataField):
|
|||
except ValueError: # if there already were no value tag
|
||||
pass
|
||||
|
||||
def is_valid(self):
|
||||
if not self.required:
|
||||
return True, ''
|
||||
if not self.value:
|
||||
return False, ''
|
||||
return True, ''
|
||||
|
||||
|
||||
class ListField(DataField):
|
||||
"""
|
||||
|
@ -404,10 +411,10 @@ class ListSingleField(ListField, StringField):
|
|||
"""
|
||||
def is_valid(self):
|
||||
if not self.required:
|
||||
return True
|
||||
return True, ''
|
||||
if not self.value:
|
||||
return False
|
||||
return True
|
||||
return False, ''
|
||||
return True, ''
|
||||
|
||||
|
||||
class JidSingleField(ListSingleField):
|
||||
|
@ -418,12 +425,12 @@ class JidSingleField(ListSingleField):
|
|||
if self.value:
|
||||
try:
|
||||
helpers.parse_jid(self.value)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
return True, ''
|
||||
except Exception as error:
|
||||
return False, error
|
||||
if self.required:
|
||||
return False
|
||||
return True
|
||||
return False, ''
|
||||
return True, ''
|
||||
|
||||
|
||||
class ListMultiField(ListField):
|
||||
|
@ -458,10 +465,10 @@ class ListMultiField(ListField):
|
|||
|
||||
def is_valid(self):
|
||||
if not self.required:
|
||||
return True
|
||||
return True, ''
|
||||
if not self.values:
|
||||
return False
|
||||
return True
|
||||
return False, ''
|
||||
return True, ''
|
||||
|
||||
|
||||
class JidMultiField(ListMultiField):
|
||||
|
@ -473,12 +480,12 @@ class JidMultiField(ListMultiField):
|
|||
for value in self.values:
|
||||
try:
|
||||
helpers.parse_jid(value)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
except Exception as error:
|
||||
return False, error
|
||||
return True, ''
|
||||
if self.required:
|
||||
return False
|
||||
return True
|
||||
return False, ''
|
||||
return True, ''
|
||||
|
||||
|
||||
class TextMultiField(DataField):
|
||||
|
@ -505,6 +512,13 @@ class TextMultiField(DataField):
|
|||
for element in self.getTags('value'):
|
||||
self.delChild(element)
|
||||
|
||||
def is_valid(self):
|
||||
if not self.required:
|
||||
return True, ''
|
||||
if not self.value:
|
||||
return False, ''
|
||||
return True, ''
|
||||
|
||||
|
||||
class DataRecord(ExtendedNode):
|
||||
"""
|
||||
|
@ -573,7 +587,7 @@ class DataRecord(ExtendedNode):
|
|||
|
||||
def is_valid(self):
|
||||
for field in self.iter_fields():
|
||||
if not field.is_valid():
|
||||
if not field.is_valid()[0]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ list.settings > row > box {
|
|||
#StartChatListBox > row:not(.activatable) label { color: @insensitive_fg_color }
|
||||
|
||||
/* GroupchatConfig */
|
||||
#GroupchatConfig buttonbox { margin: 0px 12px 12px 12px; }
|
||||
#GroupchatConfig > box > buttonbox { margin: 0px 12px 12px 12px; }
|
||||
#GroupchatConfig stack { border-bottom: 1px solid; border-color: @borders;}
|
||||
#GroupchatConfig stacksidebar > scrolledwindow {
|
||||
background-color:@theme_base_color;
|
||||
|
@ -249,8 +249,4 @@ button.flat.link { padding: 0; border: 0; }
|
|||
.data-form-title { font-size: 16px; font-weight: bold; }
|
||||
.data-form-widget grid { margin: 18px; }
|
||||
.data-form-widget treeview { padding: 5px; }
|
||||
.field-required { border: 1px solid; border-color: red; }
|
||||
.field-normal { border: 1px solid; border-color:@unfocused_borders; }
|
||||
|
||||
.entry-field-required { border: 1px solid; border-color: red; }
|
||||
.entry-field-required:focus { box-shadow: inset 0 0 0 1px red; border-color: red; }
|
||||
.data-form-widget scrolledwindow { border: 1px solid; border-color:@unfocused_borders; }
|
||||
|
|
|
@ -13,10 +13,17 @@
|
|||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Pango
|
||||
|
||||
from gajim.common.i18n import _
|
||||
|
||||
|
||||
class DataFormWidget(Gtk.ScrolledWindow):
|
||||
|
||||
__gsignals__ = {'is-valid': (GObject.SignalFlags.RUN_LAST, None, (bool,))}
|
||||
|
||||
def __init__(self, form_node, options=None):
|
||||
Gtk.ScrolledWindow.__init__(self)
|
||||
self.set_hexpand(True)
|
||||
|
@ -39,6 +46,9 @@ class DataFormWidget(Gtk.ScrolledWindow):
|
|||
def instructions(self):
|
||||
return self._form_grid.instructions
|
||||
|
||||
def validate(self):
|
||||
return self._form_grid.validate(True)
|
||||
|
||||
def get_submit_form(self):
|
||||
self._form_node.type_ = 'submit'
|
||||
return self._form_node
|
||||
|
@ -109,10 +119,11 @@ class FormGrid(Gtk.Grid):
|
|||
continue
|
||||
|
||||
widget = self._fields[field.type_]
|
||||
self.add_row(widget(field, options))
|
||||
self.add_row(widget(field, self, options))
|
||||
|
||||
def is_valid(self):
|
||||
return self._data_form.is_valid()
|
||||
def validate(self, is_valid):
|
||||
value = self._data_form.is_valid() if is_valid else False
|
||||
self.get_parent().get_parent().emit('is-valid', value)
|
||||
|
||||
|
||||
class Title:
|
||||
|
@ -135,8 +146,10 @@ class Instructions:
|
|||
|
||||
|
||||
class Field:
|
||||
def __init__(self, field, options):
|
||||
def __init__(self, field, form_grid, options):
|
||||
self._field = field
|
||||
self._form_grid = form_grid
|
||||
self._validate_source_id = None
|
||||
|
||||
self._label = Gtk.Label(label=field.label)
|
||||
self._label.set_single_line_mode(False)
|
||||
|
@ -147,15 +160,64 @@ class Field:
|
|||
self._label.set_xalign(bool(options.get('right_align')))
|
||||
self._label.set_tooltip_text(field.description)
|
||||
|
||||
self._warning_image = Gtk.Image.new_from_icon_name(
|
||||
'dialog-warning-symbolic', Gtk.IconSize.MENU)
|
||||
self._warning_image.get_style_context().add_class('warning-color')
|
||||
self._warning_image.set_no_show_all(True)
|
||||
self._warning_image.set_valign(Gtk.Align.CENTER)
|
||||
self._warning_image.set_tooltip_text(_('Required'))
|
||||
self._warning_box = Gtk.Box()
|
||||
self._warning_box.set_size_request(16, -1)
|
||||
self._warning_box.add(self._warning_image)
|
||||
|
||||
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)
|
||||
form_grid.attach_next_to(self._widget,
|
||||
self._label,
|
||||
Gtk.PositionType.RIGHT, 1, 1)
|
||||
if self._field.type_ in ('jid-single',
|
||||
'jid-multi',
|
||||
'text-single',
|
||||
'text-private',
|
||||
'text-multi'):
|
||||
form_grid.attach_next_to(self._warning_box,
|
||||
self._widget,
|
||||
Gtk.PositionType.RIGHT, 1, 1)
|
||||
self._set_warning(False, '')
|
||||
|
||||
def _set_warning(self, is_valid, error):
|
||||
if not self._field.required and not is_valid and not error:
|
||||
# If its not valid and no error is given, its the inital call
|
||||
# to show all icons on required fields.
|
||||
return
|
||||
|
||||
style = self._warning_image.get_style_context()
|
||||
if error:
|
||||
style.remove_class('warning-color')
|
||||
style.add_class('error-color')
|
||||
else:
|
||||
error = _('Required')
|
||||
style.remove_class('error-color')
|
||||
style.add_class('warning-color')
|
||||
self._warning_image.set_tooltip_text(str(error))
|
||||
self._warning_image.set_visible(not is_valid)
|
||||
|
||||
def _validate(self):
|
||||
if self._validate_source_id is not None:
|
||||
GLib.source_remove(self._validate_source_id)
|
||||
|
||||
def _start_validation():
|
||||
is_valid, error = self._field.is_valid()
|
||||
self._set_warning(is_valid, error)
|
||||
self._form_grid.validate(is_valid)
|
||||
self._validate_source_id = None
|
||||
|
||||
self._validate_source_id = GLib.timeout_add(500, _start_validation)
|
||||
|
||||
|
||||
class BooleanField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
|
||||
self._widget = Gtk.CheckButton()
|
||||
self._widget.set_active(field.value)
|
||||
|
@ -166,8 +228,8 @@ class BooleanField(Field):
|
|||
|
||||
|
||||
class FixedField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
|
||||
self._label.set_text(field.value)
|
||||
|
||||
|
@ -186,8 +248,8 @@ class FixedField(Field):
|
|||
|
||||
|
||||
class ListSingleField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
|
||||
self._widget = Gtk.ComboBoxText()
|
||||
for value, label in field.iter_options():
|
||||
|
@ -203,8 +265,8 @@ class ListSingleField(Field):
|
|||
|
||||
|
||||
class ListMultiField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
self._label.set_valign(Gtk.Align.START)
|
||||
|
||||
self._treeview = ListMutliTreeView(field)
|
||||
|
@ -214,7 +276,6 @@ class ListMultiField(Field):
|
|||
self._widget.set_min_content_height(100)
|
||||
self._widget.set_max_content_height(300)
|
||||
self._widget.add(self._treeview)
|
||||
self._widget.get_style_context().add_class('field-normal')
|
||||
|
||||
|
||||
class ListMutliTreeView(Gtk.TreeView):
|
||||
|
@ -264,32 +325,26 @@ class ListMutliTreeView(Gtk.TreeView):
|
|||
|
||||
|
||||
class JidMultiField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
self._label.set_valign(Gtk.Align.START)
|
||||
|
||||
self._treeview = JidMutliTreeView(field)
|
||||
self._treeview = JidMutliTreeView(field, self)
|
||||
|
||||
self._add_button = Gtk.Button.new_from_icon_name(
|
||||
'list-add-symbolic', Gtk.IconSize.MENU)
|
||||
self._add_button = Gtk.ToolButton(icon_name='list-add-symbolic')
|
||||
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 = Gtk.ToolButton(icon_name='list-remove-symbolic')
|
||||
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._toolbar = Gtk.Toolbar()
|
||||
self._toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
self._toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
self._toolbar.get_style_context().add_class('inline-toolbar')
|
||||
self._toolbar.add(self._add_button)
|
||||
self._toolbar.add(self._remove_button)
|
||||
|
||||
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)
|
||||
|
@ -298,9 +353,7 @@ class JidMultiField(Field):
|
|||
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)
|
||||
|
||||
self._treeview.update_required_css()
|
||||
self._widget.pack_end(self._toolbar, False, False, 0)
|
||||
|
||||
def _add_clicked(self, _widget):
|
||||
self._treeview.get_model().append([''])
|
||||
|
@ -317,14 +370,18 @@ class JidMultiField(Field):
|
|||
continue
|
||||
jids.append(row[0])
|
||||
self._field.values = jids
|
||||
self._treeview.update_required_css()
|
||||
self._validate()
|
||||
|
||||
def validate(self):
|
||||
self._validate()
|
||||
|
||||
|
||||
class JidMutliTreeView(Gtk.TreeView):
|
||||
def __init__(self, field):
|
||||
def __init__(self, field, multi_field):
|
||||
Gtk.TreeView.__init__(self)
|
||||
|
||||
self._field = field
|
||||
self._multi_field = multi_field
|
||||
|
||||
self._store = Gtk.ListStore(str)
|
||||
|
||||
|
@ -348,7 +405,7 @@ class JidMutliTreeView(Gtk.TreeView):
|
|||
iter_ = self._store.get_iter(path)
|
||||
self._store.set_value(iter_, 0, new_text)
|
||||
self._set_values()
|
||||
self.update_required_css()
|
||||
self._multi_field.validate()
|
||||
|
||||
def _set_values(self):
|
||||
jids = []
|
||||
|
@ -358,58 +415,35 @@ class JidMutliTreeView(Gtk.TreeView):
|
|||
jids.append(row[0])
|
||||
self._field.values = jids
|
||||
|
||||
def update_required_css(self):
|
||||
style = self.get_parent().get_style_context()
|
||||
if not self._field.required:
|
||||
style.add_class('field-normal')
|
||||
return
|
||||
|
||||
if self._field.values:
|
||||
style.remove_class('field-required')
|
||||
style.add_class('field-normal')
|
||||
else:
|
||||
style.remove_class('field-normal')
|
||||
style.add_class('field-required')
|
||||
|
||||
|
||||
class TextSingleField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
|
||||
self._widget = Gtk.Entry()
|
||||
self._widget.set_text(field.value)
|
||||
self._widget.connect('changed', self._changed)
|
||||
self._update_required_css()
|
||||
|
||||
def _changed(self, _widget):
|
||||
self._field.value = self._widget.get_text()
|
||||
self._update_required_css()
|
||||
|
||||
def _update_required_css(self):
|
||||
if not self._field.required:
|
||||
return
|
||||
style = self._widget.get_style_context()
|
||||
if self._field.value:
|
||||
style.remove_class('entry-field-required')
|
||||
else:
|
||||
style.add_class('entry-field-required')
|
||||
self._validate()
|
||||
|
||||
|
||||
class TextPrivateField(TextSingleField):
|
||||
def __init__(self, field, options):
|
||||
TextSingleField.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
TextSingleField.__init__(self, field, form_grid, options)
|
||||
self._widget.set_input_purpose(Gtk.InputPurpose.PASSWORD)
|
||||
self._widget.set_visibility(False)
|
||||
|
||||
|
||||
class JidSingleField(TextSingleField):
|
||||
def __init__(self, field, options):
|
||||
TextSingleField.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
TextSingleField.__init__(self, field, form_grid, options)
|
||||
|
||||
|
||||
class TextMultiField(Field):
|
||||
def __init__(self, field, options):
|
||||
Field.__init__(self, field, options)
|
||||
def __init__(self, field, form_grid, options):
|
||||
Field.__init__(self, field, form_grid, options)
|
||||
self._label.set_valign(Gtk.Align.START)
|
||||
|
||||
self._widget = Gtk.ScrolledWindow()
|
||||
|
@ -422,17 +456,7 @@ class TextMultiField(Field):
|
|||
self._textview.get_buffer().connect('changed', self._changed)
|
||||
|
||||
self._widget.add(self._textview)
|
||||
self._update_required_css()
|
||||
|
||||
def _changed(self, widget):
|
||||
self._field.value = widget.get_text(*widget.get_bounds(), False)
|
||||
self._update_required_css()
|
||||
|
||||
def _update_required_css(self):
|
||||
if not self._field.required:
|
||||
return
|
||||
style = self._widget.get_style_context()
|
||||
if self._field.value:
|
||||
style.remove_class('field-required')
|
||||
else:
|
||||
style.add_class('field-required')
|
||||
self._validate()
|
||||
|
|
Loading…
Reference in New Issue