details:   https://code.tryton.org/tryton/commit/de353f5eef00
branch:    default
user:      Cédric Krier <[email protected]>
date:      Tue Feb 17 18:10:15 2026 +0100
description:
        Add visual hint on widget of modified field

        All the widget properties used for style are regrouped under a unique 
styled
        widget which simplify the CSS rules.
        For xxx2Many widget, there is not style applied.
        The style is reset when the widget is not linked to a field.

        Closes #14613
diffstat:

 sao/CHANGELOG                                                      |   1 +
 sao/src/view/form.js                                               |  48 
++++-----
 tryton/CHANGELOG                                                   |   1 +
 tryton/tryton/client.py                                            |  24 ++++-
 tryton/tryton/gui/window/view_form/view/form_gtk/binary.py         |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py      |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/char.py           |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py     |   2 +-
 tryton/tryton/gui/window/view_form/view/form_gtk/integer.py        |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py      |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py       |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py |   2 +-
 tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py       |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/reference.py      |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/selection.py      |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py        |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py      |   4 +
 tryton/tryton/gui/window/view_form/view/form_gtk/widget.py         |  38 
++++---
 18 files changed, 112 insertions(+), 48 deletions(-)

diffs (401 lines):

diff -r 24668633fde6 -r de353f5eef00 sao/CHANGELOG
--- a/sao/CHANGELOG     Tue Feb 24 12:41:49 2026 +0100
+++ b/sao/CHANGELOG     Tue Feb 17 18:10:15 2026 +0100
@@ -1,3 +1,4 @@
+* Add visual hint on widget of modified field
 * Add support for Python 3.14
 * Remove support for Python 3.9
 * Remove bower dependency
diff -r 24668633fde6 -r de353f5eef00 sao/src/view/form.js
--- a/sao/src/view/form.js      Tue Feb 24 12:41:49 2026 +0100
+++ b/sao/src/view/form.js      Tue Feb 17 18:10:15 2026 +0100
@@ -1295,6 +1295,10 @@
             var invisible = this.attributes.invisible;
             var required = this.attributes.required;
             if (!field) {
+                if (this._styled_el) {
+                    this._styled_el.removeClass(
+                        ['readonly', 'required', 'has-error', 'has-warning']);
+                }
                 if (readonly === undefined) {
                     readonly = true;
                 }
@@ -1326,24 +1330,18 @@
                 readonly = true;
             }
             this.set_readonly(readonly);
-            if (readonly) {
-                this.el.addClass('readonly');
-            } else {
-                this.el.removeClass('readonly');
-            }
-            var required_el = this._required_el();
             this.set_required(required);
-            if (!readonly && required) {
-                required_el.addClass('required');
-            } else {
-                required_el.removeClass('required');
-            }
-            var invalid = state_attrs.invalid;
-            var invalid_el = this._invalid_el();
-            if (!readonly && invalid) {
-                invalid_el.addClass('has-error');
-            } else {
-                invalid_el.removeClass('has-error');
+            if (this._styled_el) {
+                this._styled_el.toggleClass('readonly', readonly);
+                this._styled_el.toggleClass(
+                    'required', !readonly && required);
+                this._styled_el.toggleClass(
+                    'has-error', !readonly && state_attrs.invalid);
+                this._styled_el.toggleClass(
+                    'has-warning',
+                    (this.record.id >= 0)
+                    && Object.hasOwn(
+                        this.record.modified_fields,this.field_name));
             }
             if (invisible === undefined) {
                 invisible = field.get_state_attrs(this.record).invisible;
@@ -1353,10 +1351,7 @@
             }
             this.set_invisible(invisible);
         },
-        _required_el: function () {
-            return this.el;
-        },
-        _invalid_el: function() {
+        get _styled_el() {
             return this.el;
         },
         get field_name() {
@@ -3476,6 +3471,9 @@
 
             this._popup = false;
         },
+        get _styled_el() {
+            return null;
+        },
         get_access: function(type) {
             var model = this.attributes.relation;
             if (model) {
@@ -4127,6 +4125,9 @@
             });
             this._popup = false;
         },
