From b06c9083e7ed5bd81fa5df24e9437da9ad26d3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 11 Dec 2023 19:28:39 +0100 Subject: [PATCH] Prevent opening multiple popup window from the same widget Closes #12819 --- sao/src/view/form.js | 82 ++++++++++++++++++- .../view_form/view/form_gtk/dictionary.py | 7 ++ .../view_form/view/form_gtk/many2many.py | 24 ++++-- .../view_form/view/form_gtk/many2one.py | 34 +++++--- .../view_form/view/form_gtk/one2many.py | 31 +++++-- .../window/view_form/view/list_gtk/widget.py | 14 +++- 6 files changed, 163 insertions(+), 29 deletions(-) diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 8c05130080..7db93caa22 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -2521,6 +2521,7 @@ function eval_pyson(value){ } this.el.change(this.focus_out.bind(this)); this._readonly = false; + this._popup = false; }, get_screen: function(search) { var domain = this.field.get_domain(this.record); @@ -2703,6 +2704,11 @@ function eval_pyson(value){ this.focus(); return; } + if (this._popup) { + return; + } else { + this._popup = true; + } if (this.has_target(value)) { var m2o_id = this.id_from_value(record.field_get(this.field_name)); @@ -2727,6 +2733,7 @@ function eval_pyson(value){ value, true); }); } + this._popup = false; }; screen.switch_view().done(() => { screen.load([m2o_id]); @@ -2750,6 +2757,7 @@ function eval_pyson(value){ this.record.field_set_client(this.field_name, value, true); } + this._popup = false; }; var parser = new Sao.common.DomainParser(); new Sao.Window.Search( @@ -2768,12 +2776,18 @@ function eval_pyson(value){ }); return; } + this._popup = false; }, new_: function(defaults=null) { var model = this.get_model(); if (!model || ! Sao.common.MODELACCESS.get(model).create) { return; } + if (this._popup) { + return; + } else { + this._popup = true; + } var screen = this.get_screen(true); if (defaults) { defaults = jQuery.extend({}, defaults); @@ -2791,6 +2805,7 @@ function eval_pyson(value){ this.record.field_set_client(this.field_name, value); }); } + this._popup = false; }; screen.switch_view().done(() => { var win = new Sao.Window.Form(screen, callback, { @@ -2846,6 +2861,11 @@ function eval_pyson(value){ var value = record.field_get(this.field_name); var sao_model = new Sao.Model(model); + if (this._popup) { + return; + } else { + this._popup = true; + } if (model && !this.has_target(value)) { var text = this.entry.val(); if (!this._readonly && (text || @@ -2865,6 +2885,7 @@ function eval_pyson(value){ } else { this.entry.val(''); } + this._popup = false; }; var parser = new Sao.common.DomainParser(); var win = new Sao.Window.Search( @@ -2882,8 +2903,10 @@ function eval_pyson(value){ title: this.attributes.string, exclude_field: this.attributes.relation_field, }); + return; } } + this._popup = false; }, _set_completion: function() { if (this.wid_completion) { @@ -3296,6 +3319,8 @@ function eval_pyson(value){ // Use keydown to not receive focus-in TAB this.wid_text.on('keydown', this.key_press.bind(this)); } + + this._popup = false; }, get_access: function(type) { var model = this.attributes.relation; @@ -3491,6 +3516,12 @@ function eval_pyson(value){ domain = ['OR', domain, ['id', 'in', removed_ids]]; var text = this.wid_text.val(); + if (this._popup) { + return; + } else { + this._popup = true; + } + var sequence = this._sequence(); const callback = result => { @@ -3512,6 +3543,7 @@ function eval_pyson(value){ this.screen.set_cursor(); }); this.wid_text.val(''); + this._popup = false; }; var parser = new Sao.common.DomainParser(); var order = this.field.get_search_order(this.record); @@ -3558,12 +3590,18 @@ function eval_pyson(value){ }); }, new_single: function(defaults=null) { + if (this._popup) { + return; + } else { + this._popup = true; + } var sequence = this._sequence(); const update_sequence = () => { if (sequence) { this.screen.group.set_sequence( sequence, this.screen.new_position); } + this._popup = false; }; if (this.screen.current_view.creatable) { this.screen.new_().then(update_sequence); @@ -3585,6 +3623,12 @@ function eval_pyson(value){ var product = {}; var screen = this.screen; + if (this._popup) { + return; + } else { + this._popup = true; + } + screen.new_(false).then(first => { first.default_get(defaults).then(default_ => { first.set_default(default_); @@ -3622,6 +3666,7 @@ function eval_pyson(value){ }; const make_product = () => { + this._popup = false; screen.group.remove(first, true); if (jQuery.isEmptyObject(product)) { return; @@ -3681,7 +3726,14 @@ function eval_pyson(value){ return this.validate().then(() => { var record = this.screen.current_record; if (record) { - var win = new Sao.Window.Form(this.screen, function() {}); + if (this._popup) { + return; + } else { + this._popup = true; + } + var win = new Sao.Window.Form(this.screen, () => { + this._popup = false; + }); } }); }, @@ -3884,6 +3936,7 @@ function eval_pyson(value){ this.prm = this.screen.switch_view('tree').done(() => { this.content.append(this.screen.screen_container.el); }); + this._popup = false; }, get_access: function(type) { var model = this.attributes.relation; @@ -3993,6 +4046,12 @@ function eval_pyson(value){ var order = this.field.get_search_order(this.record); var value = this.entry.val(); + if (this._popup) { + return; + } else { + this._popup = true; + } + const callback = result => { if (!jQuery.isEmptyObject(result)) { var ids = []; @@ -4004,6 +4063,7 @@ function eval_pyson(value){ this.screen.display(); } this.entry.val(''); + this._popup = false; }; var parser = new Sao.common.DomainParser(); var win = new Sao.Window.Search(this.attributes.relation, @@ -4068,6 +4128,11 @@ function eval_pyson(value){ if (jQuery.isEmptyObject(this.screen.current_record)) { return; } + if (this._popup) { + return; + } else { + this._popup = true; + } // Create a new screen that is not linked to the parent otherwise // on the save of the record will trigger the save of the parent var screen = this._get_screen_form(); @@ -4082,6 +4147,7 @@ function eval_pyson(value){ } }); } + this._popup = false; }; screen.switch_view().done(() => { screen.load([this.screen.current_record.id]); @@ -4091,6 +4157,11 @@ function eval_pyson(value){ }); }, new_: function(defauts=null) { + if (this._popup) { + return; + } else { + this._popup = true; + } var screen = this._get_screen_form(); if (defaults) { defaults = jQuery.extend({}, defaults); @@ -4105,6 +4176,7 @@ function eval_pyson(value){ this.screen.group.load([record.id], true); } this.entry.val(''); + this._popup = false; }; screen.switch_view().done(() => { new Sao.Window.Form(screen, callback, { @@ -4801,6 +4873,7 @@ function eval_pyson(value){ this._readonly = false; this._record_id = null; + this._popup = false; }, _required_el: function() { return this.wid_text; @@ -4813,6 +4886,12 @@ function eval_pyson(value){ var value = this.wid_text.val(); var domain = this.field.get_domain(this.record); + if (this._popup) { + return; + } else { + this._popup = true; + } + const callback = result => { if (!jQuery.isEmptyObject(result)) { var ids = result.map(function(e) { @@ -4821,6 +4900,7 @@ function eval_pyson(value){ this.add_new_keys(ids); } this.wid_text.val(''); + this._popup = false; }; var parser = new Sao.common.DomainParser(); diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py index a1dfaa5388..d1750633e1 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py @@ -470,6 +470,7 @@ class DictWidget(Widget): self._readonly = False self._record_id = None + self._popup = False @property def _invalid_widget(self): @@ -491,10 +492,16 @@ class DictWidget(Widget): value = self.wid_text.get_text() domain = self.field.domain_get(self.record) + if self._popup: + return + else: + self._popup = True + def callback(result): if result: self.add_new_keys([r[0] for r in result]) self.wid_text.set_text('') + self._popup = False win = WinSearch(self.schema_model, callback, sel_multi=True, context=context, domain=domain, new=False) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py index cb8c25a751..e577e87bd5 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py @@ -48,7 +48,6 @@ class Many2Many(Widget): self.wid_text.set_placeholder_text(_('Search')) self.wid_text.set_property('width_chars', 13) self.wid_text.connect('focus-out-event', self._focus_out) - self.focus_out = True hbox.pack_start(self.wid_text, expand=True, fill=True, padding=0) if int(self.attrs.get('completion', 1)): @@ -113,6 +112,8 @@ class Many2Many(Widget): self.screen.widget.connect('key_press_event', self.on_keypress) self.wid_text.connect('key_press_event', self.on_keypress) + self._popup = False + def on_keypress(self, widget, event): editable = self.wid_text.get_editable() activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab] @@ -161,8 +162,6 @@ class Many2Many(Widget): return int(self.attrs.get('create', 1)) and self.get_access('create') def _sig_add(self, *args): - if not self.focus_out: - return domain = self.field.domain_get(self.record) add_remove = self.record.expr_eval(self.attrs.get('add_remove')) if add_remove: @@ -171,15 +170,18 @@ class Many2Many(Widget): order = self.field.get_search_order(self.record) value = self.wid_text.get_text() - self.focus_out = False + if self._popup: + return + else: + self._popup = True def callback(result): - self.focus_out = True if result: ids = [x[0] for x in result] self.screen.load(ids, modified=True) self.screen.set_cursor() self.wid_text.set_text('') + self._popup = False win = WinSearch(self.attrs['relation'], callback, sel_multi=True, context=context, domain=domain, order=order, view_ids=self.attrs.get('view_ids', '').split(','), @@ -215,6 +217,10 @@ class Many2Many(Widget): def _sig_edit(self): if not self.screen.current_record: return + if self._popup: + return + else: + self._popup = True # Create a new screen that is not linked to the parent otherwise on the # save of the record will trigger the save of the parent screen = self._get_screen_form() @@ -231,22 +237,26 @@ class Many2Many(Widget): self.screen.current_record.modified_fields.setdefault('id') # Force a display to clear the CellCache self.screen.display() + self._popup = False WinForm(screen, callback) def _sig_new(self, defaults=None): + if self._popup: + return + else: + self._popup = True screen = self._get_screen_form() defaults = defaults.copy() if defaults is not None else {} defaults['rec_name'] = self.wid_text.get_text() 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._popup = False - self.focus_out = False WinForm( screen, callback, new=True, save_current=True, defaults=defaults) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py index 292373e1ed..4269002eab 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py @@ -37,7 +37,7 @@ class Many2One(Widget): lambda x, y: self._focus_out()) self.wid_text.connect('changed', self.sig_changed) self.changed = True - self.focus_out = True + self._popup = False if int(self.attrs.get('completion', 1)): self.wid_text.connect('changed', self._update_completion) @@ -103,13 +103,16 @@ class Many2One(Widget): model = self.get_model() if not model or not common.MODELACCESS[model]['read']: return - if not self.focus_out or not self.field: + if not self.field: return self.changed = False value = self.field.get(self.record) model = self.get_model() - self.focus_out = False + if self._popup: + return + else: + self._popup = True if model and not self.has_target(value): if (not self._readonly and (self.wid_text.get_text() @@ -126,7 +129,7 @@ class Many2One(Widget): self.value_from_id(*result[0]), force_change=True) else: self.wid_text.set_text('') - self.focus_out = True + self._popup = False self.changed = True win = WinSearch(model, callback, sel_multi=False, @@ -139,7 +142,7 @@ class Many2One(Widget): win.screen.search_filter(quote(text)) win.show() return - self.focus_out = True + self._popup = False self.changed = True return @@ -164,7 +167,10 @@ class Many2One(Widget): def sig_new(self, defaults=None): if not self.create_access: return - self.focus_out = False + if self._popup: + return + else: + self._popup = True screen = self.get_screen(search=True) defaults = defaults.copy() if defaults is not None else {} defaults['rec_name'] = self.wid_text.get_text() @@ -174,7 +180,7 @@ class Many2One(Widget): self.field.set_client(self.record, self.value_from_id(screen.current_record.id, screen.current_record.rec_name())) - self.focus_out = True + self._popup = False WinForm( screen, callback, new=True, save_current=True, defaults=defaults) @@ -184,10 +190,9 @@ class Many2One(Widget): model = self.get_model() if not model or not common.MODELACCESS[model]['read']: return - if not self.focus_out or not self.field: + if not self.field: return self.changed = False - self.focus_out = False value = self.field.get(self.record) if (icon_pos == Gtk.EntryIconPosition.SECONDARY @@ -196,9 +201,12 @@ class Many2One(Widget): self.field.set_client(self.record, self.value_from_id(None, '')) self.wid_text.set_text('') self.changed = True - self.focus_out = True return + if self._popup: + return + else: + self._popup = True if self.has_target(value): m2o_id = self.id_from_value(self.field.get(self.record)) screen = self.get_screen() @@ -211,7 +219,7 @@ class Many2One(Widget): self.value_from_id(screen.current_record.id, screen.current_record.rec_name()), force_change=True) - self.focus_out = True + self._popup = False self.changed = True WinForm(screen, callback, save_current=True) return @@ -225,7 +233,7 @@ class Many2One(Widget): if result: self.field.set_client(self.record, self.value_from_id(*result[0]), force_change=True) - self.focus_out = True + self._popup = False self.changed = True win = WinSearch(model, callback, sel_multi=False, context=context, domain=domain, order=order, @@ -236,7 +244,7 @@ class Many2One(Widget): win.screen.search_filter(quote(text)) win.show() return - self.focus_out = True + self._popup = False self.changed = True def sig_key_press(self, widget, event, *args): diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py index 49a02c1e7e..aaeaafb8f0 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py @@ -75,7 +75,6 @@ class One2Many(Widget): hbox.pack_start(Gtk.VSeparator(), expand=False, fill=True, padding=0) - self.focus_out = True self.wid_completion = None if attrs.get('add_remove'): @@ -182,6 +181,8 @@ class One2Many(Widget): if self.attrs.get('add_remove'): self.wid_text.connect('key_press_event', self.on_keypress) + self._popup = False + def get_access(self, type_): model = self.attrs['relation'] if model: @@ -370,12 +371,17 @@ class One2Many(Widget): self._new_single(defaults) def _new_single(self, defaults=None): + if self._popup: + return + else: + self._popup = True sequence = self._sequence() def update_sequence(): if sequence: self.screen.group.set_sequence( field=sequence, position=self.screen.new_position) + self._popup = False if self.screen.current_view.creatable: self.screen.new() @@ -392,6 +398,10 @@ class One2Many(Widget): fields = self.attrs['product'].split(',') product = {} + if self._popup: + return + else: + self._popup = True first = self.screen.new(default=False) default = first.default_get(defaults=defaults) first.set_default(default) @@ -420,6 +430,7 @@ class One2Many(Widget): win_search.show() def make_product(): + self._popup = False self.screen.group.remove(first, remove=True) if not product: return @@ -448,7 +459,14 @@ class One2Many(Widget): return record = self.screen.current_record if record: - WinForm(self.screen, lambda a: None) + if self._popup: + return + else: + self._popup = True + + def callback(result): + self._popup = False + WinForm(self.screen, callback) def _sig_next(self, widget): if not self._validate(): @@ -475,8 +493,6 @@ class One2Many(Widget): self.screen.unremove() def _sig_add(self, *args): - if not self.focus_out: - return if not self.write_access or not self.read_access: return self.view.set_value() @@ -487,12 +503,14 @@ class One2Many(Widget): domain = ['OR', domain, ('id', 'in', removed_ids)] text = self.wid_text.get_text() - self.focus_out = False + if self._popup: + return + else: + self._popup = True sequence = self._sequence() def callback(result): - self.focus_out = True if result: ids = [x[0] for x in result] self.screen.load(ids, modified=True) @@ -501,6 +519,7 @@ class One2Many(Widget): field=sequence, position=self.screen.new_position) self.screen.set_cursor() self.wid_text.set_text('') + self._popup = False order = self.field.get_search_order(self.record) win = WinSearch(self.attrs['relation'], callback, sel_multi=True, diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py index a11e5eeb42..587be2e676 100644 --- a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -802,6 +802,7 @@ class M2O(GenericText): if renderer is None and int(attrs.get('completion', 1)): renderer = partial(CellRendererTextCompletion, self.set_completion) super(M2O, self).__init__(view, attrs, renderer=renderer) + self._popup = False def get_model(self, record, field): return self.attrs['relation'] @@ -830,7 +831,8 @@ class M2O(GenericText): if model and common.get_toplevel_window().get_focus(): field = record[self.attrs['name']] win = self.search_remote(record, field, text, callback=callback) - win.show() + if win: + win.show() def editing_started(self, cell, editable, path): super(M2O, self).editing_started(cell, editable, path) @@ -899,7 +901,9 @@ class M2O(GenericText): domain = field.domain_get(record) context = field.get_context(record) if not create and changed: - self.search_remote(record, field, text, callback=callback).show() + win = self.search_remote(record, field, text, callback=callback) + if win: + win.show() return target_id = self.id_from_value(field.get(record)) @@ -939,6 +943,11 @@ class M2O(GenericText): access = common.MODELACCESS[model] create_access = int(self.attrs.get('create', 1)) and access['create'] + if self._popup: + return + else: + self._popup = True + def search_callback(found): value = None if found: @@ -946,6 +955,7 @@ class M2O(GenericText): field.set_client(record, value) if callback: callback() + self._popup = False win = WinSearch(model, search_callback, sel_multi=False, context=context, domain=domain, order=order, view_ids=self.attrs.get('view_ids', '').split(','),