From 6cf227235a0e24c06395fffe6ce5e8550161baa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 26 Jan 2013 15:19:09 +0100 Subject: [PATCH] Add Dict fields on form review538002 --- CHANGELOG | 1 + tryton/gui/window/view_form/model/field.py | 14 + .../view_form/view/form_gtk/dictionary.py | 502 ++++++++++++++++++ .../window/view_form/view/form_gtk/parser.py | 5 +- 4 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 tryton/gui/window/view_form/view/form_gtk/dictionary.py diff --git a/CHANGELOG b/CHANGELOG index 6b7906a4dd..42005f30e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add Dict fields on form * Remove Goto * Add global search * Add toggle button for menu diff --git a/tryton/gui/window/view_form/model/field.py b/tryton/gui/window/view_form/model/field.py index a7d1ee73c8..4e594ba22f 100644 --- a/tryton/gui/window/view_form/model/field.py +++ b/tryton/gui/window/view_form/model/field.py @@ -920,6 +920,19 @@ class BinaryField(CharField): self.set(record, filename) return self.get(record) + +class DictField(CharField): + + _default = {} + + def validation_domains(self, record): + screen_domain, attr_domain = self.domains_get(record) + return screen_domain, screen_domain + + def domain_get(self, record): + screen_domain, attr_domain = self.domains_get(record) + return localize_domain(inverse_leaf(screen_domain)) + attr_domain + TYPES = { 'char': CharField, 'sha': CharField, @@ -939,4 +952,5 @@ TYPES = { 'time': TimeField, 'one2one': O2OField, 'binary': BinaryField, + 'dict': DictField, } diff --git a/tryton/gui/window/view_form/view/form_gtk/dictionary.py b/tryton/gui/window/view_form/view/form_gtk/dictionary.py new file mode 100644 index 0000000000..ecd5fc8f35 --- /dev/null +++ b/tryton/gui/window/view_form/view/form_gtk/dictionary.py @@ -0,0 +1,502 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of this +# repository contains the full copyright notices and license terms. + +import operator +import gobject +import gtk +import locale +import decimal +import gettext +from decimal import Decimal + +from interface import WidgetInterface +from tryton.config import CONFIG +from tryton.gui.window.win_search import WinSearch +from tryton.common import RPCExecute, RPCException, timezoned_date, \ + datetime_strftime, Tooltips +from tryton.common.date_widget import DateEntry +from tryton.common.placeholder_entry import PlaceholderEntry +from tryton.translate import date_format + +_ = gettext.gettext + + +class DictEntry(object): + expand = True + fill = True + + def __init__(self, name, parent_widget): + self.name = name + self.definition = parent_widget.keys[name] + self.parent_widget = parent_widget + self.widget = self.create_widget() + + def create_widget(self): + widget = gtk.Entry() + widget.connect('key-press-event', self.parent_widget.send_modified) + widget.connect('focus-out-event', + lambda w, e: self.parent_widget._focus_out()) + return widget + + def modified(self, value): + return self.get_value() != value.get(self.name) + + def get_value(self): + return self.widget.get_text() + + def set_value(self, value): + return self.widget.set_text(value) + + def set_readonly(self, readonly): + self.widget.set_editable(not readonly) + + +class DictBooleanEntry(DictEntry): + + def create_widget(self): + widget = gtk.CheckButton() + widget.connect('toggled', self.parent_widget.send_modified) + widget.connect('focus-out-event', lambda w, e: + self.parent_widget._focus_out()) + return widget + + def get_value(self): + return self.widget.props.active + + def set_value(self, value): + self.widget.props.active = bool(value) + + def set_readonly(self, readonly): + self.widget.set_sensitive(not readonly) + + +class DictSelectionEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = gtk.ComboBoxEntry() + + # customizing entry + widget.child.props.activates_default = True + widget.child.connect('changed', self.parent_widget.send_modified) + widget.child.connect('focus-out-event', + lambda w, e: self.parent_widget._focus_out()) + widget.child.connect('activate', + lambda w, e: self.parent_widget._focus_out()) + widget.connect('notify::active', + lambda w, e: self.parent_widget._focus_out()) + + # setting completion and selection + model = gtk.ListStore(gobject.TYPE_STRING) + model.append(('',)) + self._selection = {'': None} + width = 10 + for value, name in sorted(self.definition['selection'], + key=operator.itemgetter(1)): + name = str(name) + self._selection[name] = value + model.append((name,)) + width = max(width, len(name)) + widget.set_model(model) + widget.set_text_column(0) + widget.child.set_width_chars(width) + completion = gtk.EntryCompletion() + if hasattr(completion, 'set_inline_selection'): + completion.set_inline_selection(True) + completion.set_model(model) + widget.child.set_completion(completion) + completion.set_text_column(0) + return widget + + def get_value(self): + text = self.widget.child.get_text() + return self._selection[text] + + def set_value(self, value): + values = dict(self.definition['selection']) + self.widget.child.set_text(values.get(value, '')) + + def set_readonly(self, readonly): + self.widget.set_sensitive(not readonly) + + +class DictIntegerEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = super(DictIntegerEntry, self).create_widget() + widget.set_width_chars(8) + widget.set_max_length(0) + widget.set_alignment(1.0) + widget.connect('insert-text', self.sig_insert_text) + return widget + + def sig_insert_text(self, entry, new_text, new_text_length, position): + value = entry.get_text() + position = entry.get_position() + new_value = value[:position] + new_text + value[position:] + if new_value == '-': + return + try: + locale.atoi(new_value) + except ValueError: + entry.stop_emission('insert-text') + + def get_value(self): + txt_value = self.widget.get_text() + if txt_value: + try: + return locale.atoi(txt_value) + except ValueError: + pass + return None + + def set_value(self, value): + if value is not None: + txt_val = locale.format('%d', value, True) + else: + txt_val = '' + self.widget.set_text(txt_val) + + +class DictFloatEntry(DictIntegerEntry): + + def digits(self): + default = (16, 2) + record = self.parent_widget.record + if not record: + return default + return tuple(y if x is None else x for x, y in zip( + record.expr_eval(self.definition.get('digits', default)), + default)) + + def sig_insert_text(self, entry, new_text, new_text_length, position): + value = entry.get_text() + position = entry.get_position() + new_value = value[:position] + new_text + value[position:] + decimal_point = locale.localeconv()['decimal_point'] + + if new_value in ('-', decimal_point): + return + + digits = self.digits() + + try: + locale.atof(new_value) + except ValueError: + entry.stop_emission('insert-text') + return + + new_int = new_value + new_decimal = '' + if decimal_point in new_value: + new_int, new_decimal = new_value.rsplit(decimal_point, 1) + + if (len(new_int) > digits[0] + or len(new_decimal) > digits[1]): + entry.stop_emission('insert-text') + + def get_value(self): + txt_value = self.widget.get_text() + if txt_value: + try: + return locale.atof(txt_value) + except ValueError: + pass + return None + + def set_value(self, value): + digits = self.digits() + if value is not None: + txt_val = locale.format('%.' + str(digits[1]) + 'f', value, True) + else: + txt_val = '' + self.widget.set_width_chars(sum(digits)) + self.widget.set_text(txt_val) + + +class DictNumericEntry(DictFloatEntry): + + def get_value(self): + txt_value = self.widget.get_text() + if txt_value: + try: + return locale.atof(txt_value, Decimal) + except decimal.InvalidOperation: + pass + return None + + +class DictDateTimeEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = DateEntry('') + widget.set_format(self.get_format()) + widget.connect('key_press_event', self.parent_widget.send_modified) + widget.connect('focus-out-event', lambda w, e: + self.parent_widget._focus_out()) + return widget + + def modified(self, value): + if value.get(self.name): + text = datetime_strftime(timezoned_date(value[self.name]), + self.get_format()) + else: + text = '' + return self.widget.compute_date(self.widget.get_text()) != text + + def get_format(self): + return date_format() + ' %H:%M:%S' # got to find a way + + def get_value(self): + return self.widget.date_get() + + def set_value(self, value): + self.widget.date_set(value) + txt = self.widget.get_text() + if txt: + if len(txt) > self.widget.get_width_chars(): + self.widget.set_width_chars(len(txt)) + + +class DictDateEntry(DictDateTimeEntry): + + def modified(self, value): + if value.get(self.name): + text = datetime_strftime(value[self.name], self.get_format()) + else: + text = '' + return self.widget.compute_date(self.widget.get_text()) != text + + def get_format(self): + return date_format() + + def get_value(self): + dt = super(DictDateEntry, self).get_value() + return dt.date() if dt else None + + +DICT_ENTRIES = { + 'char': DictEntry, + 'boolean': DictBooleanEntry, + 'selection': DictSelectionEntry, + 'datetime': DictDateTimeEntry, + 'date': DictDateEntry, + 'integer': DictIntegerEntry, + 'float': DictFloatEntry, + 'numeric': DictNumericEntry, + } + + +class DictWidget(WidgetInterface): + + def __init__(self, field_name, model_name, attrs=None): + super(DictWidget, self).__init__(field_name, model_name, attrs=attrs) + self.schema_model = attrs['schema_model'] + self.keys = {} + self.fields = {} + self.buttons = {} + self.rows = {} + + self.widget = gtk.Frame(attrs.get('string', '')) + self.widget.set_shadow_type(gtk.SHADOW_OUT) + + vbox = gtk.VBox() + self.widget.add(vbox) + + self.table = gtk.Table(1, 3, homogeneous=False) + self.table.set_col_spacings(0) + self.table.set_row_spacings(0) + self.table.set_border_width(0) + vbox.pack_start(self.table, expand=True, fill=True) + + hbox = gtk.HBox() + hbox.set_border_width(2) + self.wid_text = PlaceholderEntry() + self.wid_text.set_placeholder_text(_('Search')) + self.wid_text.props.width_chars = 13 + self.wid_text.connect('activate', self._sig_activate) + hbox.pack_start(self.wid_text, expand=True, fill=True) + self.but_add = gtk.Button() + self.but_add.connect('clicked', self._sig_add) + img_add = gtk.Image() + img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) + img_add.set_alignment(0.5, 0.5) + self.but_add.add(img_add) + self.but_add.set_relief(gtk.RELIEF_NONE) + hbox.pack_start(self.but_add, expand=False, fill=False) + hbox.set_focus_chain([self.wid_text]) + vbox.pack_start(hbox, expand=True, fill=True) + + self.tooltips = Tooltips() + self.tooltips.set_tip(self.but_add, _('Add value')) + self.tooltips.enable() + + self._readonly = False + self._record_id = None + + def _new_remove_btn(self): + but_remove = gtk.Button() + img_remove = gtk.Image() + img_remove.set_from_stock('tryton-list-remove', + gtk.ICON_SIZE_SMALL_TOOLBAR) + img_remove.set_alignment(0.5, 0.5) + but_remove.add(img_remove) + but_remove.set_relief(gtk.RELIEF_NONE) + return but_remove + + def _sig_activate(self, *args): + if self.wid_text.get_editable(): + self._sig_add() + + def _sig_add(self, *args): + context = self.field.context_get(self.record) + value = self.wid_text.get_text() + domain = self.field.domain_get(self.record) + dom = [('rec_name', 'ilike', '%%%s%%' % value)] if value else [] + dom.append(('id', 'not in', + [self.keys[f]['id'] for f in self.fields])) + try: + ids = RPCExecute('model', self.schema_model, 'search', + domain + dom, 0, CONFIG['client.limit'], None, context=context) + except RPCException: + return False + + def callback(result): + if result: + self.send_modified() + try: + new_fields = RPCExecute('model', self.schema_model, + 'get_keys', [r[0] for r in result], + context=context) + except RPCException: + new_fields = [] + for new_field in new_fields: + if new_field['name'] not in self.fields: + self.keys[new_field['name']] = new_field + self.add_line(new_field['name']) + self.wid_text.set_text('') + + if len(ids) != 1: + WinSearch(self.schema_model, callback, sel_multi=True, ids=ids, + context=context, domain=domain, new=False) + else: + callback([(id, None) for id in ids]) + + def _sig_remove(self, button, key, modified=True): + del self.fields[key] + del self.buttons[key] + for widget in self.rows[key]: + self.table.remove(widget) + widget.destroy() + del self.rows[key] + if modified: + self.send_modified() + self.set_value(self.record, self.field) + + def set_value(self, record, field): + value = {} + for key, widget in self.fields.items(): + wid_value = widget.get_value() + value[key] = wid_value + field.set_client(record, value) + + @property + def modified(self): + if self.record and self.field: + value = self.field.get_client(self.record) + return any(widget.modified(value) + for widget in self.fields.itervalues()) + return False + + def _readonly_set(self, readonly): + self._readonly = readonly + self._set_button_sensitive() + for widget in self.fields.values(): + widget.set_readonly(readonly) + self.wid_text.set_editable(not readonly) + + def _set_button_sensitive(self): + self.but_add.set_sensitive(bool( + not self._readonly + and self.attrs.get('create', True))) + for button in self.buttons.itervalues(): + button.set_sensitive(bool( + not self._readonly + and self.attrs.get('delete', True))) + + def add_line(self, key): + self.fields[key] = DICT_ENTRIES[self.keys[key]['type_']](key, self) + field = self.fields[key] + alignment = gtk.Alignment( + float(self.attrs.get('xalign', 0.0)), + float(self.attrs.get('yalign', 0.5)), + float(self.attrs.get('xexpand', 1.0)), + float(self.attrs.get('yexpand', 1.0))) + hbox = gtk.HBox() + hbox.pack_start(field.widget, expand=field.expand, fill=field.fill) + alignment.add(hbox) + n_rows = self.table.props.n_rows + self.table.resize(n_rows + 1, 3) + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text = _(':') + self.keys[key]['string'] + else: + text = self.keys[key]['string'] + _(':') + label = gtk.Label(text) + label.set_alignment(1., .5) + self.table.attach(label, 0, 1, n_rows - 1, n_rows, + xoptions=gtk.FILL, yoptions=False, xpadding=2) + label.show() + self.table.attach(alignment, 1, 2, n_rows - 1, n_rows, + xoptions=gtk.FILL | gtk.EXPAND, yoptions=False, xpadding=2) + alignment.show_all() + remove_but = self._new_remove_btn() + self.tooltips.set_tip(remove_but, _('Remove "%s"') % + self.keys[key]['string']) + self.table.attach(remove_but, 2, 3, n_rows - 1, n_rows, + xoptions=gtk.FILL, yoptions=False, xpadding=2) + remove_but.connect('clicked', self._sig_remove, key) + remove_but.show_all() + self.rows[key] = [label, alignment, remove_but] + self.buttons[key] = remove_but + + def add_key(self, key): + context = self.field.context_get(self.record) + try: + key_ids = RPCExecute('model', self.schema_model, 'search', + [('name', '=', key)], 0, CONFIG['client.limit'], + None, context=context) + self.keys[key] = RPCExecute('model', self.schema_model, + 'get_keys', key_ids, context=context)[0] + except RPCException: + pass + + def display(self, record, field): + super(DictWidget, self).display(record, field) + + if field is None: + return + + record_id = record.id if record else None + if record_id != self._record_id: + for key in self.fields.keys(): + self._sig_remove(None, key, modified=False) + self._record_id = record_id + + value = field.get_client(record) if field else {} + for key in sorted(value.iterkeys()): + val = value[key] + if key not in self.keys: + self.add_key(key) + if key not in self.fields: + self.add_line(key) + widget = self.fields[key] + widget.set_value(val) + for key in set(self.fields.keys()) - set(value.keys()): + self._sig_remove(None, key, modified=False) + + self._set_button_sensitive() diff --git a/tryton/gui/window/view_form/view/form_gtk/parser.py b/tryton/gui/window/view_form/view/form_gtk/parser.py index 7db96346eb..ad79ad6416 100644 --- a/tryton/gui/window/view_form/view/form_gtk/parser.py +++ b/tryton/gui/window/view_form/view/form_gtk/parser.py @@ -421,7 +421,8 @@ class ParserForm(ParserInterface): for attr_name in ('relation', 'domain', 'selection', 'relation_field', 'string', 'views', 'invisible', 'add_remove', 'sort', 'context', 'size', 'filename', - 'autocomplete', 'translate', 'create', 'delete'): + 'autocomplete', 'translate', 'create', 'delete', + 'schema_model'): if attr_name in fields[name].attrs and \ not attr_name in attrs: attrs[attr_name] = fields[name].attrs[attr_name] @@ -544,6 +545,7 @@ from image import Image as Image2 from progressbar import ProgressBar from one2one import One2One from richtextbox import RichTextBox +from dictionary import DictWidget WIDGETS_TYPE = { @@ -573,4 +575,5 @@ WIDGETS_TYPE = { 'progressbar': (ProgressBar, 1, False, False), 'one2one': (One2One, 1, False, False), 'richtext': (RichTextBox, 1, True, True), + 'dict': (DictWidget, 1, False, False), }