diff --git a/CHANGELOG b/CHANGELOG index 952e76b615..f94fb0660d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Support notification message * Add option to define logging output location * Remove TreeViewControl * Manage optional column diff --git a/tryton/common/__init__.py b/tryton/common/__init__.py index f62a79a07f..e7f7a28083 100644 --- a/tryton/common/__init__.py +++ b/tryton/common/__init__.py @@ -2,16 +2,16 @@ # this repository contains the full copyright notices and license terms. from . import timedelta from .common import ( - COLOR_SCHEMES, MODELACCESS, MODELHISTORY, MODELNAME, TRYTON_ICON, - VIEW_SEARCH, IconFactory, Login, Logout, RPCContextReload, RPCException, - RPCExecute, RPCProgress, Tooltips, apply_label_attributes, ask, - check_version, concurrency, data2pixbuf, date_format, ellipsize, error, - file_open, file_selection, file_write, filter_domain, generateColorscheme, - get_align, get_hostname, get_port, get_sensible_widget, - get_toplevel_window, hex2rgb, highlight_rgb, humanize, idle_add, mailto, - message, node_attributes, process_exception, resize_pixbuf, selection, - setup_window, slugify, sur, sur_3b, timezoned_date, to_xml, - untimezoned_date, userwarning, warning) + COLOR_SCHEMES, MODELACCESS, MODELHISTORY, MODELNAME, MODELNOTIFICATION, + TRYTON_ICON, VIEW_SEARCH, IconFactory, Login, Logout, RPCContextReload, + RPCException, RPCExecute, RPCProgress, Tooltips, apply_label_attributes, + ask, check_version, concurrency, data2pixbuf, date_format, ellipsize, + error, file_open, file_selection, file_write, filter_domain, + generateColorscheme, get_align, get_hostname, get_port, + get_sensible_widget, get_toplevel_window, hex2rgb, highlight_rgb, humanize, + idle_add, mailto, message, node_attributes, process_exception, + resize_pixbuf, selection, setup_window, slugify, sur, sur_3b, + timezoned_date, to_xml, untimezoned_date, userwarning, warning) from .domain_inversion import ( concat, domain_inversion, eval_domain, extract_reference_models, filter_leaf, inverse_leaf, localize_domain, merge, @@ -19,16 +19,16 @@ from .domain_inversion import ( from .environment import EvalEnvironment __all__ = [ - IconFactory, MODELACCESS, MODELHISTORY, MODELNAME, VIEW_SEARCH, - get_toplevel_window, get_sensible_widget, selection, file_selection, - slugify, file_write, file_open, mailto, message, warning, userwarning, sur, - sur_3b, ask, concurrency, error, to_xml, process_exception, Login, Logout, - node_attributes, hex2rgb, highlight_rgb, generateColorscheme, RPCException, - RPCProgress, RPCExecute, RPCContextReload, Tooltips, COLOR_SCHEMES, - filter_domain, timezoned_date, untimezoned_date, humanize, get_hostname, - get_port, resize_pixbuf, data2pixbuf, apply_label_attributes, ellipsize, - get_align, date_format, idle_add, domain_inversion, eval_domain, - localize_domain, merge, inverse_leaf, filter_leaf, - prepare_reference_domain, extract_reference_models, concat, simplify, - unique_value, EvalEnvironment, timedelta, check_version, TRYTON_ICON, - setup_window] + IconFactory, MODELACCESS, MODELHISTORY, MODELNAME, MODELNOTIFICATION, + VIEW_SEARCH, get_toplevel_window, get_sensible_widget, selection, + file_selection, slugify, file_write, file_open, mailto, message, warning, + userwarning, sur, sur_3b, ask, concurrency, error, to_xml, + process_exception, Login, Logout, node_attributes, hex2rgb, highlight_rgb, + generateColorscheme, RPCException, RPCProgress, RPCExecute, + RPCContextReload, Tooltips, COLOR_SCHEMES, filter_domain, timezoned_date, + untimezoned_date, humanize, get_hostname, get_port, resize_pixbuf, + data2pixbuf, apply_label_attributes, ellipsize, get_align, date_format, + idle_add, domain_inversion, eval_domain, localize_domain, merge, + inverse_leaf, filter_leaf, prepare_reference_domain, + extract_reference_models, concat, simplify, unique_value, EvalEnvironment, + timedelta, check_version, TRYTON_ICON, setup_window] diff --git a/tryton/common/common.py b/tryton/common/common.py index c97a1dcf30..a741a3f5d6 100644 --- a/tryton/common/common.py +++ b/tryton/common/common.py @@ -286,6 +286,25 @@ class ModelName: MODELNAME = ModelName() +class ModelNotification: + _depends = {} + + def load_names(self): + try: + self._depends = rpc.execute( + 'model', 'ir.model', 'get_notification', rpc.CONTEXT) + except TrytonServerError: + pass + + def get(self, model): + if not self._depends: + self.load_names() + return self._depends.get(model, []) + + +MODELNOTIFICATION = ModelNotification() + + class ViewSearch(object): searches = {} diff --git a/tryton/gui/main.py b/tryton/gui/main.py index 1a1bfb0e78..8242917117 100644 --- a/tryton/gui/main.py +++ b/tryton/gui/main.py @@ -518,6 +518,7 @@ class Main(Gtk.Application): common.IconFactory.load_icons, common.MODELACCESS.load_models, common.MODELHISTORY.load_history, + common.MODELNOTIFICATION.load_names, common.VIEW_SEARCH.load_searches, ): t = threading.Thread(target=target) diff --git a/tryton/gui/window/form.py b/tryton/gui/window/form.py index cc6d4febf9..4f1e278776 100644 --- a/tryton/gui/window/form.py +++ b/tryton/gui/window/form.py @@ -293,7 +293,7 @@ class Form(TabContent): def sig_logs(self, widget=None): current_record = self.screen.current_record if not current_record or current_record.id < 0: - self.message_info( + self.info_bar_add( _('You have to select one record.'), Gtk.MessageType.INFO) return False @@ -407,10 +407,10 @@ class Form(TabContent): msg = _('Are you sure to remove those records?') if sur(msg): if not self.screen.remove(delete=True, force_remove=True): - self.message_info( + self.info_bar_add( _('Records not removed.'), Gtk.MessageType.ERROR) else: - self.message_info(_('Records removed.'), Gtk.MessageType.INFO) + self.info_bar_add(_('Records removed.'), Gtk.MessageType.INFO) self.screen.count_tab_domain(True) def sig_import(self, widget=None): @@ -467,7 +467,7 @@ class Form(TabContent): if not self.modified_save(): return self.screen.new() - self.message_info() + self.info_bar_clear() self.activate_save() def sig_copy(self, widget=None): @@ -476,7 +476,8 @@ class Form(TabContent): if not self.modified_save(): return if self.screen.copy(): - self.message_info(_('Working now on the duplicated record(s).'), + self.info_bar_add( + _('Working now on the duplicated record(s).'), Gtk.MessageType.INFO) self.screen.count_tab_domain(True) @@ -489,11 +490,11 @@ class Form(TabContent): or self.screen.writable): return if self.screen.save_current(): - self.message_info(_('Record saved.'), Gtk.MessageType.INFO) + self.info_bar_add(_('Record saved.'), Gtk.MessageType.INFO) self.screen.count_tab_domain(True) return True else: - self.message_info( + self.info_bar_add( self.screen.invalid_message(), Gtk.MessageType.ERROR) return False @@ -501,14 +502,14 @@ class Form(TabContent): if not self.modified_save(): return self.screen.display_prev() - self.message_info() + self.info_bar_clear() self.activate_save() def sig_next(self, widget=None): if not self.modified_save(): return self.screen.display_next() - self.message_info() + self.info_bar_clear() self.activate_save() def sig_reload(self, test_modified=True): @@ -529,7 +530,7 @@ class Form(TabContent): set_cursor = True break self.screen.display(set_cursor=set_cursor) - self.message_info() + self.info_bar_clear() self.set_buttons_sensitive() self.activate_save() self.screen.count_tab_domain() @@ -628,7 +629,7 @@ class Form(TabContent): else: msg = "%s/%s" % (name, common.humanize(size)) self.status_label.set_text(msg) - self.message_info() + self.info_bar_clear() self.activate_save() self.refresh_attachment_preview() diff --git a/tryton/gui/window/infobar.py b/tryton/gui/window/infobar.py index 32b055d495..67ba14e43e 100644 --- a/tryton/gui/window/infobar.py +++ b/tryton/gui/window/infobar.py @@ -9,27 +9,31 @@ class InfoBar(object): toolbar = None def create_info_bar(self): - self.info_label = Gtk.Label() + self.__box = Gtk.VBox() + self.__box.show() + self.__messages = set() + return self.__box - self.info_bar = Gtk.InfoBar() - self.info_bar.get_content_area().pack_start( - self.info_label, False, False, 0) - self.info_bar.set_show_close_button(True) - self.info_bar.connect('response', lambda i, r: i.hide()) + def info_bar_add(self, message, type_=Gtk.MessageType.ERROR): + key = (message, type_) + if key not in self.__messages: + info_bar = Gtk.InfoBar() + self.__box.pack_start(info_bar, False, False, 0) + area = info_bar.get_content_area() + area.add(Gtk.Label(label=message)) + info_bar.set_show_close_button(True) + info_bar.connect('response', self.__response, key) + info_bar.set_message_type(type_) + info_bar.show_all() - def response(self, bar, response): - if response == Gtk.ResponseType.CLOSE: - bar.hide() + def __response(self, widget, response, key): + self.__messages.add(key) + self.__box.remove(widget) - def message_info(self, message=None, type_=Gtk.MessageType.ERROR): - if message: - # Work around https://bugzilla.gnome.org/show_bug.cgi?id=710888 - parent = self.info_bar.get_parent() - self.info_bar.unparent() - self.info_bar.set_parent(parent) + def info_bar_refresh(self): + for child in self.__box.get_children(): + self.__box.remove(child) - self.info_label.set_label(message) - self.info_bar.set_message_type(type_) - self.info_bar.show_all() - else: - self.info_bar.hide() + def info_bar_clear(self): + self.info_bar_refresh() + self.__messages.clear() diff --git a/tryton/gui/window/tabcontent.py b/tryton/gui/window/tabcontent.py index 3dce55c019..9c60c9ce79 100644 --- a/tryton/gui/window/tabcontent.py +++ b/tryton/gui/window/tabcontent.py @@ -203,9 +203,8 @@ class TabContent(InfoBar): self.scrolledwindow.show() self.main.pack1(self.scrolledwindow, resize=True, shrink=False) - self.create_info_bar() self.widget.pack_start( - self.info_bar, expand=False, fill=True, padding=0) + self.create_info_bar(), expand=False, fill=True, padding=0) def make_title_bar(self): tooltips = common.Tooltips() diff --git a/tryton/gui/window/view_form/model/group.py b/tryton/gui/window/view_form/model/group.py index 22fb22cff5..2bfc662c44 100644 --- a/tryton/gui/window/view_form/model/group.py +++ b/tryton/gui/window/view_form/model/group.py @@ -524,3 +524,7 @@ class Group(list): self.parent.group.fields[self.child_name].sig_changed(self.parent) self.parent.validate(softvalidation=True) self.parent.group.record_modified() + + def record_notify(self, notifications): + for screen in self.screens: + screen.record_notify(notifications) diff --git a/tryton/gui/window/view_form/model/record.py b/tryton/gui/window/view_form/model/record.py index 7363163a05..581ba498af 100644 --- a/tryton/gui/window/view_form/model/record.py +++ b/tryton/gui/window/view_form/model/record.py @@ -566,9 +566,22 @@ class Record: 'model', self.model_name, 'on_change', values, fieldnames, context=self.get_context()) except RPCException: - return - for change in changes: - self.set_on_change(change) + pass + else: + for change in changes: + self.set_on_change(change) + + notification_fields = common.MODELNOTIFICATION.get(self.model_name) + if set(fieldnames) & set(notification_fields): + values = self._get_on_change_args(notification_fields) + try: + notifications = RPCExecute( + 'model', self.model_name, 'on_change_notify', values, + context=self.get_context()) + except RPCException: + pass + else: + self.group.record_notify(notifications) def on_change_with(self, field_names): field_names = set(field_names) diff --git a/tryton/gui/window/view_form/screen/screen.py b/tryton/gui/window/view_form/screen/screen.py index 9cf90fbb89..2b2c7a2013 100644 --- a/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/gui/window/view_form/screen/screen.py @@ -20,6 +20,7 @@ from tryton.common import ( sur, warning) from tryton.common.domain_parser import DomainParser from tryton.config import CONFIG +from tryton.gui.window.infobar import InfoBar from tryton.gui.window.view_form.model.group import Group from tryton.gui.window.view_form.view import View from tryton.gui.window.view_form.view.screen_container import ScreenContainer @@ -447,6 +448,18 @@ class Screen: if display: self.display() + def record_notify(self, notifications): + for window in self.windows: + if isinstance(window, InfoBar): + window.info_bar_refresh() + for type_, message in notifications: + type_ = { + 'info': Gtk.MessageType.INFO, + 'warning': Gtk.MessageType.WARNING, + 'error': Gtk.MessageType.ERROR, + }.get(type_, Gtk.MessageType.WARNING) + window.info_bar_add(message, type_) + def record_message(self, position, size, max_size, record_id): for window in self.windows: if hasattr(window, 'record_message'): diff --git a/tryton/gui/window/win_form.py b/tryton/gui/window/win_form.py index 0e14e1320b..3bf122dfbe 100644 --- a/tryton/gui/window/win_form.py +++ b/tryton/gui/window/win_form.py @@ -274,9 +274,8 @@ class WinForm(NoModal, InfoBar): scroll.add(self.screen.screen_container.alternate_viewport) - self.create_info_bar() self.win.vbox.pack_start( - self.info_bar, expand=False, fill=True, padding=0) + self.create_info_bar(), expand=False, fill=True, padding=0) self.screen.windows.append(self) if view_type == 'tree': @@ -422,12 +421,12 @@ class WinForm(NoModal, InfoBar): if record: validate = record.pre_validate() if not validate: - self.message_info(self.screen.invalid_message(), - Gtk.MessageType.ERROR) + self.info_bar_add( + self.screen.invalid_message(), Gtk.MessageType.ERROR) self.screen.set_cursor() self.screen.display() return - self.message_info() + self.info_bar_clear() if response_id == Gtk.ResponseType.ACCEPT: self.new() return diff --git a/tryton/gui/window/wizard.py b/tryton/gui/window/wizard.py index 214469ae69..5ecfce953c 100644 --- a/tryton/gui/window/wizard.py +++ b/tryton/gui/window/wizard.py @@ -171,10 +171,10 @@ class Wizard(InfoBar): if (button_attrs.get('validate', True) and not self.screen.current_record.validate()): self.screen.display(set_cursor=True) - self.message_info( + self.info_bar_add( self.screen.invalid_message(), Gtk.MessageType.ERROR) return - self.message_info() + self.info_bar_clear() self.state = state self.process() @@ -242,9 +242,8 @@ class Wizard(InfoBar): self.widget.pack_start( self.scrolledwindow, expand=True, fill=True, padding=0) - self.create_info_bar() self.widget.pack_start( - self.info_bar, expand=False, fill=True, padding=0) + self.create_info_bar(), expand=False, fill=True, padding=0) class WizardForm(Wizard, TabContent):