+        get _styled_el() {
+            return null;
+        },
         get_access: function(type) {
             var model = this.attributes.relation;
             if (model) {
@@ -5167,10 +5168,7 @@
             this._record_id = null;
             this._popup = false;
         },
-        _required_el: function() {
-            return this.wid_text;
-        },
-        _invalid_el: function() {
+        get _styled_el() {
             return this.wid_text;
         },
         add: function() {
diff -r 24668633fde6 -r de353f5eef00 tryton/CHANGELOG
--- a/tryton/CHANGELOG  Tue Feb 24 12:41:49 2026 +0100
+++ b/tryton/CHANGELOG  Tue Feb 17 18:10:15 2026 +0100
@@ -1,3 +1,4 @@
+* Add visual hint on widget of modified field
 * Add support for Python 3.14
 * Remove support for Python 3.9
 
diff -r 24668633fde6 -r de353f5eef00 tryton/tryton/client.py
--- a/tryton/tryton/client.py   Tue Feb 24 12:41:49 2026 +0100
+++ b/tryton/tryton/client.py   Tue Feb 17 18:10:15 2026 +0100
@@ -15,14 +15,30 @@
 
 def main():
     CSS = b"""
-    .readonly entry, .readonly text {
+    entry.readonly,
+    textview.readonly > text {
         background-color: @insensitive_bg_color;
     }
-    .required entry, entry.required, .required text, text.required {
+    entry.required,
+    textview.required > text,
+    checkbutton.required > check,
+    box.required,
+    scrolledwindow.required {
         border-color: darker(@unfocused_borders);
     }
-    .invalid entry, entry.invalid, .invalid text, text.invalid {
-        border-color: @error_color;
+    entry.invalid,
+    textview.invalid > text,
+    checkbutton.invalid > check,
+    box.invalid,
+    scrolledwindow.invalid {
+        border-bottom: 1px solid @error_color;
+    }
+    entry.modified,
+    textview.modified > text,
+    checkbutton.modified > check,
+    box.modified,
+    scrolledwindow.modified {
+        border-bottom: 1px solid lighter(@warning_color);
     }
     label.required {
         font-weight: bold;
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/binary.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py        Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py        Tue Feb 
17 18:10:15 2026 +0100
@@ -173,6 +173,10 @@
         self.widget.pack_start(
             self.toolbar(), expand=False, fill=False, padding=0)
 
+    @property
+    def _styled_widget(self):
+        return self.wid_size
+
     def _readonly_set(self, value):
         self.but_select.set_sensitive(not value)
         self.but_clear.set_sensitive(not value)
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py     Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py     Tue Feb 
17 18:10:15 2026 +0100
@@ -34,6 +34,10 @@
         self.widget.pack_start(self.entry, expand=False, fill=False, padding=0)
 
     @property
+    def _styled_widget(self):
+        return self.entry
+
+    @property
     def real_entry(self):
         return self.entry
 
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/char.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/char.py  Tue Feb 24 
12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/char.py  Tue Feb 17 
18:10:15 2026 +0100
@@ -51,6 +51,10 @@
                 IconFactory.get_pixbuf('tryton-translate', Gtk.IconSize.MENU))
             self.entry.connect('icon-press', self.translate)
 
+    @property
+    def _styled_widget(self):
+        return self.entry.get_child() if self.autocomplete else self.entry
+
     def translate_widget(self):
         entry = Gtk.Entry()
         entry.set_property('activates_default', True)
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py    Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py    Tue Feb 
17 18:10:15 2026 +0100
@@ -521,7 +521,7 @@
         self._popup = False
 
     @property
-    def _invalid_widget(self):
+    def _styleed_widget(self):
         return self.wid_text
 
     def _new_remove_btn(self):
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/integer.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/integer.py       Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/integer.py       Tue Feb 
17 18:10:15 2026 +0100
@@ -34,6 +34,10 @@
                 self.symbol_end, expand=False, fill=False, padding=1)
 
     @property
+    def _styled_widget(self):
+        return self.entry
+
+    @property
     def modified(self):
         if self.record and self.field:
             value = self.get_client_value()
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py     Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py     Tue Feb 
17 18:10:15 2026 +0100
@@ -122,6 +122,10 @@
 
         self._popup = False
 
+    @property
+    def _styled_widget(self):
+        return
+
     def on_keypress(self, widget, event):
         editable = self.wid_text.get_editable()
         activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab]
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py      Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py      Tue Feb 
17 18:10:15 2026 +0100
@@ -49,6 +49,10 @@
 
         self._readonly = False
 
+    @property
+    def _styled_widget(self):
+        return self.wid_text
+
     def get_model(self):
         return self.attrs['relation']
 
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py        
Tue Feb 24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py        
Tue Feb 17 18:10:15 2026 +0100
@@ -13,7 +13,7 @@
     def __init__(self, view, attrs):
         super().__init__(view, attrs)
 
-        if int(attrs.get('yexpand', self.expand)):
+        if int(attrs.get('yexpand', self.expand)) or True:
             self.widget = Gtk.ScrolledWindow()
             self.widget.set_policy(
                 Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py      Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py      Tue Feb 
17 18:10:15 2026 +0100
@@ -184,6 +184,10 @@
 
         self._popup = False
 
+    @property
+    def _styled_widget(self):
+        return
+
     def get_access(self, type_):
         model = self.attrs['relation']
         if model:
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/reference.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/reference.py     Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/reference.py     Tue Feb 
17 18:10:15 2026 +0100
@@ -35,6 +35,10 @@
         self.init_selection()
         self.set_popdown(self.selection, self.widget_combo)
 
+    @property
+    def _styled_widget(self):
+        return self.widget
+
     def get_model(self):
         active = self.widget_combo.get_active()
         if active < 0:
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/selection.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/selection.py     Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/selection.py     Tue Feb 
17 18:10:15 2026 +0100
@@ -35,6 +35,10 @@
         self.init_selection()
         self.set_popdown(self.selection, self.entry)
 
+    @property
+    def _styled_widget(self):
+        return self.mnemonic_widget
+
     def changed(self, combobox):
         def focus_out():
             if combobox.props.window:
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py       Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py       Tue Feb 
17 18:10:15 2026 +0100
@@ -49,6 +49,10 @@
         self.widget.pack_end(
             self.scrolledwindow, expand=True, fill=True, padding=0)
 
+    @property
+    def _styled_widget(self):
+        return self.textview
+
     def _get_textview(self):
         if self.attrs.get('size'):
             textbuffer = TextBufferLimitSize(int(self.attrs['size']))
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py     Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py     Tue Feb 
17 18:10:15 2026 +0100
@@ -23,6 +23,10 @@
         self.widget.pack_start(self.entry, expand=True, fill=True, padding=0)
 
     @property
+    def _styled_widget(self):
+        return self.entry
+
+    @property
     def modified(self):
         if self.record and self.field:
             value = self.entry.get_text()
diff -r 24668633fde6 -r de353f5eef00 
tryton/tryton/gui/window/view_form/view/form_gtk/widget.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/widget.py        Tue Feb 
24 12:41:49 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/widget.py        Tue Feb 
17 18:10:15 2026 +0100
@@ -58,15 +58,8 @@
     def _required_set(self, required):
         pass
 
-    def _invisible_widget(self):
-        return self.widget
-
     @property
-    def _invalid_widget(self):
-        return self.widget
-
-    @property
-    def _required_widget(self):
+    def _styled_widget(self):
         return self.widget
 
     @property
@@ -89,13 +82,12 @@
         return False
 
     def invisible_set(self, value):
-        widget = self._invisible_widget()
         if value and value != '0':
             self.visible = False
-            widget.hide()
+            self.widget.hide()
         else:
             self.visible = True
-            widget.show()
+            self.widget.show()
 
     def _focus_out(self, *args):
         if not self.field:
@@ -106,6 +98,9 @@
 
     def display(self):
         if not self.field:
+            if self._styled_widget:
+                for name in ['readonly', 'required', 'invalid', 'modifed']:
+                    widget_class(self._styled_widget, name, False)
             self._readonly_set(self.attrs.get('readonly', True))
             self.invisible_set(self.attrs.get('invisible', False))
             self._required_set(False)
@@ -115,13 +110,22 @@
         if self.view.screen.readonly:
             readonly = True
         self._readonly_set(readonly)
-        widget_class(self.widget, 'readonly', readonly)
         self._required_set(not readonly and states.get('required', False))
-        widget_class(
-            self._required_widget, 'required',
-            not readonly and states.get('required', False))
-        invalid = states.get('invalid', False)
-        widget_class(self._invalid_widget, 'invalid', not readonly and invalid)
+        if self._styled_widget:
+            widget_class(self._styled_widget, 'readonly', readonly)
+            widget_class(
+                self._styled_widget,
+                'required',
+                not readonly and states.get('required', False))
+            widget_class(
+                self._styled_widget,
+                'invalid',
+                not readonly and states.get('invalid', False))
+            widget_class(
+                self._styled_widget,
+                'modified',
+                self.record.id >= 0
+                and self.field_name in self.record.modified_fields)
         self.invisible_set(self.attrs.get(
                 'invisible', states.get('invisible', False)))
 

Reply via email to