diff --git a/CHANGELOG b/CHANGELOG index ff78ad6751..6dfac0a095 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add a multi selection widget for many2many * Remove auto-refresh * Add support of domain for non-relation field * Allow drag & drop on the attachment button diff --git a/tryton/common/selection.py b/tryton/common/selection.py index b46d409f94..acfff7f836 100644 --- a/tryton/common/selection.py +++ b/tryton/common/selection.py @@ -14,6 +14,7 @@ class SelectionMixin(object): def __init__(self, *args, **kwargs): super(SelectionMixin, self).__init__(*args, **kwargs) + self.nullable_widget = True self.selection = None self.inactive_selection = [] self._last_domain = None @@ -73,7 +74,8 @@ class SelectionMixin(object): result = False if isinstance(result, list): selection = [(x['id'], x['rec_name']) for x in result] - selection.append((None, '')) + if self.nullable_widget: + selection.append((None, '')) self._last_domain = (domain, context) self._domain_cache[domain_cache_key] = selection else: diff --git a/tryton/gui/window/view_form/model/field.py b/tryton/gui/window/view_form/model/field.py index d5ed2d6d4f..4ef79b76c2 100644 --- a/tryton/gui/window/view_form/model/field.py +++ b/tryton/gui/window/view_form/model/field.py @@ -603,7 +603,8 @@ class O2MField(CharField): if mode == 'list ids': for old_record in group: - group.remove(old_record, remove=True, signal=False) + if old_record.id not in value: + group.remove(old_record, remove=True, signal=False) group.load(value) else: for vals in value: @@ -645,7 +646,8 @@ class O2MField(CharField): previous_ids = [r.id for r in record.value.get(self.name) or []] self._set_value(record, value) - if previous_ids != value: + # The order of the ids is not significant + if set(previous_ids) != set(value): record.modified_fields.setdefault(self.name) record.signal('record-modified') self.sig_changed(record) diff --git a/tryton/gui/window/view_form/view/form_gtk/multiselection.py b/tryton/gui/window/view_form/view/form_gtk/multiselection.py new file mode 100644 index 0000000000..0e44673d61 --- /dev/null +++ b/tryton/gui/window/view_form/view/form_gtk/multiselection.py @@ -0,0 +1,99 @@ +# 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 gtk +import gobject + +from .interface import WidgetInterface +from tryton.common.selection import SelectionMixin + +MOVEMENT_KEYS = {gtk.keysyms.Up, gtk.keysyms.Down, gtk.keysyms.space, + gtk.keysyms.Left, gtk.keysyms.KP_Left, + gtk.keysyms.Right, gtk.keysyms.KP_Right, + gtk.keysyms.Home, gtk.keysyms.KP_Home, + gtk.keysyms.End, gtk.keysyms.KP_End} + + +class MultiSelection(WidgetInterface, SelectionMixin): + + def __init__(self, field_name, model_name, attrs=None): + super(MultiSelection, self).__init__(field_name, model_name, + attrs=attrs) + + self.widget = viewport = gtk.Viewport() + viewport.set_shadow_type(gtk.SHADOW_ETCHED_IN) + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.set_placement(gtk.CORNER_TOP_LEFT) + viewport.add(scroll) + + self.model = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING) + self.tree = gtk.TreeView() + self.tree.set_model(self.model) + self.tree.set_search_column(1) + self.tree.connect('focus-out-event', lambda *a: self._focus_out()) + self.tree.connect('button-press-event', self.__button_press) + self.tree.connect('key_press_event', self.__key_press) + selection = self.tree.get_selection() + selection.set_mode(gtk.SELECTION_MULTIPLE) + selection.connect('changed', self.send_modified) + scroll.add(self.tree) + name_column = gtk.TreeViewColumn(attrs.get('string', '')) + name_cell = gtk.CellRendererText() + name_column.pack_start(name_cell) + name_column.add_attribute(name_cell, 'text', 1) + self.tree.append_column(name_column) + + self.nullable_widget = False + self.init_selection() + self.id2path = {} + + def _color_widget(self): + return self.tree + + def grab_focus(self): + self.tree.grab_focus() + + @property + def modified(self): + if self.record and self.field: + group = set(r.id for r in self.field.get_client(self.record)) + value = set(self.get_value()) + return value != group + return False + + def send_modified(self, *args): + if self.record: + self.record.signal('record-modified') + + def get_value(self): + model, paths = self.tree.get_selection().get_selected_rows() + return [model[path][0] for path in paths] + + def set_value(self, record, field): + field.set_client(record, self.get_value()) + + def display(self, record, field): + self.update_selection(record, field) + super(MultiSelection, self).display(record, field) + self.model.clear() + if field is None: + return + id2path = {} + for idx, (value, name) in enumerate(self.selection): + self.model.append((value, name)) + id2path[value] = idx + selection = self.tree.get_selection() + selection.unselect_all() + group = field.get_client(record) + for element in group: + if (element not in group.record_removed + and element not in group.record_deleted): + selection.select_path(id2path[element.id]) + + def __button_press(self, treeview, event): + if event.button == 1: + event.state |= gtk.gdk.CONTROL_MASK + + def __key_press(self, treeview, event): + if event.keyval in MOVEMENT_KEYS: + event.state |= gtk.gdk.CONTROL_MASK 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 0f3fc69d63..9c8205ec05 100644 --- a/tryton/gui/window/view_form/view/form_gtk/parser.py +++ b/tryton/gui/window/view_form/view/form_gtk/parser.py @@ -537,6 +537,7 @@ from progressbar import ProgressBar from one2one import One2One from richtextbox import RichTextBox from dictionary import DictWidget +from multiselection import MultiSelection WIDGETS_TYPE = { @@ -567,4 +568,5 @@ WIDGETS_TYPE = { 'one2one': (One2One, 1, False, False), 'richtext': (RichTextBox, 1, True, True), 'dict': (DictWidget, 1, False, False), + 'multiselection': (MultiSelection, 1, True, True), }