details: https://code.tryton.org/tryton/commit/9c009cac1512
branch: default
user: Cédric Krier <[email protected]>
date: Tue Mar 03 19:48:24 2026 +0100
description:
Store column widths per occurrence
Closes #14645
diffstat:
sao/src/view/tree.js | 41 +++++-
tryton/tryton/gui/window/view_form/screen/screen.py | 3 +-
tryton/tryton/gui/window/view_form/view/list.py | 34 ++++-
trytond/trytond/ir/message.xml | 4 +
trytond/trytond/ir/ui/view.py | 119 ++++++++++++++-----
trytond/trytond/ir/view/ui_view_tree_width_form.xml | 5 +-
trytond/trytond/ir/view/ui_view_tree_width_list.xml | 1 +
trytond/trytond/model/modelview.py | 19 +-
trytond/trytond/tests/test_ir.py | 30 ++---
9 files changed, 177 insertions(+), 79 deletions(-)
diffs (545 lines):
diff -r 9bd2b4e3d941 -r 9c009cac1512 sao/src/view/tree.js
--- a/sao/src/view/tree.js Tue Mar 03 18:50:22 2026 +0100
+++ b/sao/src/view/tree.js Tue Mar 03 19:48:24 2026 +0100
@@ -455,6 +455,23 @@
}
Sao.Screen.tree_column_optional[this.view_id] = fields;
},
+ _get_column_occurence: function(column) {
+ let occurrence = 0;
+ let found = false;
+ for (let col of this.columns) {
+ if (col.attributes.name == column.attributes.name) {
+ occurrence += 1;
+ if (col === column) {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ occurrence += 1;
+ }
+ return occurrence;
+ },
_set_column_width: function(column) {
let default_width = {
'integer': 8,
@@ -475,9 +492,10 @@
default_width = default_width * 100 * factor + '%';
column.col.data('default-width', default_width);
+ let index = this._get_column_occurence(column) - 1;
let tree_column_width = (
Sao.Screen.tree_column_width[this.screen.model_name] || {});
- let width = tree_column_width[column.attributes.name];
+ let width = (tree_column_width[column.attributes.name] ||
[])[index];
if (width || column.attributes.width) {
if (!width) {
width = column.attributes.width;
@@ -489,9 +507,9 @@
column.col.css('width', width);
},
save_width: function() {
- var widths = {};
+ let fields = {};
for (let column of this.columns) {
- if (!column.get_visible() || !column.attributes.name ||
+ if (!column.attributes.name ||
column instanceof Sao.View.Tree.ButtonColumn) {
continue;
}
@@ -499,25 +517,30 @@
// Use the DOM element to retrieve the exact style set
var width = column.col[0].style.width;
let custom_width = column.col.data('custom-width');
- if (width.endsWith('px') && (width != custom_width)) {
- widths[column.attributes.name] = Number(width.slice(0,
-2));
+ if (!width.endsWith('px') ||
+ (width == custom_width) ||
+ !column.get_visible()) {
+ width = null;
+ } else {
+ width = Number(width.slice(0, -2));
}
+ Sao.setdefault(fields, column.attributes.name, []).push(width);
}
- if (!jQuery.isEmptyObject(widths)) {
+ if (Object.values(fields).some(w => w.some(v => v))) {
let model_name = this.screen.model_name;
let TreeWidth = new Sao.Model('ir.ui.view_tree_width');
TreeWidth.execute(
'set_width',
- [model_name, widths, window.screen.width],
+ [model_name, fields, window.screen.width],
{});
if (Object.prototype.hasOwnProperty.call(
Sao.Screen.tree_column_width, model_name)) {
Object.assign(
Sao.Screen.tree_column_width[model_name],
- widths);
+ fields);
} else {
- Sao.Screen.tree_column_width[model_name] = widths;
+ Sao.Screen.tree_column_width[model_name] = fields;
}
}
},
diff -r 9bd2b4e3d941 -r 9c009cac1512
tryton/tryton/gui/window/view_form/screen/screen.py
--- a/tryton/tryton/gui/window/view_form/screen/screen.py Tue Mar 03
18:50:22 2026 +0100
+++ b/tryton/tryton/gui/window/view_form/screen/screen.py Tue Mar 03
19:48:24 2026 +0100
@@ -38,7 +38,8 @@
# Width of tree columns per model
# It is shared with all connection but it is the price for speed.
- tree_column_width = collections.defaultdict(lambda: {})
+ tree_column_width = collections.defaultdict(
+ lambda: collections.defaultdict(list))
tree_column_optional = {}
def __init__(self, model_name, **attributes):
diff -r 9bd2b4e3d941 -r 9c009cac1512
tryton/tryton/gui/window/view_form/view/list.py
--- a/tryton/tryton/gui/window/view_form/view/list.py Tue Mar 03 18:50:22
2026 +0100
+++ b/tryton/tryton/gui/window/view_form/view/list.py Tue Mar 03 19:48:24
2026 +0100
@@ -434,6 +434,17 @@
column.set_widget(widget)
column.set_alignment(align)
+ def _get_column_occurence(self, column):
+ occurrence = 0
+ for col in self.view.treeview.get_columns():
+ if col.name == column.name:
+ occurrence += 1
+ if col == column:
+ break
+ else:
+ occurrence += 1
+ return occurrence
+
def _set_column_width(self, column, attributes):
default_width = {
'integer': 80,
@@ -454,7 +465,13 @@
computed_width = 80
screen = self.view.screen
- width = screen.tree_column_width[screen.model_name].get(column.name)
+ index = self._get_column_occurence(column) - 1
+ widths = screen.tree_column_width[screen.model_name].get(
+ column.name, [])
+ try:
+ width = widths[index]
+ except IndexError:
+ width = None
if width or attributes.get('width'):
if not width:
width = int(attributes['width'])
@@ -1117,23 +1134,24 @@
def save_width(self):
if not CONFIG['client.save_tree_width']:
return
- fields = {}
+ fields = defaultdict(list)
last_col = None
for col in self.treeview.get_columns():
if col.get_visible():
last_col = col
if not hasattr(col, 'name') or not hasattr(col, 'width'):
continue
- if (col.get_width() != col.width and col.get_visible()
- and not col.get_expand()):
- fields[col.name] = col.get_width()
+ width = col.get_width()
+ if width == col.width or not col.get_visible() or col.get_expand():
+ width = None
+ fields[col.name].append(width)
# Don't set width for last visible columns
# as it depends of the screen size
- if last_col and last_col.name in fields:
- del fields[last_col.name]
+ if last_col and fields[last_col.name]:
+ fields[last_col.name][-1] = None
screen_width, _ = get_monitor_size()
- if fields and any(fields.values()):
+ if any(any(w) for w in fields.values()):
model_name = self.screen.model_name
try:
RPCExecute('model', 'ir.ui.view_tree_width', 'set_width',
diff -r 9bd2b4e3d941 -r 9c009cac1512 trytond/trytond/ir/message.xml
--- a/trytond/trytond/ir/message.xml Tue Mar 03 18:50:22 2026 +0100
+++ b/trytond/trytond/ir/message.xml Tue Mar 03 19:48:24 2026 +0100
@@ -287,6 +287,10 @@
<field name="text">Invalid XML for view "%(name)s".</field>
</record>
+ <record model="ir.message"
id="msg_view_tree_width_field_occurrence_user_unique">
+ <field name="text">A user can set only one width per occurrence of
field.</field>
+ </record>
+
<record model="ir.message" id="msg_action_wrong_wizard_model">
<field name="text">Wrong wizard model in keyword action
"%(name)s".</field>
</record>
diff -r 9bd2b4e3d941 -r 9c009cac1512 trytond/trytond/ir/ui/view.py
--- a/trytond/trytond/ir/ui/view.py Tue Mar 03 18:50:22 2026 +0100
+++ b/trytond/trytond/ir/ui/view.py Tue Mar 03 19:48:24 2026 +0100
@@ -1,15 +1,21 @@
# 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 copy
import json
import logging
import os
+from collections import defaultdict
from lxml import etree
from sql import Literal, Null
+from sql.conditionals import Coalesce
+from sql.operators import Equal
from trytond.cache import Cache, MemoryCache
from trytond.i18n import gettext
-from trytond.model import Index, ModelSQL, ModelView, fields, sequence_ordered
+from trytond.model import (
+ Exclude, Index, ModelSQL, ModelView, fields, sequence_ordered)
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.pyson import PYSON, Bool, Eval, If, PYSONDecoder
@@ -456,10 +462,20 @@
__name__ = 'ir.ui.view_tree_width'
model = fields.Char('Model', required=True)
field = fields.Char('Field', required=True)
+ occurrence = fields.Integer("Occurrence", required=True)
user = fields.Many2One('res.user', 'User', required=True,
ondelete='CASCADE')
- screen_width = fields.Integer("Screen Width")
- width = fields.Integer('Width', required=True)
+ screen_width = fields.Integer(
+ "Screen Width",
+ domain=[
+ ('screen_width', '>=', 0),
+ ])
+ width = fields.Integer(
+ "Width",
+ domain=['OR',
+ ('width', '=', None),
+ ('width', '>=', 0),
+ ])
@classmethod
def __setup__(cls):
@@ -469,12 +485,36 @@
'set_width': RPC(readonly=False),
'reset_width': RPC(readonly=False),
})
+ cls._sql_constraints += [
+ ('field_occurrence_user_unique',
+ Exclude(table,
+ (table.model, Equal),
+ (table.field, Equal),
+ (table.occurrence, Equal),
+ (table.user, Equal),
+ (Coalesce(table.screen_width, -1), Equal)),
+ 'ir.msg_view_tree_width_field_occurrence_user_unique'),
+ ]
cls._sql_indexes.add(
Index(
table,
(table.user, Index.Range()),
(table.model, Index.Equality()),
- (table.field, Index.Equality())))
+ (table.field, Index.Equality()),
+ (table.occurrence, Index.Equality())))
+
+ @classmethod
+ def __register__(cls, module):
+ table_h = cls.__table_handler__(module)
+
+ super().__register__(module)
+
+ # Migration from 7.8: remove required on width
+ table_h.not_null_action('width', 'remove')
+
+ @classmethod
+ def default_occurrence(cls):
+ return 1
def get_rec_name(self, name):
return f'{self.field_ref.rec_name} @ {self.model_ref.rec_name}'
@@ -502,14 +542,15 @@
if width >= screen_width:
break
else:
- screen_width = 0
+ screen_width = None
user = Transaction().user
records = cls.search([
('user', '=', user),
('model', '=', model),
('screen_width', '=', screen_width),
- ])
+ ],
+ order=[('occurrence', 'ASC')])
if not records:
records = cls.search([
@@ -522,58 +563,70 @@
],
order=[
('screen_width', 'DESC NULLS LAST'),
+ ('occurrence', 'ASC'),
])
- widths = {}
+ screen_width = None
+ widths = defaultdict(list)
for width in records:
- if width.field not in widths:
- widths[width.field] = width.width
+ if screen_width is None:
+ screen_width = width.screen_width
+ if screen_width != width.screen_width:
+ break
+ if len(widths[width.field]) + 1 < width.occurrence:
+ for _ in range(
+ width.occurrence - len(widths[width.field]) - 1):
+ widths[width.field].append(None)
+ widths[width.field].insert(width.occurrence, width.width)
return widths
@classmethod
def set_width(cls, model, fields, width):
'''
Set width for the current user on the model.
- fields is a dictionary with key: field name and value: width.
+ fields is dictionary with field name as key and a list of widths
+ as value.
+ width is the screen width.
'''
for screen_width in WIDTH_BREAKPOINTS:
if width >= screen_width:
break
else:
- screen_width = 0
+ screen_width = None
user_id = Transaction().user
records = cls.search([
('user', '=', user_id),
('model', '=', model),
('field', 'in', list(fields.keys())),
- ['OR',
- ('screen_width', '=', screen_width),
- ('screen_width', '=', None),
- ],
- ])
+ ('screen_width', '=', screen_width),
+ ],
+ order=[('occurrence', 'DESC')])
- fields = fields.copy()
- to_save, to_delete = [], []
+ fields = copy.deepcopy(fields)
+ to_save = []
for tree_width in records:
if tree_width.screen_width == screen_width:
- if tree_width.field in fields:
- tree_width.width = fields.pop(tree_width.field)
- to_save.append(tree_width)
- else:
- to_delete.append(tree_width)
+ index = tree_width.occurrence - 1
+ if index <= len(fields[tree_width.field]):
+ width = fields[tree_width.field][index]
+ fields[tree_width.field][index] = None
+ if width is not None:
+ tree_width.width = width
+ to_save.append(tree_width)
- for name, width in fields.items():
- to_save.append(cls(
- user=user_id,
- model=model,
- field=name,
- screen_width=screen_width,
- width=width))
+ for name, widths in fields.items():
+ for occurrence, width in enumerate(widths, start=1):
+ if width is not None:
+ to_save.append(cls(
+ user=user_id,
+ model=model,
+ field=name,
+ occurrence=occurrence,
+ screen_width=screen_width,
+ width=width))
if to_save:
cls.save(to_save)
- if to_delete:
- cls.delete(to_delete)
@classmethod
def reset_width(cls, model, width):
@@ -581,7 +634,7 @@
if width >= screen_width:
break
else:
- screen_width = 0
+ screen_width = None
user_id = Transaction().user
records = cls.search([
diff -r 9bd2b4e3d941 -r 9c009cac1512
trytond/trytond/ir/view/ui_view_tree_width_form.xml
--- a/trytond/trytond/ir/view/ui_view_tree_width_form.xml Tue Mar 03
18:50:22 2026 +0100
+++ b/trytond/trytond/ir/view/ui_view_tree_width_form.xml Tue Mar 03
19:48:24 2026 +0100
@@ -5,7 +5,10 @@
<label name="model_ref"/>
<field name="model_ref"/>
<label name="field_ref"/>
- <field name="field_ref"/>
+ <group id="field" col="-1">
+ <field name="field_ref"/>
+ <field name="occurrence"/>
+ </group>
<label name="user"/>
<field name="user"/>
<label name="screen_width"/>
diff -r 9bd2b4e3d941 -r 9c009cac1512
trytond/trytond/ir/view/ui_view_tree_width_list.xml
--- a/trytond/trytond/ir/view/ui_view_tree_width_list.xml Tue Mar 03
18:50:22 2026 +0100
+++ b/trytond/trytond/ir/view/ui_view_tree_width_list.xml Tue Mar 03
19:48:24 2026 +0100
@@ -4,6 +4,7 @@
<tree>
<field name="model_ref" expand="1"/>
<field name="field_ref" expand="1"/>
+ <field name="occurrence" optional="1"/>
<field name="user" expand="1"/>
<field name="screen_width"/>
<field name="width"/>
diff -r 9bd2b4e3d941 -r 9c009cac1512 trytond/trytond/model/modelview.py
--- a/trytond/trytond/model/modelview.py Tue Mar 03 18:50:22 2026 +0100
+++ b/trytond/trytond/model/modelview.py Tue Mar 03 19:48:24 2026 +0100
@@ -430,7 +430,7 @@
if nodes and depends:
view_depends.extend(depends)
- fields_width = {}
+ fields_width = collections.defaultdict(list)
fields_optional = {}
tree_root = tree.getroottree().getroot()
@@ -497,10 +497,7 @@
width, _ = Transaction().context.get('screen_size', (None, None))
if Transaction().context.get('view_tree_width'):
ViewTreeWidth = pool.get('ir.ui.view_tree_width')
- col_widths = ViewTreeWidth.get_width(cls.__name__, width)
- fields_width.update({fname: w
- for fname, w in col_widths.items()
- if w > 0})
+ fields_width = ViewTreeWidth.get_width(cls.__name__, width)
if view_id:
ViewTreeOptional = pool.get('ir.ui.view_tree_optional')
@@ -571,7 +568,7 @@
ActionWindow = pool.get('ir.action.act_window')
if fields_width is None:
- fields_width = {}
+ fields_width = collections.defaultdict(list)
if fields_optional is None:
fields_optional = {}
if _fields_attrs is None:
@@ -639,9 +636,13 @@
fields_attrs[fname].setdefault('views', {}).update(views)
if type == 'tree':
- if element.get('name') in fields_width:
- element.set(
- 'width', str(fields_width[element.get('name')]))
+ try:
+ width = fields_width[element.get('name')].pop(0)
+ except IndexError:
+ pass
+ else:
+ if width is not None:
+ element.set('width', str(width))
if element.get('optional'):
if element.get('name') in fields_optional:
optional = str(int(
diff -r 9bd2b4e3d941 -r 9c009cac1512 trytond/trytond/tests/test_ir.py
--- a/trytond/trytond/tests/test_ir.py Tue Mar 03 18:50:22 2026 +0100
+++ b/trytond/trytond/tests/test_ir.py Tue Mar 03 19:48:24 2026 +0100
@@ -214,8 +214,8 @@
model = 'ir.ui.view_tree_width'
ViewTreeWidth.set_width(model, {
- 'user': 100,
- 'screen_width': 50,
+ 'user': [100],
+ 'screen_width': [50],
}, 1000)
records = ViewTreeWidth.search([
@@ -242,16 +242,10 @@
'field': 'user',
'screen_width': 992,
'width': 200,
- }, {
- 'user': Transaction().user,
- 'model': model,
- 'field': 'user',
- 'screen_width': 992,
- 'width': 300,
}])
ViewTreeWidth.set_width(model, {
- 'user': 100,
- 'screen_width': 50,
+ 'user': [100],
+ 'screen_width': [50],
}, 1000)
records = ViewTreeWidth.search([
@@ -307,16 +301,16 @@
model = 'ir.ui.view_tree_width'
ViewTreeWidth.set_width(model, {
- 'user': 100,
- 'screen_width': 50,
+ 'user': [100],
+ 'screen_width': [50],
}, 1000)
widths = ViewTreeWidth.get_width(model, 1000)
self.assertEqual(
widths, {
- 'user': 100,
- 'screen_width': 50,
+ 'user': [100],
+ 'screen_width': [50],
})
@with_transaction()
@@ -327,16 +321,16 @@
model = 'ir.ui.view_tree_width'
ViewTreeWidth.set_width(model, {
- 'user': 100,
- 'screen_width': 50,
+ 'user': [100],
+ 'screen_width': [50],
}, 500)
widths = ViewTreeWidth.get_width(model, 1000)
self.assertEqual(
widths, {
- 'user': 100,
- 'screen_width': 50,
+ 'user': [100],
+ 'screen_width': [50],
})
@with_transaction()