# Copyright 2017 Canonical, Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from urwid import ( AttrMap, connect_signal, delegate_to_widget_mixin, emit_signal, MetaSignals, Text, WidgetDecoration, WidgetDisable, WidgetWrap, ) from subiquitycore.ui.buttons import cancel_btn, done_btn from subiquitycore.ui.container import Columns, Pile from subiquitycore.ui.interactive import StringEditor from subiquitycore.ui.utils import Color class Toggleable(delegate_to_widget_mixin('_original_widget'), WidgetDecoration): def __init__(self, original, active_color): self.original = original self.active_color = active_color self.enabled = False self.enable() def enable(self): if not self.enabled: self.original_widget = AttrMap(self.original, self.active_color, self.active_color + ' focus') self.enabled = True def disable(self): if self.enabled: self.original_widget = WidgetDisable(Color.info_minor(self.original)) self.enabled = False class _Validator(WidgetWrap): def __init__(self, field, w): self.field = field super().__init__(w) def lost_focus(self): self.field.validate() class FormField(object): next_index = 0 def __init__(self, caption=None, cleaner=None, validator=None, help=None): self.caption = caption self.cleaner = cleaner self.validator = validator self.help = help self.index = FormField.next_index FormField.next_index += 1 def bind(self, form): return self.bound_class(self, form) class BoundFormField(object): def __init__(self, field, form): self.field = field self.form = form self.in_error = False self._help = None self.widget = self._make_widget() def _make_widget(self): raise NotImplementedError(self._make_widget) def clean(self, value): if self.field.cleaner is not None: value = self.field.cleaner(value) cleaner = getattr(self.form, "clean_" + self.field.name, None) if cleaner is not None: value = cleaner(value) return value def _validate(self): try: v = self.value except ValueError as e: return str(e) if self.field.validator is not None: r = self.field.validator(v) if r is not None: return r validator = getattr(self.form, "validate_" + self.field.name, None) if validator is not None: return validator() def validate(self): r = self._validate() if r is None: self.in_error = False self.hide_extra() else: self.in_error = True extra = Color.info_error(Text(r, align="center")) self.show_extra(extra) self.form.validated() def hide_extra(self): if len(self.pile.contents) > 1: self.pile.contents = self.pile.contents[:1] def show_extra(self, extra): t = (extra, self.pile.options('pack')) if len(self.pile.contents) > 1: self.pile.contents[1] = t else: self.pile.contents.append(t) @property def value(self): return self.clean(self.widget.value) @value.setter def value(self, val): self.widget.value = val @property def help(self): if self._help is not None: return self._help else: return self.field.help @help.setter def help(self, val): self._help = val def as_row(self, include_help): text = Text(self.field.caption, align="right") input = _Validator(self, self.widget) cols = [ ("weight", 0.2, text), ("weight", 0.3, Color.string_input(input)), ] if include_help: if self.help is not None: help = self.help else: help = "" cols.append( ("weight", 0.5, Text(help))) self.pile = Pile([Columns(cols, dividechars=4)]) return self.pile class BoundStringField(BoundFormField): def _make_widget(self): return StringEditor(caption="") class StringField(FormField): bound_class = BoundStringField class MetaForm(MetaSignals): def __init__(self, name, bases, attrs): super().__init__(name, bases, attrs) _unbound_fields = [] for k, v in attrs.items(): if isinstance(v, FormField): v.name = k if v.caption is None: v.caption = k + ":" _unbound_fields.append(v) _unbound_fields.sort(key=lambda f:f.index) self._unbound_fields = _unbound_fields class Form(object, metaclass=MetaForm): signals = ['submit', 'cancel'] opts = {} def __init__(self): self.done_btn = Toggleable(done_btn(), 'button') self.cancel_btn = Toggleable(cancel_btn(), 'button') connect_signal(self.done_btn.base_widget, 'click', self._click_done) connect_signal(self.cancel_btn.base_widget, 'click', self._click_cancel) self.buttons = Pile([self.done_btn, self.cancel_btn]) self._fields = [] for field in self._unbound_fields: bf = field.bind(self) setattr(self, bf.field.name, bf) self._fields.append(bf) def _click_done(self, sender): emit_signal(self, 'submit', self) def _click_cancel(self, sender): emit_signal(self, 'cancel', self) def as_rows(self): has_help = False for field in self._fields: if field.help is not None: has_help = True break rows = [] for field in self._fields: rows.append(field.as_row(has_help)) return Pile(rows) def validated(self): in_error = False for f in self._fields: if f.in_error: in_error = True break if in_error: self.buttons.contents[0][0].disable() self.buttons.focus_position = 1 else: self.buttons.contents[0][0].enable()