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)))