From 2c6bd741ff7186cd74f28a1bf795ca94b690292e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 16 Nov 2012 20:24:33 +0100 Subject: [PATCH] Add completion on Many2One, Many2Many and One2Many review580002 --- CHANGELOG | 1 + tryton/common/cellrenderertext.py | 16 ++++ tryton/common/completion.py | 59 +++++++++++++ tryton/gui/window/view_form/view/form.py | 7 -- .../window/view_form/view/form_gtk/binary.py | 4 - .../window/view_form/view/form_gtk/image.py | 4 - .../view_form/view/form_gtk/interface.py | 5 -- .../view_form/view/form_gtk/many2many.py | 83 ++++++++++++++---- .../view_form/view/form_gtk/many2one.py | 65 ++++++++++---- .../view_form/view/form_gtk/one2many.py | 84 ++++++++++++++----- .../view_form/view/form_gtk/reference.py | 13 ++- .../view_form/view/form_gtk/richtextbox.py | 1 - .../view_form/view/form_gtk/selection.py | 5 +- .../gui/window/view_form/view/form_gtk/url.py | 1 - .../window/view_form/view/list_gtk/parser.py | 74 ++++++++++++++-- tryton/gui/window/win_form.py | 3 - 16 files changed, 336 insertions(+), 89 deletions(-) create mode 100644 tryton/common/completion.py diff --git a/CHANGELOG b/CHANGELOG index 15d66736dd..7ed50bb905 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add completion on Many2One, Many2Many and One2Many * Add bookmark on search widget * Manage domains on Action Window * Use range for Date/Time fields in filter box diff --git a/tryton/common/cellrenderertext.py b/tryton/common/cellrenderertext.py index a704a5aab0..743221eb14 100644 --- a/tryton/common/cellrenderertext.py +++ b/tryton/common/cellrenderertext.py @@ -113,4 +113,20 @@ class CellRendererText(gtk.GenericCellRenderer): editable.show() return editable + +class CellRendererTextCompletion(CellRendererText): + + def __init__(self, set_completion): + super(CellRendererTextCompletion, self).__init__() + self.set_completion = set_completion + + def on_start_editing(self, event, widget, path, background_area, cell_area, + flags): + editable = super(CellRendererTextCompletion, + self).on_start_editing(event, widget, path, background_area, + cell_area, flags) + self.set_completion(editable, path) + return editable + gobject.type_register(CellRendererText) +gobject.type_register(CellRendererTextCompletion) diff --git a/tryton/common/completion.py b/tryton/common/completion.py new file mode 100644 index 0000000000..2f5b3d702b --- /dev/null +++ b/tryton/common/completion.py @@ -0,0 +1,59 @@ +#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 gettext +import gobject + +from tryton.config import CONFIG +from tryton.common import RPCExecute +from tryton.exceptions import TrytonServerError, TrytonError + +_ = gettext.gettext + + +def get_completion(): + "Return a EntryCompletion" + completion = gtk.EntryCompletion() + completion.set_match_func(lambda *a: True) + completion.set_model(gtk.ListStore(str, int)) + completion.set_text_column(0) + if hasattr(completion.props, 'popup_set_width'): + completion.props.popup_set_width = False + completion.insert_action_markup(0, _('Search...')) + completion.insert_action_markup(1, _('Create...')) + return completion + + +def update_completion(entry, record, field, model, domain=None): + "Update entry completion" + def update(search_text, domain): + if search_text != entry.get_text().decode('utf-8'): + return False + completion_model = entry.get_completion().get_model() + if not search_text or not model: + completion_model.clear() + completion_model.search_text = search_text + return False + if getattr(completion_model, 'search_text', None) == search_text: + return False + if domain is None: + domain = field.domain_get(record) + context = field.context_get(record) + domain = [('rec_name', 'ilike', '%' + search_text + '%'), domain] + try: + results = RPCExecute('model', model, 'search_read', domain, 0, + CONFIG['client.limit'], None, ['rec_name'], context=context, + process_exception=False) + except (TrytonError, TrytonServerError): + results = [] + if search_text != entry.get_text().decode('utf-8'): + return False + completion_model.clear() + for result in results: + completion_model.append([result['rec_name'], result['id']]) + completion_model.search_text = search_text + # Force display of popup + entry.emit('changed') + return False + search_text = entry.get_text().decode('utf-8') + gobject.timeout_add(300, update, search_text, domain) diff --git a/tryton/gui/window/view_form/view/form.py b/tryton/gui/window/view_form/view/form.py index c6653491ec..835d396531 100644 --- a/tryton/gui/window/view_form/view/form.py +++ b/tryton/gui/window/view_form/view/form.py @@ -4,7 +4,6 @@ import operator from functools import reduce import gtk import gettext -import gobject from interface import ParserView _ = gettext.gettext @@ -21,7 +20,6 @@ class ViewForm(ParserView): for widget in self.state_widgets: if isinstance(widget, gtk.Button): widget.connect('clicked', self.button_clicked) - widget.set_focus_on_click(False) # Force to display the first time it switches on a page # This avoids glitch in position of widgets @@ -42,7 +40,6 @@ class ViewForm(ParserView): vbox = gtk.VBox() vp = gtk.Viewport() vp.set_shadow_type(gtk.SHADOW_NONE) - vp.connect('leave-notify-event', self.leave) vp.add(self.widget) scroll = gtk.ScrolledWindow() scroll.add(vp) @@ -171,10 +168,6 @@ class ViewForm(ParserView): notebook.set_current_page(i) focus_widget.grab_focus() - def leave(self, widget, event): - # leave could be called during event process - gobject.idle_add(self.set_value, True) - def button_clicked(self, widget): record = self.screen.current_record fields = self.get_fields() diff --git a/tryton/gui/window/view_form/view/form_gtk/binary.py b/tryton/gui/window/view_form/view/form_gtk/binary.py index 66df1bc664..d9d1593d98 100644 --- a/tryton/gui/window/view_form/view/form_gtk/binary.py +++ b/tryton/gui/window/view_form/view/form_gtk/binary.py @@ -46,7 +46,6 @@ class Binary(WidgetInterface): self.but_new.set_image(img_new) self.but_new.set_relief(gtk.RELIEF_NONE) self.but_new.connect('clicked', self.sig_new) - self.but_new.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_new, _('Select a File...')) self.widget.pack_start(self.but_new, expand=False, fill=False) @@ -57,7 +56,6 @@ class Binary(WidgetInterface): self.but_open.set_image(img_open) self.but_open.set_relief(gtk.RELIEF_NONE) self.but_open.connect('clicked', self.sig_open) - self.but_open.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_open, _('Open...')) self.widget.pack_start(self.but_open, expand=False, fill=False) else: @@ -70,7 +68,6 @@ class Binary(WidgetInterface): self.but_save_as.set_image(img_save_as) self.but_save_as.set_relief(gtk.RELIEF_NONE) self.but_save_as.connect('clicked', self.sig_save_as) - self.but_save_as.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_save_as, _('Save As...')) self.widget.pack_start(self.but_save_as, expand=False, fill=False) @@ -80,7 +77,6 @@ class Binary(WidgetInterface): self.but_remove.set_image(img_remove) self.but_remove.set_relief(gtk.RELIEF_NONE) self.but_remove.connect('clicked', self.sig_remove) - self.but_remove.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_remove, _('Clear')) self.widget.pack_start(self.but_remove, expand=False, fill=False) diff --git a/tryton/gui/window/view_form/view/form_gtk/image.py b/tryton/gui/window/view_form/view/form_gtk/image.py index 7c1f8a2053..dc30c039b9 100644 --- a/tryton/gui/window/view_form/view/form_gtk/image.py +++ b/tryton/gui/window/view_form/view/form_gtk/image.py @@ -48,7 +48,6 @@ class Image(WidgetInterface): self.but_add.set_image(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) self.but_add.connect('clicked', self.sig_add) - self.but_add.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_add, _('Select an Image...')) hbox.pack_start(self.but_add, expand=False, fill=False) @@ -60,7 +59,6 @@ class Image(WidgetInterface): self.but_open.set_image(img_open) self.but_open.set_relief(gtk.RELIEF_NONE) self.but_open.connect('clicked', self.sig_open) - self.but_open.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_open, _('Open...')) hbox.pack_start(self.but_open, expand=False, fill=False) else: @@ -73,7 +71,6 @@ class Image(WidgetInterface): self.but_save_as.set_image(img_save_as) self.but_save_as.set_relief(gtk.RELIEF_NONE) self.but_save_as.connect('clicked', self.sig_save_as) - self.but_save_as.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_save_as, _('Save As...')) hbox.pack_start(self.but_save_as, expand=False, fill=False) @@ -84,7 +81,6 @@ class Image(WidgetInterface): self.but_remove.set_image(img_remove) self.but_remove.set_relief(gtk.RELIEF_NONE) self.but_remove.connect('clicked', self.sig_remove) - self.but_remove.connect('enter-notify-event', self.enter) self.tooltips.set_tip(self.but_remove, _('Clear')) hbox.pack_start(self.but_remove, expand=False, fill=False) diff --git a/tryton/gui/window/view_form/view/form_gtk/interface.py b/tryton/gui/window/view_form/view/form_gtk/interface.py index b8f158d1c1..70a7191b9e 100644 --- a/tryton/gui/window/view_form/view/form_gtk/interface.py +++ b/tryton/gui/window/view_form/view/form_gtk/interface.py @@ -164,10 +164,6 @@ class WidgetInterface(object): def cancel(self): pass - def enter(self, widget, event): - if self.view: - self.view.set_value(focused_widget=True) - class TranslateDialog(NoModal): @@ -299,7 +295,6 @@ class TranslateMixin: button.set_image(img) button.set_relief(gtk.RELIEF_NONE) button.connect('clicked', self.translate) - button.connect('enter-notify-event', self.enter) return button def translate(self, widget): diff --git a/tryton/gui/window/view_form/view/form_gtk/many2many.py b/tryton/gui/window/view_form/view/form_gtk/many2many.py index 89c6eac132..81d9929b91 100644 --- a/tryton/gui/window/view_form/view/form_gtk/many2many.py +++ b/tryton/gui/window/view_form/view/form_gtk/many2many.py @@ -1,6 +1,8 @@ #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 tryton.gui.window.view_form.screen import Screen from interface import WidgetInterface from tryton.gui.window.win_search import WinSearch @@ -10,6 +12,7 @@ import tryton.common as common import gettext from tryton.common import RPCExecute, RPCException from tryton.common.placeholder_entry import PlaceholderEntry +from tryton.common.completion import get_completion, update_completion _ = gettext.gettext @@ -37,15 +40,24 @@ class Many2Many(WidgetInterface): self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.set_property('width_chars', 13) - self.wid_text.connect('activate', self._sig_activate) - self.wid_text.connect('focus-out-event', self._focus_out) + self.wid_text.connect('focus-out-event', lambda *a: self._focus_out()) self.focus_out = True hbox.pack_start(self.wid_text, expand=True, fill=True) + if int(self.attrs.get('completion', 1)): + self.wid_completion = get_completion() + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_completion.connect('action-activated', + self._completion_action_activated) + self.wid_text.set_completion(self.wid_completion) + self.wid_text.connect('changed', self._update_completion) + else: + self.wid_completion = None + self.but_add = gtk.Button() tooltips.set_tip(self.but_add, _('Add existing record')) self.but_add.connect('clicked', self._sig_add) - self.but_add.connect('enter-notify-event', self.enter) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -57,7 +69,6 @@ class Many2Many(WidgetInterface): self.but_remove = gtk.Button() tooltips.set_tip(self.but_remove, _('Remove selected record ')) self.but_remove.connect('clicked', self._sig_remove) - self.but_remove.connect('enter-notify-event', self.enter) img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -95,6 +106,10 @@ class Many2Many(WidgetInterface): return self.wid_text.grab_focus() def on_keypress(self, widget, event): + editable = self.wid_text.get_editable() + activate_keys = [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab] + if not self.wid_completion: + activate_keys.append(gtk.keysyms.Return) if event.keyval == gtk.keysyms.F3: self._sig_add() return False @@ -105,6 +120,13 @@ class Many2Many(WidgetInterface): and widget == self.screen.widget: self._sig_remove() return False + if (widget == self.wid_text + and event.keyval in activate_keys + and editable + and self.wid_text.get_text()): + self._sig_add() + self.wid_text.grab_focus() + return False def destroy(self): self.screen.destroy() @@ -120,11 +142,7 @@ class Many2Many(WidgetInterface): widget.modify_text(gtk.STATE_INSENSITIVE, self.colors['text_color_insensitive']) - def _focus_out(self, *args): - if self.wid_text.get_text(): - self._sig_add() - - def _sig_add(self, *args): + def _sig_add(self, *args, **kwargs): if not self.focus_out: return domain = self.field.domain_get(self.record) @@ -151,7 +169,7 @@ class Many2Many(WidgetInterface): self.screen.display(res_id=ids[0]) self.screen.set_cursor() self.wid_text.set_text('') - if len(ids) != 1 or not value: + if len(ids) != 1 or not value or kwargs.get('win_search', False): WinSearch(self.attrs['relation'], callback, sel_multi=True, ids=ids, context=context, domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), @@ -163,10 +181,6 @@ class Many2Many(WidgetInterface): def _sig_remove(self, *args): self.screen.remove(remove=True) - def _sig_activate(self, *args): - self._sig_add() - self.wid_text.grab_focus() - def _on_activate(self): self._sig_edit() @@ -222,3 +236,44 @@ class Many2Many(WidgetInterface): self.screen.save_tree_state() self.screen.current_view.set_value() return True + + def _completion_match_selected(self, completion, model, iter_): + record_id, = model.get(iter_, 1) + self.screen.load([record_id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + return True + + def _update_completion(self, widget): + if self._readonly: + return + if not self.record: + return + model = self.attrs['relation'] + update_completion(self.wid_text, self.record, self.field, model) + + def _completion_action_activated(self, completion, index): + if index == 0: + self._sig_add(win_search=True) + self.wid_text.grab_focus() + elif index == 1: + model = self.attrs['relation'] + domain = self.field.domain_get(self.record) + context = self.field.context_get(self.record) + + screen = Screen(model, domain, context=context, mode=['form']) + + def callback(result): + self.focus_out = True + if result: + record = screen.current_record + self.screen.load([record.id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() + + self.focus_out = False + WinForm(screen, callback, new=True, save_current=True) diff --git a/tryton/gui/window/view_form/view/form_gtk/many2one.py b/tryton/gui/window/view_form/view/form_gtk/many2one.py index a5fc0d20c3..774cc3b258 100644 --- a/tryton/gui/window/view_form/view/form_gtk/many2one.py +++ b/tryton/gui/window/view_form/view/form_gtk/many2one.py @@ -11,6 +11,7 @@ from tryton.gui.window.win_form import WinForm from tryton.config import CONFIG from tryton.common.popup_menu import populate from tryton.common import RPCExecute, RPCException +from tryton.common.completion import get_completion, update_completion _ = gettext.gettext @@ -34,17 +35,25 @@ class Many2One(WidgetInterface): lambda x, y: self._focus_out()) self.wid_text.connect_after('changed', self.sig_changed) self.changed = True - self.wid_text.connect('activate', self.sig_activate) - self.wid_text.connect_after('focus-out-event', self.sig_activate) self.focus_out = True + if int(self.attrs.get('completion', 1)): + self.wid_completion = get_completion() + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_completion.connect('action-activated', + self._completion_action_activated) + self.wid_text.set_completion(self.wid_completion) + self.wid_text.connect('changed', self._update_completion) + else: + self.wid_completion = None + self.but_open = gtk.Button() img_find = gtk.Image() img_find.set_from_stock('tryton-find', gtk.ICON_SIZE_SMALL_TOOLBAR) self.but_open.set_image(img_find) self.but_open.set_relief(gtk.RELIEF_NONE) self.but_open.connect('clicked', self.sig_edit) - self.but_open.connect('enter-notify-event', self.enter) self.but_open.set_alignment(0.5, 0.5) self.but_new = gtk.Button() @@ -53,7 +62,6 @@ class Many2One(WidgetInterface): self.but_new.set_image(img_new) self.but_new.set_relief(gtk.RELIEF_NONE) self.but_new.connect('clicked', self.sig_new) - self.but_new.connect('enter-notify-event', self.enter) self.but_new.set_alignment(0.5, 0.5) self.widget.pack_end(self.but_new, expand=False, fill=False) @@ -124,7 +132,7 @@ class Many2One(WidgetInterface): def id_from_value(value): return value - def sig_activate(self, widget=None, event=None, key_press=False): + def sig_activate(self): model = self.get_model() if not model or not common.MODELACCESS[model]['read']: return @@ -136,13 +144,10 @@ class Many2One(WidgetInterface): self.focus_out = False if model and not self.has_target(value): - if not key_press and not event and widget: - widget.emit_stop_by_name('activate') if (not self._readonly and (self.wid_text.get_text() - or (self.field.get_state_attrs( - self.record)['required']) - and key_press)): + or self.field.get_state_attrs( + self.record)['required'])): domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) self.wid_text.grab_focus() @@ -208,7 +213,7 @@ class Many2One(WidgetInterface): self.focus_out = True WinForm(screen, callback, new=True, save_current=True) - def sig_edit(self, widget): + def sig_edit(self, *args): model = self.get_model() if not model or not common.MODELACCESS[model]['read']: return @@ -275,6 +280,9 @@ class Many2One(WidgetInterface): def sig_key_press(self, widget, event, *args): editable = self.wid_text.get_editable() + activate_keys = [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab] + if not self.wid_completion: + activate_keys.append(gtk.keysyms.Return) if (event.keyval == gtk.keysyms.F3 and editable and self.but_new.get_property('sensitive')): @@ -284,9 +292,9 @@ class Many2One(WidgetInterface): and self.but_open.get_property('sensitive')): self.sig_edit(widget) return True - elif (event.keyval in (gtk.keysyms.Tab, gtk.keysyms.Return) + elif (event.keyval in activate_keys and editable): - self.sig_activate(widget, event, key_press=True) + self.sig_activate() elif (self.has_target(self.field.get(self.record)) and editable and event.keyval in (gtk.keysyms.Delete, @@ -311,8 +319,9 @@ class Many2One(WidgetInterface): return False def set_value(self, record, field): - # Simulate a focus-out - self.sig_activate() + if field.get_client(record) != self.wid_text.get_text(): + field.set_client(record, self.value_from_id(None, '')) + self.wid_text.set_text('') def set_text(self, value): if not value: @@ -350,3 +359,29 @@ class Many2One(WidgetInterface): gobject.idle_add(populate, menu, self.get_model(), self.id_from_value(value)) return True + + def _completion_match_selected(self, completion, model, iter_): + rec_name, record_id = model.get(iter_, 0, 1) + self.field.set_client(self.record, + self.value_from_id(record_id, rec_name), force_change=True) + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + return True + + def _update_completion(self, widget): + if self._readonly: + return + if not self.record: + return + if self.field.get(self.record) is not None: + return + model = self.get_model() + update_completion(self.wid_text, self.record, self.field, model) + + def _completion_action_activated(self, completion, index): + if index == 0: + self.sig_edit() + elif index == 1: + self.sig_new() diff --git a/tryton/gui/window/view_form/view/form_gtk/one2many.py b/tryton/gui/window/view_form/view/form_gtk/one2many.py index 22d78e210a..0611d022bd 100644 --- a/tryton/gui/window/view_form/view/form_gtk/one2many.py +++ b/tryton/gui/window/view_form/view/form_gtk/one2many.py @@ -2,6 +2,8 @@ #this repository contains the full copyright notices and license terms. import gtk import gettext +import gobject + from interface import WidgetInterface from tryton.gui.window.view_form.screen import Screen from tryton.gui.window.win_search import WinSearch @@ -10,6 +12,7 @@ from tryton.config import CONFIG import tryton.common as common from tryton.common import RPCExecute, RPCException from tryton.common.placeholder_entry import PlaceholderEntry +from tryton.common.completion import get_completion, update_completion _ = gettext.gettext @@ -36,19 +39,28 @@ class One2Many(WidgetInterface): tooltips = common.Tooltips() self.focus_out = True + self.wid_completion = None if attrs.get('add_remove'): self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.set_property('width_chars', 13) - self.wid_text.connect('activate', self._sig_activate) - self.wid_text.connect('focus-out-event', self._focus_out) + self.wid_text.connect('focus-out-event', + lambda *a: self._focus_out()) hbox.pack_start(self.wid_text, expand=True, fill=True) + if int(self.attrs.get('completion', 1)): + self.wid_completion = get_completion() + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_completion.connect('action-activated', + self._completion_action_activated) + self.wid_text.set_completion(self.wid_completion) + self.wid_text.connect('changed', self._update_completion) + self.but_add = gtk.Button() tooltips.set_tip(self.but_add, _('Add existing record')) self.but_add.connect('clicked', self._sig_add) - self.but_add.connect('enter-notify-event', self.enter) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -61,7 +73,6 @@ class One2Many(WidgetInterface): tooltips.set_tip(self.but_remove, _('Remove selected record')) self.but_remove.connect('clicked', self._sig_remove, True) - self.but_remove.connect('enter-notify-event', self.enter) img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -75,7 +86,6 @@ class One2Many(WidgetInterface): self.but_new = gtk.Button() tooltips.set_tip(self.but_new, _('Create a new record ')) self.but_new.connect('clicked', self._sig_new) - self.but_new.connect('enter-notify-event', self.enter) img_new = gtk.Image() img_new.set_from_stock('tryton-new', gtk.ICON_SIZE_SMALL_TOOLBAR) img_new.set_alignment(0.5, 0.5) @@ -86,7 +96,6 @@ class One2Many(WidgetInterface): self.but_open = gtk.Button() tooltips.set_tip(self.but_open, _('Edit selected record ')) self.but_open.connect('clicked', self._sig_edit) - self.but_open.connect('enter-notify-event', self.enter) img_open = gtk.Image() img_open.set_from_stock('tryton-open', gtk.ICON_SIZE_SMALL_TOOLBAR) img_open.set_alignment(0.5, 0.5) @@ -97,7 +106,6 @@ class One2Many(WidgetInterface): self.but_del = gtk.Button() tooltips.set_tip(self.but_del, _('Delete selected record ')) self.but_del.connect('clicked', self._sig_remove, False) - self.but_del.connect('enter-notify-event', self.enter) img_del = gtk.Image() img_del.set_from_stock('tryton-delete', gtk.ICON_SIZE_SMALL_TOOLBAR) img_del.set_alignment(0.5, 0.5) @@ -108,7 +116,6 @@ class One2Many(WidgetInterface): self.but_undel = gtk.Button() tooltips.set_tip(self.but_undel, _('Undelete selected record ')) self.but_undel.connect('clicked', self._sig_undelete) - self.but_undel.connect('enter-notify-event', self.enter) img_undel = gtk.Image() img_undel.set_from_stock('tryton-undo', gtk.ICON_SIZE_SMALL_TOOLBAR) img_undel.set_alignment(0.5, 0.5) @@ -121,7 +128,6 @@ class One2Many(WidgetInterface): self.but_pre = gtk.Button() tooltips.set_tip(self.but_pre, _('Previous')) self.but_pre.connect('clicked', self._sig_previous) - self.but_pre.connect('enter-notify-event', self.enter) img_pre = gtk.Image() img_pre.set_from_stock('tryton-go-previous', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -136,7 +142,6 @@ class One2Many(WidgetInterface): self.but_next = gtk.Button() tooltips.set_tip(self.but_next, _('Next')) self.but_next.connect('clicked', self._sig_next) - self.but_next.connect('enter-notify-event', self.enter) img_next = gtk.Image() img_next.set_from_stock('tryton-go-next', gtk.ICON_SIZE_SMALL_TOOLBAR) img_next.set_alignment(0.5, 0.5) @@ -149,7 +154,6 @@ class One2Many(WidgetInterface): but_switch = gtk.Button() tooltips.set_tip(but_switch, _('Switch')) but_switch.connect('clicked', self.switch_view) - but_switch.connect('enter-notify-event', self.enter) img_switch = gtk.Image() img_switch.set_from_stock('tryton-fullscreen', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -212,6 +216,18 @@ class One2Many(WidgetInterface): if event.keyval == gtk.keysyms.Insert and widget == self.screen.widget: self._sig_undelete(widget) return False + if self.attrs.get('add_remove'): + editable = self.wid_text.get_editable() + activate_keys = [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab] + if not self.wid_completion: + activate_keys.append(gtk.keysyms.Return) + if (widget == self.wid_text + and event.keyval in activate_keys + and editable + and self.wid_text.get_text()): + self._sig_add() + self.wid_text.grab_focus() + return False def destroy(self): self.screen.destroy() @@ -298,7 +314,7 @@ class One2Many(WidgetInterface): return False return True - def _sig_new(self, widget): + def _sig_new(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['create']: return if not self._validate(): @@ -356,15 +372,7 @@ class One2Many(WidgetInterface): def _sig_undelete(self, button): self.screen.unremove() - def _sig_activate(self, *args): - self._sig_add() - self.wid_text.grab_focus() - - def _focus_out(self, *args): - if self.wid_text.get_text(): - self._sig_add() - - def _sig_add(self, *args): + def _sig_add(self, *args, **kwargs): if not self.focus_out: return access = common.MODELACCESS[self.screen.model_name] @@ -399,7 +407,7 @@ class One2Many(WidgetInterface): self.screen.display(res_id=ids[0]) self.screen.set_cursor() self.wid_text.set_text('') - if len(ids) != 1: + if len(ids) != 1 or kwargs.get('win_search', False): WinSearch(self.attrs['relation'], callback, sel_multi=True, ids=ids, context=context, domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), @@ -456,3 +464,35 @@ class One2Many(WidgetInterface): record.modified_fields.setdefault(field.name) record.signal('record-modified') return True + + def _completion_match_selected(self, completion, model, iter_): + record_id, = model.get(iter_, 1) + self.screen.load([record_id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + return True + + def _update_completion(self, widget): + if self._readonly: + return + if not self.record: + return + model = self.attrs['relation'] + domain = self.field.domain_get(self.record) + domain = domain[:] + domain.extend(self.record.expr_eval(self.attrs.get('add_remove'))) + removed_ids = self.field.get_removed_ids(self.record) + domain = ['OR', domain, ('id', 'in', removed_ids)] + update_completion(self.wid_text, self.record, self.field, model, + domain=domain) + + def _completion_action_activated(self, completion, index): + if index == 0: + self._sig_add(win_search=True) + self.wid_text.grab_focus() + elif index == 1: + self._sig_new() diff --git a/tryton/gui/window/view_form/view/form_gtk/reference.py b/tryton/gui/window/view_form/view/form_gtk/reference.py index b546c750ac..8d08de6356 100644 --- a/tryton/gui/window/view_form/view/form_gtk/reference.py +++ b/tryton/gui/window/view_form/view/form_gtk/reference.py @@ -122,8 +122,17 @@ class Reference(Many2One): if not value: field.set_client(record, None) else: - return field.set_client(record, ('', value)) - return super(Reference, self).set_value(record, field) + field.set_client(record, ('', value)) + return + else: + try: + model, name = field.get_client(record) + except (ValueError, TypeError): + model, name = '', '' + if (model != self.get_model() + or name != self.wid_text.get_text()): + field.set_client(record, None) + self.wid_text.set_text('') def set_text(self, value): if value: diff --git a/tryton/gui/window/view_form/view/form_gtk/richtextbox.py b/tryton/gui/window/view_form/view/form_gtk/richtextbox.py index b2b355339e..b6f24e102d 100644 --- a/tryton/gui/window/view_form/view/form_gtk/richtextbox.py +++ b/tryton/gui/window/view_form/view/form_gtk/richtextbox.py @@ -180,7 +180,6 @@ class RichTextBox(TextBox): for local in (3, 6, 11, 14): self.tool_bar.insert(separator(), local) self.widget.pack_start(self.tool_bar, False) - self.widget.connect('enter-notify-event', self.enter) def get_value_markup(self): if self.tools['markup'].get_active(): diff --git a/tryton/gui/window/view_form/view/form_gtk/selection.py b/tryton/gui/window/view_form/view/form_gtk/selection.py index 0f7f58a73a..67eaf1e3d8 100644 --- a/tryton/gui/window/view_form/view/form_gtk/selection.py +++ b/tryton/gui/window/view_form/view/form_gtk/selection.py @@ -14,8 +14,6 @@ class Selection(WidgetInterface): super(Selection, self).__init__(field_name, model_name, attrs=attrs) self.widget = gtk.HBox(spacing=3) - ev_box = gtk.EventBox() - ev_box.connect('enter-notify-event', self.enter) self.entry = gtk.ComboBoxEntry() child = self.entry.child child.set_property('activates_default', True) @@ -27,8 +25,7 @@ class Selection(WidgetInterface): child.connect_after('focus-out-event', self.sig_activate) child.connect('changed', self.send_modified) self.entry.connect('notify::active', lambda *a: self._focus_out()) - ev_box.add(self.entry) - self.widget.pack_start(ev_box) + self.widget.pack_start(self.entry) self.widget.set_focus_chain([child]) self._selection = {} diff --git a/tryton/gui/window/view_form/view/form_gtk/url.py b/tryton/gui/window/view_form/view/form_gtk/url.py index d28540618d..4e4651a022 100644 --- a/tryton/gui/window/view_form/view/form_gtk/url.py +++ b/tryton/gui/window/view_form/view/form_gtk/url.py @@ -19,7 +19,6 @@ class URL(Char): self.button.set_image(img) self.button.set_relief(gtk.RELIEF_NONE) self.button.connect('clicked', self.button_clicked) - self.button.connect('enter-notify-event', self.enter) self.button.set_alignment(0.5, 0.5) self.widget.pack_start(self.button, expand=False, fill=False) self.widget.set_focus_chain([self.entry]) diff --git a/tryton/gui/window/view_form/view/list_gtk/parser.py b/tryton/gui/window/view_form/view/list_gtk/parser.py index b003f0045d..1479aa1a9e 100644 --- a/tryton/gui/window/view_form/view/list_gtk/parser.py +++ b/tryton/gui/window/view_form/view/list_gtk/parser.py @@ -22,7 +22,8 @@ from tryton.common import COLORS, node_attributes, \ import tryton.common as common from tryton.common.cellrendererbutton import CellRendererButton from tryton.common.cellrendererdate import CellRendererDate -from tryton.common.cellrenderertext import CellRendererText +from tryton.common.cellrenderertext import CellRendererText, \ + CellRendererTextCompletion from tryton.common.cellrenderertoggle import CellRendererToggle from tryton.common.cellrenderercombo import CellRendererCombo from tryton.common.cellrendererinteger import CellRendererInteger @@ -30,6 +31,7 @@ from tryton.common.cellrendererfloat import CellRendererFloat from tryton.common.cellrendererbinary import CellRendererBinary from tryton.translate import date_format from tryton.common import RPCExecute, RPCException +from tryton.common.completion import get_completion, update_completion _ = gettext.gettext @@ -391,6 +393,12 @@ class Char(object): def editing_started(self, cell, editable, path): return False + def _get_record_field(self, path): + store = self.treeview.get_model() + record = store.get_value(store.get_iter(path), 0) + field = record.group.fields[self.field_name] + return record, field + class Int(Char): @@ -523,12 +531,6 @@ class Binary(Char): size = record[self.field_name].get_size(record) cell.set_property('size', common.humanize(size) if size else '') - def _get_record_field(self, path): - store = self.treeview.get_model() - record = store.get_value(store.get_iter(path), 0) - field = record.group.fields[self.field_name] - return record, field - def new_binary(self, renderer, path): filename = file_selection(_('Open...')) record, field = self._get_record_field(path) @@ -575,6 +577,13 @@ class Binary(Char): class M2O(Char): + def __init__(self, field_name, model_name, treeview, attrs=None): + super(M2O, self).__init__(field_name, model_name, treeview, + attrs=attrs) + if int(attrs.get('completion', 1)): + self.renderer = CellRendererTextCompletion(self.set_completion) + self.renderer.connect('editing-started', self.editing_started) + def value_from_text(self, record, text, callback=None): field = record.group.fields[self.field_name] if not text: @@ -671,6 +680,57 @@ class M2O(Char): WinSearch(relation, search_callback, sel_multi=False, ids=ids, context=context, domain=domain) + def set_completion(self, entry, path): + if entry.get_completion(): + entry.set_completion(None) + completion = get_completion() + completion.connect('match-selected', self._completion_match_selected, + path) + completion.connect('action-activated', + self._completion_action_activated, path) + entry.set_completion(completion) + entry.connect('key-press-event', self._key_press, path) + entry.connect('changed', self._update_completion, path) + + def _key_press(self, entry, event, path): + record, field = self._get_record_field(path) + if (field.get(record) is not None + and event.keyval in (gtk.keysyms.Delete, + gtk.keysyms.BackSpace)): + entry.set_text('') + field.set_client(record, None) + return False + + def _completion_match_selected(self, completion, model, iter_, path): + record, field = self._get_record_field(path) + rec_name, record_id = model.get(iter_, 0, 1) + field.set_client(record, (record_id, rec_name)) + + completion.get_entry().set_text(rec_name) + completion_model = completion.get_model() + completion_model.clear() + completion_model.search_text = rec_name + return True + + def _update_completion(self, entry, path): + record, field = self._get_record_field(path) + if field.get(record) is not None: + return + model = field.attrs['relation'] + update_completion(entry, record, field, model) + + def _completion_action_activated(self, completion, index, path): + record, field = self._get_record_field(path) + entry = completion.get_entry() + + def callback(): + entry.set_text(field.get_client(record)) + if index == 0: + self.open_remote(record, create=False, changed=True, + text=entry.get_text(), callback=callback) + elif index == 1: + self.open_remote(record, create=True, callback=callback) + class O2O(M2O): pass diff --git a/tryton/gui/window/win_form.py b/tryton/gui/window/win_form.py index 5d5f4ff0a7..a0896840a7 100644 --- a/tryton/gui/window/win_form.py +++ b/tryton/gui/window/win_form.py @@ -57,12 +57,10 @@ class WinForm(NoModal): icon_cancel = gtk.STOCK_CANCEL self.but_cancel = self.win.add_button(icon_cancel, gtk.RESPONSE_CANCEL) - self.but_cancel.set_focus_on_click(False) if new and self.many: self.but_new = self.win.add_button(gtk.STOCK_NEW, gtk.RESPONSE_ACCEPT) - self.but_new.set_focus_on_click(False) self.but_new.set_accel_path('/Form/New', self.accel_group) if self.save_current: @@ -77,7 +75,6 @@ class WinForm(NoModal): else: self.but_ok = self.win.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - self.but_ok.set_focus_on_click(False) self.but_ok.add_accelerator('clicked', self.accel_group, gtk.keysyms.Return, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) self.win.set_default_response(gtk.RESPONSE_OK)